Skip to content

Commit b30ef72

Browse files
committed
double precision mode
TICO-DCO-1.0-Signed-off-by: s.malakhov <s.malakhov@partner.samsung.com>
1 parent 7543d59 commit b30ef72

7 files changed

Lines changed: 660 additions & 90 deletions

File tree

tico/quantization/algorithm/gptq/gptq.py

Lines changed: 242 additions & 55 deletions
Large diffs are not rendered by default.

tico/quantization/algorithm/gptq/llama_quantizer.py

Lines changed: 247 additions & 15 deletions
Large diffs are not rendered by default.

tico/quantization/algorithm/gptq/quant.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def configure(
4949
maxshrink=0.8,
5050
trits=False,
5151
sensitivity=None,
52+
mse_tolerance=1e-5,
5253
):
5354
self.maxq = torch.tensor(2**bits - 1)
5455
self.perchannel = perchannel
@@ -58,6 +59,7 @@ def configure(
5859
self.grid = grid
5960
self.maxshrink = maxshrink
6061
self.sensitivity = sensitivity
62+
self.mse_tolerance = mse_tolerance
6163
if trits:
6264
self.maxq = torch.tensor(-1)
6365

@@ -209,7 +211,10 @@ def _update_best_params(self, best, err, scale1, zero1):
209211
Returns:
210212
Updated best error values
211213
"""
212-
tmp = err < best
214+
# Relative tolerance: only update if new error is significantly better.
215+
# This prevents tiny float noise (from batch-size-dependent Hessian
216+
# rounding) from flipping the winning scale/zero.
217+
tmp = err < best * (1 - self.mse_tolerance)
213218
if torch.any(tmp):
214219
best[tmp] = err[tmp]
215220
self.scale[tmp] = scale1[tmp]
@@ -226,7 +231,7 @@ def _grid_search(self, x, xmin, xmax, compute_error_fn):
226231
compute_error_fn: Function that takes (x, scale1, zero1) and returns error tensor
227232
"""
228233
dev = x.device
229-
best = torch.full([x.shape[0]], float("inf"), device=dev)
234+
best = torch.full([x.shape[0]], float("inf"), device=dev, dtype=x.dtype)
230235
for i in range(int(self.maxshrink * self.grid)):
231236
p = 1 - i / self.grid
232237
scale1, zero1 = self._compute_shrink_params(p, xmin, xmax)

tico/quantization/algorithm/gptq/quantizer.py

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,63 @@ def __init__(self, config: GPTQConfig):
107107
# Reference to original model for use_orig_model_inference and GPTQv2
108108
self.orig_model: Optional[torch.nn.Module] = None
109109

110+
@staticmethod
111+
def _cast_to_double(obj):
112+
"""Recursively cast all tensors in a list/dict/tensor to float64."""
113+
if isinstance(obj, torch.Tensor):
114+
return obj.double()
115+
if isinstance(obj, (list, tuple)):
116+
return type(obj)(GPTQQuantizer._cast_to_double(o) for o in obj)
117+
if isinstance(obj, dict):
118+
return {k: GPTQQuantizer._cast_to_double(v) for k, v in obj.items()}
119+
return obj
120+
121+
def _run_layer_forward_double_precision(
122+
self,
123+
layer: torch.nn.Module,
124+
args: List[Any],
125+
kwargs: Dict[str, Any],
126+
double_precision: bool,
127+
):
128+
"""Run a layer forward, optionally in float64 for batch-size-independence.
129+
130+
When double_precision=True, temporarily casts the layer and its inputs to float64
131+
so that GPU matmul tiling differences don't produce different results
132+
for different batch sizes. Outputs are kept in float64 to avoid losing
133+
precision when they become the next layer's input.
134+
"""
135+
if not double_precision:
136+
return layer(*args, **kwargs)
137+
138+
# Save original dtypes and cast layer to double
139+
orig_dtypes: Dict[str, torch.dtype] = {}
140+
for name, param in layer.named_parameters():
141+
orig_dtypes[name] = param.dtype
142+
param.data = param.data.double()
143+
for name, buf in layer.named_buffers():
144+
orig_dtypes[f"buf:{name}"] = buf.dtype
145+
buf.data = buf.data.double()
146+
147+
# Cast inputs to double
148+
args_d = self._cast_to_double(args)
149+
kwargs_d = self._cast_to_double(kwargs)
150+
151+
outs = layer(*args_d, **kwargs_d)
152+
153+
# Keep outputs in float64 — casting to float32 would introduce
154+
# batch-size-dependent rounding when these outputs become the
155+
# next layer's input, defeating the purpose of double precision.
156+
if isinstance(outs, tuple):
157+
outs = outs[0]
158+
159+
# Restore original dtypes
160+
for name, param in layer.named_parameters():
161+
param.data = param.data.to(orig_dtypes[name])
162+
for name, buf in layer.named_buffers():
163+
buf.data = buf.data.to(orig_dtypes[f"buf:{name}"])
164+
165+
return outs
166+
110167
def _resolve_weight_bits(
111168
self,
112169
gptq_conf: GPTQConfig,
@@ -335,7 +392,12 @@ def convert(self, model):
335392
cache_kwargs_batch = move_to_device(cache_kwargs_batch, device)
336393

337394
orig_layer = orig_layers[l_idx].to(device)
338-
orig_layer(*cache_args_batch, **cache_kwargs_batch)
395+
if gptq_conf.double_precision:
396+
self._run_layer_forward_double_precision(
397+
orig_layer, cache_args_batch, cache_kwargs_batch, True
398+
)
399+
else:
400+
orig_layer(*cache_args_batch, **cache_kwargs_batch)
339401
orig_layer.cpu()
340402

341403
fp_inputs_cache.clear_hook()
@@ -346,10 +408,10 @@ def convert(self, model):
346408

347409
gptq: Dict[str, GPTQ] = {}
348410
for name in subset:
349-
gptq[name] = GPTQ(subset[name])
411+
full_module_name = module_name[subset[name]]
412+
gptq[name] = GPTQ(subset[name], double_precision=gptq_conf.double_precision, layer_name=full_module_name)
350413
gptq[name].saturation_threshold = gptq_conf.saturation_threshold
351414
gptq[name].saturation_min_batches = gptq_conf.saturation_min_batches
352-
full_module_name = module_name[subset[name]]
353415
weight_bits = self._resolve_weight_bits(
354416
gptq_conf,
355417
full_module_name=full_module_name,
@@ -369,6 +431,7 @@ def convert(self, model):
369431
sym=gptq_conf.symmetric,
370432
mse=gptq_conf.mse,
371433
sensitivity=cur_sensitivity,
434+
mse_tolerance=gptq_conf.mse_tolerance,
372435
)
373436

374437
# GPTQv2: Assign native_inp from FPInputsCache
@@ -413,7 +476,12 @@ def _hook(_, inp, out):
413476
)
414477
cache_kwargs_batch = move_to_device(cache_kwargs_batch, device)
415478

416-
layer(*cache_args_batch, **cache_kwargs_batch)
479+
if gptq_conf.double_precision:
480+
self._run_layer_forward_double_precision(
481+
layer, cache_args_batch, cache_kwargs_batch, True
482+
)
483+
else:
484+
layer(*cache_args_batch, **cache_kwargs_batch)
417485

418486
# Remove handles
419487
for h in handles:
@@ -435,6 +503,7 @@ def _hook(_, inp, out):
435503
adaptive_percdamp=gptq_conf.adaptive_percdamp,
436504
cond_threshold_good=gptq_conf.cond_threshold_good,
437505
use_iterate=gptq_conf.use_iterate,
506+
actorder_precision=gptq_conf.actorder_precision,
438507
)
439508
quantizers[full_module_name] = gptq[name].quantizer
440509
gptq[name].free()
@@ -463,9 +532,14 @@ def _hook(_, inp, out):
463532
fp_cache_args_batch = gather_single_batch_from_list(fp_inps, batch_idx)
464533
fp_cache_args_batch = move_to_device(fp_cache_args_batch, device)
465534
orig_layer = orig_layers[l_idx].to(device)
466-
fp_outs = orig_layer(*fp_cache_args_batch, **cache_kwargs_batch)
535+
if gptq_conf.double_precision:
536+
fp_outs = self._run_layer_forward_double_precision(
537+
orig_layer, fp_cache_args_batch, cache_kwargs_batch, True
538+
)
539+
else:
540+
fp_outs = orig_layer(*fp_cache_args_batch, **cache_kwargs_batch)
541+
fp_outs = fp_outs[0] if isinstance(fp_outs, tuple) else fp_outs
467542
orig_layer.cpu()
468-
fp_outs = fp_outs[0] if isinstance(fp_outs, tuple) else fp_outs
469543
# Update inputs for next iteration.
470544
if len(fp_inps) > 0:
471545
if hasattr(fp_outs, "to") and hasattr(
@@ -477,16 +551,28 @@ def _hook(_, inp, out):
477551
else:
478552
fp_inps[0][batch_idx] = fp_outs
479553

480-
if orig_layers is None or gptq_conf.gptq_v2 is True:
481-
outs = layer(*cache_args_batch, **cache_kwargs_batch)
554+
if gptq_conf.double_precision:
555+
if orig_layers is None or gptq_conf.gptq_v2 is True:
556+
outs = self._run_layer_forward_double_precision(
557+
layer, cache_args_batch, cache_kwargs_batch, True
558+
)
559+
else:
560+
orig_layer = orig_layers[l_idx].to(device)
561+
outs = self._run_layer_forward_double_precision(
562+
orig_layer, cache_args_batch, cache_kwargs_batch, True
563+
)
564+
orig_layer.cpu()
482565
else:
483-
orig_layer = orig_layers[l_idx].to(device)
484-
outs = orig_layer(*cache_args_batch, **cache_kwargs_batch)
485-
orig_layer.cpu()
486-
# LLaMA's decoder layer return type differs across Transformers versions:
487-
# some return a tuple (hidden_states, ...), others return just a tensor.
488-
# This line ensures we always take the first element when it's a tuple.
489-
outs = outs[0] if isinstance(outs, tuple) else outs
566+
if orig_layers is None or gptq_conf.gptq_v2 is True:
567+
outs = layer(*cache_args_batch, **cache_kwargs_batch)
568+
else:
569+
orig_layer = orig_layers[l_idx].to(device)
570+
outs = orig_layer(*cache_args_batch, **cache_kwargs_batch)
571+
orig_layer.cpu()
572+
# LLaMA's decoder layer return type differs across Transformers versions:
573+
# some return a tuple (hidden_states, ...), others return just a tensor.
574+
# This line ensures we always take the first element when it's a tuple.
575+
outs = outs[0] if isinstance(outs, tuple) else outs
490576
# Update inputs for next iteration.
491577
if len(self.cache_args) > 0:
492578
if hasattr(outs, "to") and hasattr(
@@ -557,8 +643,8 @@ def _quantize_lm_head(self, model, quantizers):
557643
self.cache_args[0][batch_idx] = move_to_cpu(hidden_states)
558644

559645
layer = model.lm_head
560-
gptq = GPTQ(layer)
561646
full_module_name = "lm_head"
647+
gptq = GPTQ(layer, double_precision=gptq_conf.double_precision, layer_name=full_module_name)
562648
weight_bits = self._resolve_weight_bits(
563649
gptq_conf,
564650
full_module_name=full_module_name,
@@ -578,6 +664,7 @@ def _quantize_lm_head(self, model, quantizers):
578664
sym=gptq_conf.symmetric,
579665
mse=gptq_conf.mse,
580666
sensitivity=cur_sensitivity,
667+
mse_tolerance=gptq_conf.mse_tolerance,
581668
)
582669

583670
# Hook to collect (inp, out) for GPTQ with optional weights
@@ -628,6 +715,7 @@ def _hook(_, inp, out):
628715
adaptive_percdamp=gptq_conf.adaptive_percdamp,
629716
cond_threshold_good=gptq_conf.cond_threshold_good,
630717
use_iterate=gptq_conf.use_iterate,
718+
actorder_precision=gptq_conf.actorder_precision,
631719
)
632720
quantizers[f"lm_head"] = gptq.quantizer
633721
gptq.free()

tico/quantization/config/gptq.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,24 @@ class GPTQConfig(BaseConfig):
6060
mse: str | None = None
6161
sensitivity: dict[str, torch.Tensor] | None = None
6262

63+
# Relative tolerance for MSE grid search: a new scale/zero candidate only
64+
# wins if its error is at least mse_tolerance better (relatively) than the
65+
# current best. Prevents tiny float noise from flipping the winning
66+
# scale/zero, making results more batch-size-independent.
67+
mse_tolerance: float = 1e-5
68+
6369
# GPTQ.fasterquant params (algorithm hyperparams)
6470
percdamp: float = 0.01
6571
groupsize: int = -1
6672
actorder: bool = True
6773
static_groups: bool = False
6874

75+
# Precision threshold for actorder sorting. Diagonal values of the Hessian
76+
# are rounded to this precision grid before argsort, so differences smaller
77+
# than this threshold are treated as noise (ties broken by original index
78+
# via stable=True). This makes actorder more stable
79+
actorder_precision: float = 1e-2
80+
6981
# use this option to stabilize GPTQ for deep models
7082
use_orig_model_inference: bool = False
7183

@@ -94,6 +106,12 @@ class GPTQConfig(BaseConfig):
94106
# is checked. Ensures r_eff has stabilized past the initial transient.
95107
saturation_min_batches: int = 4
96108

109+
# Use float64 (double) for Hessian accumulation to make GPTQ results
110+
# batch-size-independent. Float32 accumulation causes different rounding
111+
# depending on how samples are grouped into batches. Default: False
112+
# (float32, backward compatible). Enable for reproducible results.
113+
double_precision: bool = False
114+
97115
@property
98116
def name(self) -> str:
99117
return "gptq"

tico/quantization/config/llama_gptq.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,24 @@ class LlamaGPTQConfig(BaseConfig):
6969
mse: str | None = None
7070
sensitivity: dict[str, torch.Tensor] | None = None
7171

72+
# Relative tolerance for MSE grid search: a new scale/zero candidate only
73+
# wins if its error is at least mse_tolerance better (relatively) than the
74+
# current best. Prevents tiny float noise from flipping the winning
75+
# scale/zero, making results more batch-size-independent.
76+
mse_tolerance: float = 1e-5
77+
7278
# GPTQ.fasterquant params (algorithm hyperparams)
7379
percdamp: float = 0.01
7480
groupsize: int = -1
7581
actorder: bool = True
7682
static_groups: bool = False
7783

84+
# Precision threshold for actorder sorting. Diagonal values of the Hessian
85+
# are rounded to this precision grid before argsort, so differences smaller
86+
# than this threshold are treated as noise (ties broken by original index
87+
# via stable=True). This makes actorder more stable
88+
actorder_precision: float = 1e-2
89+
7890
# use this option to stabilize GPTQ for deep models
7991
use_orig_model_inference: bool = False
8092

@@ -111,6 +123,12 @@ class LlamaGPTQConfig(BaseConfig):
111123
# is checked. Ensures r_eff has stabilized past the initial transient.
112124
saturation_min_batches: int = 4
113125

126+
# Use float64 (double) for Hessian accumulation to make GPTQ results
127+
# batch-size-independent. Float32 accumulation causes different rounding
128+
# depending on how samples are grouped into batches. Default: False
129+
# (float32, backward compatible). Enable for reproducible results.
130+
double_precision: bool = False
131+
114132
@property
115133
def name(self) -> str:
116134
return "llama_gptq"

tico/quantization/wrapq/examples/quantize_full_qmodel_with_gptq.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,17 @@ def build_parser() -> argparse.ArgumentParser:
480480
),
481481
)
482482

