Skip to content

Commit 6098f79

Browse files
Keep parameter dtype through ZeRO-3 weight quantization (deepspeedai#8215)
Fixes deepspeedai#7775 The quantizer op is fp16-only in both directions. quantize_kernel in csrc/quantization/pt_binding.cpp casts input_vals.data_ptr() to __half* whatever the tensor dtype actually is, and dequantize is bound as dequantize<__half> so it always allocates an fp16 output. CUDAQuantizer passed parameters straight through, so with bf16 enabled and zero_quantized_weights set, ZeRO-3 quantized bf16 bits reinterpreted as fp16 and then restored param.data as fp16. Training fails on the resulting dtype mismatch, which is the BERT failure in the issue. The bit reinterpretation on the way in is the quieter half of the bug: values are wrong before the dtype mismatch is ever noticed. CUDAQuantizer.quantize now converts to fp16 on the way into the kernel so the values are read correctly, and dequantize takes an optional dtype so each caller can ask for the dtype its parameter actually has. The five call sites in the gather paths pass the parameter dtype. Omitting the argument keeps the previous fp16 return, so no other caller changes behavior. Precision is not a concern here, since the values are being quantized to int8 regardless. Verification: added a parametrized test to tests/unit/runtime/zero/test_zeropp.py that stands in for the compiled op with a stub asserting the fp16 contract, and checks a bf16 and an fp16 parameter both round trip in their own dtype. It passes and it fails against the unmodified code on both halves of the fix. Run on CPU, since the test does not need the compiled op. The root cause is verified by reading csrc/quantization/pt_binding.cpp, not by running on a GPU. yapf and flake8 are clean on the changed files. --------- Signed-off-by: Aditya Singh <adisin650@gmail.com> Co-authored-by: Masahiro Tanaka <81312776+tohtana@users.noreply.github.com>
1 parent 32d51a1 commit 6098f79

2 files changed

Lines changed: 68 additions & 14 deletions

File tree

deepspeed/runtime/zero/partition_parameters.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ def __init__(self, param: Parameter) -> None:
6464
raise RuntimeError(f"expected param {param.ds_summary()} to be available")
6565

6666
if hasattr(param.ds_tensor, "ds_quant_scale"):
67-
param.data = Init.quantizer_module.dequantize(param.ds_tensor.data, param.ds_tensor.ds_quant_scale).to(
68-
device=get_accelerator().current_device_name(), non_blocking=True).view(param.ds_shape)
67+
param.data = Init.quantizer_module.dequantize(param.ds_tensor.data,
68+
param.ds_tensor.ds_quant_scale,
69+
dtype=param.dtype).to(
70+
device=get_accelerator().current_device_name(),
71+
non_blocking=True).view(param.ds_shape)
6972
else:
7073
param.data = param.ds_tensor.data.to(device=get_accelerator().current_device_name(),
7174
non_blocking=True).view(param.ds_shape)
@@ -87,8 +90,11 @@ def __init__(self, params: List[Parameter]) -> None:
8790
if param.ds_status != ZeroParamStatus.INFLIGHT:
8891
raise RuntimeError(f"expected param {param.ds_summary()} to not be available")
8992
if hasattr(param.ds_tensor, "ds_quant_scale"):
90-
param.data = Init.quantizer_module.dequantize(param.ds_tensor.data, param.ds_tensor.ds_quant_scale).to(
91-
device=get_accelerator().current_device_name(), non_blocking=True).view(param.ds_shape)
93+
param.data = Init.quantizer_module.dequantize(param.ds_tensor.data,
94+
param.ds_tensor.ds_quant_scale,
95+
dtype=param.dtype).to(
96+
device=get_accelerator().current_device_name(),
97+
non_blocking=True).view(param.ds_shape)
9298
else:
9399
param.data = param.ds_tensor.data.to(device=get_accelerator().current_device_name(),
94100
non_blocking=True).view(param.ds_shape)
@@ -708,8 +714,10 @@ def wait(self, handle_dependency=True) -> None:
708714
self.__original_dtype).to(self.__param.device)
709715
elif self.__quantization:
710716
instrument_w_nvtx(self.__quantization.quant_handle.wait)()
711-
self.__param.data = self.__quantization.backend.dequantize(
712-
self.__quantization.quantized_param, self.__quantization.scale_buffer).to(self.__param.device)
717+
self.__param.data = self.__quantization.backend.dequantize(self.__quantization.quantized_param,
718+
self.__quantization.scale_buffer,
719+
dtype=self.__param.dtype).to(
720+
self.__param.device)
713721
self.__param.ds_status = ZeroParamStatus.AVAILABLE
714722

715723

@@ -747,6 +755,9 @@ def wait(self, handle_dependency=True) -> None:
747755

748756
if self.quantization:
749757
instrument_w_nvtx(self.quantization.quant_handle.wait)()
758+
# No dtype here on purpose. A quantized coalesced bucket is not grouped by dtype the
759+
# way the non-quantized path is, so params[0].dtype is not necessarily the dtype of
760+
# the rest of the bucket. Each slice is cast to its own parameter's dtype below.
750761
flat_tensor = self.quantization.backend.dequantize(
751762
self.quantization.quantized_param, self.quantization.scale_buffer).to(self.params[0].device)
752763

