Skip to content

Commit 0408b43

Browse files
authored
fix(qqq): per-device Hessian partials for balanced multi-GPU calibration (#2979)
QQQ.add_batch previously accumulated a single H tensor on self.dev, which failed when balanced calibration placed module replicas on different GPUs. - Add per-device Hessian partials and a thread-safe accumulate step, mirroring the GPTQ path. - Materialize the global Hessian at quantize time from the merged partials, scaling by 2 / total_token_count (matching GPTQ math). - Add QQQ to ModelTest's post-quant validation backend dispatch. - Introduce tests/models/test_llama3_2_qqq.py with fast-mode arc/gsm8k baselines. Fixes #2115.
1 parent 24525da commit 0408b43

3 files changed

Lines changed: 156 additions & 9 deletions

File tree

gptqmodel/quantization/qqq.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
# Contact: qubitium@modelcloud.ai, x.com/qubitium
55

66
import math
7+
import threading
78
import time
8-
from typing import Optional
9+
from typing import Dict, Optional
910

1011
import torch
1112
import transformers
@@ -16,6 +17,7 @@
1617
from ..quantization.config import FallbackStrategy, SmoothMSE
1718
from ..quantization.quantizer import HF_OPTIMUM
1819
from ..utils import setup_logger
20+
from ..utils.device import get_device
1921
from .fallback_smooth import mse_optimal_quant, smooth_block
2022
from .gptq import get_number_of_rows_and_cols
2123
from .npu_linalg import npu_inverse_cholesky_factor
@@ -242,9 +244,10 @@ def __init__(self, module: nn.Module, qcfg: Optional[QuantizeConfig] = None):
242244
self.fallback = self.qcfg.fallback
243245
self.expected_nsamples: Optional[float] = None
244246

245-
self.H = torch.zeros((self.columns, self.columns),
246-
dtype=torch.float32,
247-
device = self.dev)
247+
self.lock = threading.Lock()
248+
self.H: Optional[torch.Tensor] = None
249+
self._device_hessian_partials: Dict[torch.device, torch.Tensor] = {}
250+
self._device_sample_counts: Dict[torch.device, int] = {}
248251

249252
@staticmethod
250253
def _validate_module(module):
@@ -424,7 +427,6 @@ def add_batch(self, inp, out):
424427
self.out1 = out
425428
if len(inp.shape) == 2:
426429
inp = inp.unsqueeze(0)
427-
tmp = inp.shape[0]
428430
if isinstance(self.layer, nn.Linear) or isinstance(
429431
self.layer, transformers.Conv1D
430432
):
@@ -441,13 +443,50 @@ def add_batch(self, inp, out):
441443
inp = unfold(inp)
442444
inp = inp.permute([1, 0, 2])
443445
inp = inp.flatten(1)
444-
self.H *= self.nsamples / (self.nsamples + tmp)
445-
self.nsamples += tmp
446-
inp = math.sqrt(2 / self.nsamples) * inp.float()
446+
447+
dev = torch.device(get_device(inp))
448+
batch_token_size = inp.shape[1]
449+
inp = inp.float()
447450
if self._tp_pad_cols:
448451
pad = inp.new_zeros((self._tp_pad_cols, inp.shape[1]))
449452
inp = torch.cat((inp, pad), dim=0)
450-
self.H += inp.matmul(inp.t())
453+
454+
xtx = inp.matmul(inp.t())
455+
456+
with self.lock:
457+
self.fwd_counter += 1
458+
existing = self._device_hessian_partials.get(dev)
459+
if existing is None:
460+
self._device_hessian_partials[dev] = xtx.to(dtype=torch.float32).detach()
461+
else:
462+
existing.add_(xtx.to(dtype=torch.float32))
463+
self._device_sample_counts[dev] = self._device_sample_counts.get(dev, 0) + batch_token_size
464+
self.nsamples += batch_token_size
465+
466+
def materialize_hessian(self, target_device: Optional[torch.device] = None) -> torch.Tensor:
467+
if target_device is None:
468+
target_device = self.dev
469+
target_device = torch.device(target_device)
470+
471+
total_samples = sum(self._device_sample_counts.values())
472+
if total_samples == 0:
473+
self.H = torch.zeros((self.columns, self.columns), dtype=torch.float32, device=target_device)
474+
self.nsamples = 0
475+
return self.H
476+
477+
result = torch.zeros((self.columns, self.columns), dtype=torch.float32, device=target_device)
478+
for partial in self._device_hessian_partials.values():
479+
if partial.device != target_device or partial.dtype != torch.float32:
480+
result.add_(partial.to(device=target_device, dtype=torch.float32))
481+
else:
482+
result.add_(partial)
483+
484+
result.mul_(2.0 / float(total_samples))
485+
self.H = result
486+
self.nsamples = total_samples
487+
self._device_hessian_partials.clear()
488+
self._device_sample_counts.clear()
489+
return self.H
451490

452491
@torch.inference_mode()
453492
def quantize(
@@ -459,6 +498,8 @@ def quantize(
459498

460499
resolved_strategy = resolve_fallback_strategy(self.fallback)
461500

501+
self.materialize_hessian()
502+
462503
percdamp = self.qcfg.damp_percent
463504
groupsize = self.qcfg.group_size
464505
actorder = self.qcfg.desc_act
@@ -696,6 +737,8 @@ def free(self):
696737
self.inp1 = None
697738
self.out1 = None
698739
self.H = None
740+
self._device_hessian_partials.clear()
741+
self._device_sample_counts.clear()
699742
self.Losses = None
700743
self.Trace = None
701744
if hasattr(self, "quantizer"):

tests/models/model_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,8 @@ def perform_post_quant_validation(self, model_path, trust_remote_code=False):
10071007
compare_backends = (BACKEND.MARLIN,)
10081008
else:
10091009
compare_backends = (self.LOAD_BACKEND,)
1010+
elif format_family == FORMAT.QQQ:
1011+
compare_backends = (self.LOAD_BACKEND,)
10101012
else:
10111013
compare_backends = (BACKEND.MARLIN, BACKEND.GEMM)
10121014
fallback_backend = None

tests/models/test_llama3_2_qqq.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# SPDX-FileCopyrightText: 2024-2025 ModelCloud.ai
2+
# SPDX-FileCopyrightText: 2024-2025 qubitium@modelcloud.ai
3+
# SPDX-License-Identifier: Apache-2.0
4+
# Contact: qubitium@modelcloud.ai, x.com/qubitium
5+
6+
import os
7+
8+
from model_test import ModelTest
9+
10+
from gptqmodel import BACKEND
11+
from gptqmodel.quantization import FORMAT, METHOD
12+
13+
14+
# QQQ equivalent of tests/models/test_llama3_2.py.
15+
#
16+
# Fast-mode post-quant baselines (eager attention, group_size=128, 4bit QQQ):
17+
# gsm8k_platinum_cot :: acc,num 0.4574
18+
# arc_challenge :: acc 0.3225
19+
# arc_challenge :: acc_norm 0.3541
20+
class TestLlama3_2_QQQ(ModelTest):
21+
NATIVE_MODEL_ID = "/monster/data/model/Llama-3.2-1B-Instruct"
22+
SAVE_PATH = os.environ.get(
23+
"GPTQMODEL_LLAMA3_2_QQQ_SAVE_PATH",
24+
"/tmp/llama3_2_qqq_saved_ckpt",
25+
)
26+
DELETE_QUANTIZED_MODEL = False
27+
EVAL_BATCH_SIZE = 64
28+
DATASET_CONCAT_SIZE = 2048
29+
USE_FLASH_ATTN = False
30+
31+
METHOD = METHOD.QQQ
32+
FORMAT = FORMAT.QQQ
33+
LOAD_BACKEND = BACKEND.QQQ
34+
QUANT_BACKEND = BACKEND.AUTO
35+
BITS = 4
36+
GROUP_SIZE = 128
37+
DESC_ACT = False
38+
SYM = True
39+
ACT_GROUP_AWARE = False
40+
41+
EVAL_TASKS_FAST = {
42+
"gsm8k_platinum_cot": {
43+
"chat_template": True,
44+
"evalution_use_model_path": True,
45+
"evalution_batch_size": "auto",
46+
"evalution_model_args": {
47+
"dtype": "bfloat16",
48+
"attn_implementation": "eager",
49+
"device": "cuda:0",
50+
},
51+
"evalution_suite_kwargs": {
52+
"batch_size": 32,
53+
"max_new_tokens": 256,
54+
"stream": True,
55+
},
56+
"acc,num": {
57+
"value": 0.4574028122415219,
58+
"floor_pct": 0.04,
59+
"ceil_pct": 1.0,
60+
},
61+
},
62+
"arc_challenge": {
63+
"chat_template": True,
64+
"acc": {
65+
"value": 0.3225255972696246,
66+
"floor_pct": 0.04,
67+
"ceil_pct": 1.0,
68+
},
69+
"acc_norm": {
70+
"value": 0.35409556313993173,
71+
"floor_pct": 0.04,
72+
"ceil_pct": 1.0,
73+
},
74+
},
75+
}
76+
77+
EVAL_TASKS_SLOW = {
78+
"gsm8k_platinum_cot": {
79+
"chat_template": True,
80+
"acc,num": {
81+
"value": 0.4574028122415219,
82+
"floor_pct": 0.10,
83+
"ceil_pct": 0.10,
84+
},
85+
},
86+
"arc_challenge": {
87+
"chat_template": True,
88+
"acc": {
89+
"value": 0.3225255972696246,
90+
"floor_pct": 0.10,
91+
"ceil_pct": 0.10,
92+
},
93+
"acc_norm": {
94+
"value": 0.35409556313993173,
95+
"floor_pct": 0.10,
96+
"ceil_pct": 0.10,
97+
},
98+
},
99+
}
100+
101+
def test_llama3_2_qqq(self):
102+
self.quantize_and_evaluate()

0 commit comments

Comments
 (0)