483+
parser.add_argument(
484+
"--gptq_double_precision",
485+
action="store_true",
486+
default=False,
487+
help=(
488+
"Use float64 (double) for Hessian accumulation to make GPTQ results "
489+
"stable. Float32 accumulation causes different rounding "
490+
"depending on how samples are grouped into batches. Default: False "
491+
"(float32, backward compatible). Enable for exact/reproducible results."
492+
),
493+
)
483494
parser.add_argument(
484495
"--gptq_saturation_threshold",
485496
type=float,
@@ -972,6 +983,7 @@ def build_gptq_config(
972983
use_subgroup_runner=args.llama_gptq_use_subgroup_runner,
973984
sample_weights=sample_weights,
974985
saturation_threshold=args.gptq_saturation_threshold,
986+
double_precision=args.gptq_double_precision,
975987
)
976988
return config
977989
else:
@@ -991,6 +1003,7 @@ def build_gptq_config(
9911003
use_iterate=args.gptq_use_iterate,
9921004
sample_weights=sample_weights,
9931005
saturation_threshold=args.gptq_saturation_threshold,
1006+
double_precision=args.gptq_double_precision,
9941007
)
9951008
return config
9961009

@@ -1001,6 +1014,10 @@ def save_model_to(
10011014
"""
10021015
Export and save the whole quantized model in circle format.
10031016
"""
1017+
if not hasattr(q_m, "wrapped"):
1018+
print("Saving whole model circle is supported only for PTQ quantized model")
1019+
return
1020+
10041021
q_m.eval()
10051022
q_m.cpu()
10061023
model_name = "model_prefill" if prefill_decode else "model"
@@ -3184,6 +3201,7 @@ def get_ptq_model_name(model, args):
31843201

31853202
# --- Calibration options ------------------------------------------------
31863203
parts.append(str(args.nsamples_for_qcalibration))
3204+
parts.append(f"bs{args.batch}")
31873205
if (
31883206
args.calibration_samples_to_use is not None
31893207
and args.calibration_samples_to_use != args.nsamples_for_qcalibration
@@ -4513,8 +4531,12 @@ def save_requested_artifacts(q_m, tokenizer, calib_inputs, args, sample_weights=
45134531
print(f"Saving calibration dataset to {save_path.resolve()}")
45144532
torch.save({"calib_inputs": calib_inputs, "sample_weights": sample_weights}, save_path)
45154533

4534+
# When --no_PTQ is used, q_m has no PTQ wrapper so there is no .wrapped
4535+
# attribute. Use q_m directly in that case.
4536+
inner = q_m if args.no_PTQ else q_m.wrapped
4537+
45164538
if should_save(args, "ptq_checkpoint"):
4517-
save_path = output_dir / get_ptq_model_name(q_m.wrapped, args)
4539+
save_path = output_dir / get_ptq_model_name(inner, args)
45184540
print(f"Saving PTQ checkpoint to {save_path.resolve()}")
45194541
torch.save(q_m, save_path)
45204542

@@ -4528,7 +4550,7 @@ def save_requested_artifacts(q_m, tokenizer, calib_inputs, args, sample_weights=
45284550
)
45294551

45304552
if should_save(args, "circle_per_layer"):
4531-
max_seq_len = args.max_seq_len or q_m.wrapped.config.max_position_embeddings
4553+
max_seq_len = args.max_seq_len or inner.config.max_position_embeddings
45324554
save_layers_to(
45334555
q_m,
45344556
max_seq_len,

0 commit comments

Comments
 (0)