@@ -865,12 +876,18 @@ def quantize(self, param, groups=None):
865876
assert param.numel(
866877
) > groups, f"Adaptive grouping algorithm cannot find a group size for input tensor of size {param.numel()}"
867878
self.group_size_cache[param.numel()] = groups
868-
return self.quantizer_cuda_module.quantize(param.to(get_accelerator().device_name()), groups, 8,
869-
self.quantizer_cuda_module.Symmetric)
879+
# The CUDA kernel reads its input through a __half* and always writes fp16 back out, so a bf16
880+
# parameter would be reinterpreted bit-for-bit and silently corrupted. Convert on the way in and
881+
# let the caller ask for its own dtype back on the way out.
882+
param = param.to(get_accelerator().device_name(), dtype=torch.half)
883+
return self.quantizer_cuda_module.quantize(param, groups, 8, self.quantizer_cuda_module.Symmetric)
870884

871-
def dequantize(self, quantized_param, scale):
872-
return self.quantizer_cuda_module.dequantize(quantized_param, scale, scale.numel(), 8,
873-
self.quantizer_cuda_module.Symmetric)
885+
def dequantize(self, quantized_param, scale, dtype=None):
886+
dequantized = self.quantizer_cuda_module.dequantize(quantized_param, scale, scale.numel(), 8,
887+
self.quantizer_cuda_module.Symmetric)
888+
if dtype is not None and dequantized.dtype != dtype:
889+
dequantized = dequantized.to(dtype)
890+
return dequantized
874891

875892

876893
def _no_gather_coalesced(params: Iterable[Parameter]) -> AllGatherCoalescedHandle:
@@ -2071,7 +2088,9 @@ def _allgather_params_coalesced(self, param_list, hierarchy=0, quantize=False):
20712088
for i, param in enumerate(param_list):
20722089
gathered_tensor = allgather_params[i]
20732090
if quantize:
2074-
gathered_tensor = self.quantizer_module.dequantize(gathered_tensor, allgather_quantize_scale[i])
2091+
gathered_tensor = self.quantizer_module.dequantize(gathered_tensor,
2092+
allgather_quantize_scale[i],
2093+
dtype=param.dtype)
20752094
param.data = gathered_tensor.narrow(0, 0, param.ds_numel).view(param.ds_shape).data
20762095

20772096
# guarantee the communication to be completed
@@ -2130,7 +2149,7 @@ def _allgather_params_sequential(self, param_list, hierarchy=0):
21302149
scale_partitions[partition_rank],
21312150
group=self.get_partition_dp_group(param),
21322151
async_op=False)
2133-
flat_tensor = self.quantizer_module.dequantize(flat_tensor, flat_scale_tensor)
2152+
flat_tensor = self.quantizer_module.dequantize(flat_tensor, flat_scale_tensor, dtype=param.dtype)
21342153

21352154
param.data = flat_tensor.narrow(0, 0, param.ds_numel).view(param.ds_shape)
21362155

tests/unit/runtime/zero/test_zeropp.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import deepspeed
1313

1414
from deepspeed.runtime.zero.config import DeepSpeedZeroConfig
15-
from deepspeed.runtime.zero.partition_parameters import Init, ZeroParamStatus
15+
from deepspeed.runtime.zero.partition_parameters import CUDAQuantizer, Init, ZeroParamStatus
1616

1717
import torch.nn as nn
1818
import torch
@@ -41,6 +41,41 @@ def test_zero_hpz_partition_size_config():
4141
assert config.zero_hpz_partition_size == 4
4242

4343

44+
class Fp16OnlyQuantizerModule:
45+
"""Stand-in for the compiled QuantizerBuilder op.
46+
47+
It mirrors the two properties of the real kernel that matter here: quantize() reads its input
48+
through a __half*, and dequantize() always allocates an fp16 output tensor.
49+
"""
50+
51+
Symmetric = 0
52+
53+
def quantize(self, param, groups, num_bits, quant_type):
54+
assert param.dtype == torch.half, f"the quantize kernel reads fp16, got {param.dtype}"
55+
return param.to(torch.int8), torch.ones(groups, dtype=torch.float32, device=param.device)
56+
57+
def dequantize(self, quantized_param, scale, num_groups, num_bits, quant_type):
58+
return quantized_param.to(torch.half)
59+
60+
61+
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.half])
62+
def test_cuda_quantizer_round_trips_parameter_dtype(monkeypatch, dtype):
63+
"""zero_quantized_weights must hand a parameter back in its own dtype.
64+
65+
The quantizer op is fp16-only, so under a bf16 config the weights used to come back as fp16 and
66+
break the forward pass with a dtype mismatch. See #7775.
67+
"""
68+
monkeypatch.setattr(CUDAQuantizer, "quantizer_cuda_module", Fp16OnlyQuantizerModule())
69+
quantizer = CUDAQuantizer()
70+
71+
param = torch.randn(4096, dtype=dtype)
72+
quantized_param, scale = quantizer.quantize(param)
73+
assert quantized_param.dtype == torch.int8
74+
75+
dequantized = quantizer.dequantize(quantized_param, scale, dtype=param.dtype)
76+
assert dequantized.dtype == dtype
77+
78+
4479
def test_zero_hpz_small_param_secondary_shard_without_overlap(monkeypatch):
4580

4681
class _FakeAccelerator:

0 commit comments

Comments
 (0)