Skip to content

Commit 185a2a4

Browse files
authored
fix(rotation): persist rotation config and apply online Hadamard for inference and calibration (#2980)
* fix(rotation): persist rotation config and apply online Hadamard for inference Cherry-pick/adapt from GPT-QModel-Ultra#66: - Persist in and round-trip via . - Fuse exact Hadamard into weights during rotation. - Add JIT Hadamard extension in (vendored from Dao-AILab/fast-hadamard-transform, BSD-3-Clause) plus . - Attach // to QuantLinear modules at and after . Restrict rotation to /. - Call in and . - Disable when is enabled. - Add and README/MANIFEST attribution/license. * fix(rotation): wire online Hadamard into loader, base, qlinear, config, version, and attributions * fix(rotation): apply online Hadamard during GPTQ calibration for mlp.down_proj - Extract apply_online_hadamard so HookedLinear and BaseQuantLinear share the same transform. - rotate_mlp_output now sets online_full_had/had_K/K on the nn.Linear before it becomes HookedLinear. - HookedLinear.from_linear copies rotation state and forward applies H to the activation used for GPTQ calibration. - Add unit test verifying HookedLinear cancellation and hook input transform.
1 parent 0408b43 commit 185a2a4

22 files changed

Lines changed: 1695 additions & 8 deletions

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ recursive-include gptqmodel_ext/machete *.h *.hpp *.cuh *.cu *.cpp *.py
66
recursive-include gptqmodel_ext/cutlass_extensions *.h *.hpp *.cuh *.cu *.cpp *.py
77
recursive-include gptqmodel_ext/paroquant *.h *.cuh *.cu *.cpp
88
recursive-include gptqmodel_ext/qqq *.h *.cuh *.cu *.cpp
9+
recursive-include gptqmodel_ext/hadamard *.h *.cuh *.cu *.cpp LICENSE
910
recursive-include gptqmodel/exllamav3/util/hadamard_data *.txt
1011
include licenses/*
1112
include gptqmodel_ext/pack_block_cpu.cpp

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,7 @@ Models quantized by GPT-QModel are inference compatible with HF Transformers (mi
666666
* EoRA: Nvidia, main-author: Shih-Yang Liu, arXiv preprint arXiv:2410.21271.
667667
* GAR: Intel, main-author: T Gafni, A Karnieli, Y Hanani, [Paper](https://openaccess.thecvf.com/content/CVPR2025W/eLVM/html/Gafni_Dual_Precision_Quantization_for_Efficient_and_Accurate_Deep_Neural_Networks_CVPRW_2025_paper.html)
668668
* GPTAQ: Yale Intelligent Computing Lab, main-author: Yuhang Li, arXiv:2504.02692.
669+
* Fast Hadamard Transform: [Dao-AILab/fast-hadamard-transform](https://github.com/Dao-AILab/fast-hadamard-transform), by Tri Dao, vendored into `gptqmodel_ext/hadamard/` under the BSD-3-Clause license. A copy of the license is included in `gptqmodel_ext/hadamard/LICENSE`.
669670
* QQQ: Meituan, main-author Ying Zhang, arXiv:2406.09904
670671
* FOEM: Zheng, Xingyu and Qin, Haotong and Li, Yuye and Chu, Haoran and Wang, Jiakai and Guo, Jinyang and Magno, Michele and Liu, Xianglong [Paper](https://ojs.aaai.org/index.php/AAAI/article/view/40123)
671672

gptqmodel/extension.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ def _resolve_extension_factory(module_name: str, attr_name: str) -> TorchOpsJitE
100100
aliases=("paroquant_rotation",),
101101
resolve=lambda: _resolve_extension_attr("gptqmodel.utils.paroquant", "_PAROQUANT_ROTATION_EXTENSION"),
102102
),
103+
_ExtensionSpec(
104+
name="hadamard",
105+
aliases=("fast_hadamard_transform",),
106+
resolve=lambda: _resolve_extension_attr("gptqmodel.utils.hadamard", "_HADAMARD_TORCH_OPS_EXTENSION"),
107+
supported=lambda: _resolve_attr("gptqmodel.utils.hadamard", "hadamard_supported")(),
108+
unsupported_error=lambda: _resolve_attr("gptqmodel.utils.hadamard", "hadamard_runtime_error")(),
109+
),
103110
)
104111

105112
_EXTENSION_SPECS_BY_NAME = {spec.name: spec for spec in _EXTENSION_SPECS}

gptqmodel/models/base.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
EXPERT_INDEX_PLACEHOLDER,
9595
META,
9696
)
97-
from .loader import ModelLoader
97+
from .loader import ModelLoader, _setup_rotation_online_had
9898
from .writer import ModelWriter
9999

100100

@@ -1001,6 +1001,15 @@ def quantize(
10011001
self.model, _ = rotate_model(model=self.model, rotate_mode=self.quantize_config.rotation,
10021002
device=rotation_device, **module_name_args)
10031003

1004+
if self.quantize_config.rotation:
1005+
backend = normalize_backend(backend, quant_method=self.quantize_config.method)
1006+
if backend == BACKEND.AUTO:
1007+
backend = BACKEND.GPTQ_TORCH
1008+
if backend not in (BACKEND.GPTQ_TORCH, BACKEND.GPTQ_TRITON):
1009+
raise NotImplementedError(
1010+
f"`rotation` is only supported with `gptq_torch` or `gptq_triton` backend, got `{backend}`."
1011+
)
1012+
10041013
if self.quantize_config.uses_weight_only_lifecycle():
10051014
result = self._quantize_weight_only(
10061015
calibration=calibration,
@@ -1029,6 +1038,7 @@ def quantize(
10291038
if timer is not None:
10301039
timer.flush()
10311040

1041+
_setup_rotation_online_had(self.model, self.quantize_config.rotation)
10321042
return result
10331043

10341044
def _quantize_with_calibration(

gptqmodel/models/loader.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,46 @@ def _set_paged_attention_safe_cuda_graphs(model) -> None:
216216
cb_config.use_cuda_graph = (False, False)
217217

218218

219+
def _setup_rotation_online_had(model, rotation: Optional[str]) -> None:
220+
"""Attach online Hadamard transform state to QuantLinear modules for rotation inference.
221+
222+
Rotation (QuaRot/SpinQuant/Hadamard) fuses an orthogonal ``Q`` and an exact
223+
Hadamard ``H`` into the weights. The ``H`` must be re-applied to activations
224+
at inference before the quantized matmul for ``mlp.down_proj``. The per-head
225+
Hadamard fused into ``v_proj``/``o_proj`` cancels inside the attention path,
226+
so ``self_attn.o_proj`` does not require an online transform. Only the
227+
``rotation`` string is persisted; ``had_K``/``K`` are recomputed from the
228+
model dimensions at load time.
229+
"""
230+
if not rotation:
231+
return
232+
233+
from ..quantization.rotation.hadamard_utils import get_hadK
234+
235+
if rotation not in {"hadamard", "random"}:
236+
raise ValueError(f"Unsupported rotation mode: `{rotation}`")
237+
238+
online_count = 0
239+
for name, module in model.named_modules():
240+
if not isinstance(module, BaseQuantLinear):
241+
continue
242+
243+
if name.endswith("mlp.down_proj"):
244+
intermediate_size = module.in_features
245+
had_K, K = get_hadK(intermediate_size)
246+
module.online_full_had = True
247+
module.K = K
248+
if had_K is not None:
249+
module.register_buffer("had_K", had_K, persistent=False)
250+
online_count += 1
251+
252+
if online_count == 0:
253+
log.warn(
254+
f"Rotation `{rotation}` requested but no `mlp.down_proj` QuantLinear "
255+
"modules were found; online Hadamard transform was not applied."
256+
)
257+
258+
219259
def _is_accelerated_attention_device(device: object) -> bool:
220260
"""Return True when the selected device can run CUDA/ROCm flash attention."""
221261

@@ -1302,6 +1342,19 @@ def from_quantized(
13021342
log.info(f"The layer {name} is not quantized.")
13031343
del modules[name]
13041344

1345+
if qcfg.rotation:
1346+
if format_code not in (FORMAT.GPTQ, FORMAT.GPTQ_V2):
1347+
raise NotImplementedError(
1348+
f"`rotation` is only supported for GPTQ/GPTQ_V2 checkpoints, got `{format_code}`."
1349+
)
1350+
backend = normalize_backend(backend, quant_method=qcfg.method)
1351+
if backend == BACKEND.AUTO:
1352+
backend = BACKEND.GPTQ_TORCH
1353+
if backend not in (BACKEND.GPTQ_TORCH, BACKEND.GPTQ_TRITON):
1354+
raise NotImplementedError(
1355+
f"`rotation` is only supported with `gptq_torch` or `gptq_triton` backend, got `{backend}`."
1356+
)
1357+
13051358
if format_code == FORMAT.EXL3:
13061359
if not isinstance(qcfg.tensor_storage, dict) or not qcfg.tensor_storage:
13071360
raise ValueError("EXL3 checkpoints require `quantization_config.tensor_storage` metadata.")
@@ -1728,6 +1781,7 @@ def assign(mod, device_id):
17281781
trust_remote_code=trust_remote_code,
17291782
model_local_path=model_local_path,
17301783
)
1784+
_setup_rotation_online_had(instance.model, qcfg.rotation)
17311785
_set_paged_attention_safe_cuda_graphs(instance.model)
17321786
return instance
17331787

gptqmodel/nn_modules/hooked_linear.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from accelerate.utils import has_offloaded_params
1111
from torch import nn
1212

13+
from ..quantization.rotation.hadamard_utils import apply_online_hadamard
1314
from ..utils.device_telemetry import emit_device_telemetry
1415
from ..utils.logger import setup_logger
1516

@@ -248,6 +249,11 @@ def from_linear(linear: torch.nn.Linear):
248249
custom_linear = HookedLinear(linear.in_features, linear.out_features)
249250
custom_linear.weight = linear.weight
250251
custom_linear.bias = linear.bias
252+
custom_linear.online_full_had = getattr(linear, "online_full_had", False)
253+
custom_linear.online_partial_had = getattr(linear, "online_partial_had", False)
254+
custom_linear.had_dim = getattr(linear, "had_dim", -1)
255+
custom_linear.had_K = getattr(linear, "had_K", None)
256+
custom_linear.K = getattr(linear, "K", 1)
251257
return custom_linear
252258

253259
@torch.inference_mode()
@@ -264,6 +270,14 @@ def forward(self, input: torch.Tensor) -> torch.Tensor:
264270
)
265271
if original_device != target_device:
266272
input = input.to(device=target_device)
273+
input = apply_online_hadamard(
274+
input,
275+
online_full_had=getattr(self, "online_full_had", False),
276+
online_partial_had=getattr(self, "online_partial_had", False),
277+
had_K=getattr(self, "had_K", None),
278+
K=getattr(self, "K", 1),
279+
had_dim=getattr(self, "had_dim", -1),
280+
)
267281
output = super().forward(input)
268282
if self.forward_hook:
269283
self.forward_hook(self, (input,), output)

gptqmodel/nn_modules/qlinear/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ def __init__(self,
121121
self._autotune_complete = False
122122
self._autotune_result: Any = None
123123

124+
# Rotation/online-Hadamard state for SpinQuant/QuaRot inference.
125+
self.online_full_had = False
126+
self.online_partial_had = False
127+
self.had_dim = -1
128+
self.K = 1
129+
self.register_buffer("had_K", None, persistent=False)
130+
124131
validate_args = {
125132
"bits": bits,
126133
"in_features": in_features,
@@ -463,6 +470,19 @@ def train(self, mode=True):
463470
self.clear_autotune()
464471
return super().train(mode)
465472

473+
def _apply_rotation_to_input(self, x: t.Tensor) -> t.Tensor:
474+
"""Apply online Hadamard transform to the input for SpinQuant/QuaRot inference."""
475+
from ...quantization.rotation.hadamard_utils import apply_online_hadamard
476+
477+
return apply_online_hadamard(
478+
x,
479+
online_full_had=self.online_full_had,
480+
online_partial_had=self.online_partial_had,
481+
had_K=self.had_K,
482+
K=self.K,
483+
had_dim=self.had_dim,
484+
)
485+
466486

467487
class GroupedQuantLinear(BaseQuantLinear):
468488
SUPPORTS_GROUP_SIZE: List[int] = None

gptqmodel/nn_modules/qlinear/torch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ def forward(self, x: torch.Tensor):
306306
return out
307307

308308
def _forward(self, x, out_shape):
309+
x = self._apply_rotation_to_input(x)
309310
cached = self._maybe_get_cached_weights(x)
310311
if cached is not None:
311312
out = torch.matmul(x, cached).reshape(out_shape)

gptqmodel/nn_modules/qlinear/tritonv2.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ def forward(self, x):
179179
if self.training:
180180
return super().forward(x)
181181

182+
x = self._apply_rotation_to_input(x)
183+
182184
# if in_features is padded, we need to pad the input as well
183185
# if x.size(-1) != self.padded_infeatures:
184186
# x = F.pad(x, (0, self.padded_infeatures - self.in_features))

gptqmodel/quantization/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2556,6 +2556,13 @@ def __post_init__(self):
25562556
self.meta = {}
25572557

25582558
self.adapter = normalize_adapter(self.adapter)
2559+
2560+
# Rotation fuses orthogonal transforms into the weights and requires
2561+
# materialized tensors; meta-device/shell loading cannot be used.
2562+
if self.rotation and self.offload_to_disk:
2563+
log.warn(f"{self.__class__.__name__}: `rotation` is incompatible with `offload_to_disk`; disabling disk offload.")
2564+
self.offload_to_disk = False
2565+
25592566
self._ensure_offload_temp_dir()
25602567

25612568
self.dense_vram_strategy = _normalize_dense_vram_strategy(self.dense_vram_strategy)
@@ -2940,6 +2947,7 @@ def to_dict(self):
29402947
FORMAT_FIELD_CODE: self.format,
29412948
FORMAT_FIELD_CHECKPOINT: self.format,
29422949
PACK_DTYPE_FIELD: str(self.pack_dtype).split(".")[-1],
2950+
"rotation": self.rotation,
29432951
META_FIELD: meta_payload,
29442952
}
29452953
self._update_output_payload(out)
@@ -3732,6 +3740,13 @@ def __post_init__(self):
37323740
self.meta = {}
37333741

37343742
self.adapter = normalize_adapter(self.adapter)
3743+
3744+
# Rotation fuses orthogonal transforms into the weights and requires
3745+
# materialized tensors; meta-device/shell loading cannot be used.
3746+
if self.rotation and self.offload_to_disk:
3747+
log.warn(f"{self.__class__.__name__}: `rotation` is incompatible with `offload_to_disk`; disabling disk offload.")
3748+
self.offload_to_disk = False
3749+
37353750
self._ensure_offload_temp_dir()
37363751

37373752
self.dense_vram_strategy = _normalize_dense_vram_strategy(self.dense_vram_strategy)

0 commit comments

Comments
 (0)