Skip to content

Commit 581bfd9

Browse files
authored
fix(qqq): accumulate Hessian in-place per device to reduce memory (#2981)
1 parent 185a2a4 commit 581bfd9

2 files changed

Lines changed: 77 additions & 11 deletions

File tree

gptqmodel/quantization/qqq.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -451,15 +451,20 @@ def add_batch(self, inp, out):
451451
pad = inp.new_zeros((self._tp_pad_cols, inp.shape[1]))
452452
inp = torch.cat((inp, pad), dim=0)
453453

454-
xtx = inp.matmul(inp.t())
455-
456454
with self.lock:
457455
self.fwd_counter += 1
458456
existing = self._device_hessian_partials.get(dev)
459457
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))
458+
existing = torch.zeros(
459+
(self.columns, self.columns),
460+
dtype=torch.float32,
461+
device=dev,
462+
)
463+
self._device_hessian_partials[dev] = existing
464+
465+
# Accumulate xtx directly into the per-device buffer to avoid
466+
# allocating a separate output tensor for every batch.
467+
existing.addmm_(inp, inp.t(), beta=1.0, alpha=1.0)
463468
self._device_sample_counts[dev] = self._device_sample_counts.get(dev, 0) + batch_token_size
464469
self.nsamples += batch_token_size
465470

@@ -474,17 +479,21 @@ def materialize_hessian(self, target_device: Optional[torch.device] = None) -> t
474479
self.nsamples = 0
475480
return self.H
476481

482+
# Merge per-device partials into a single Hessian on the target device.
483+
# Pop and delete each partial as it is added so the peak memory stays
484+
# at result + one partial instead of result + all partials.
477485
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:
486+
while self._device_hessian_partials:
487+
dev, partial = self._device_hessian_partials.popitem()
488+
if partial.device == target_device and partial.dtype == torch.float32:
482489
result.add_(partial)
490+
else:
491+
result.add_(partial.to(device=target_device, dtype=torch.float32))
492+
del partial
483493

484494
result.mul_(2.0 / float(total_samples))
485495
self.H = result
486496
self.nsamples = total_samples
487-
self._device_hessian_partials.clear()
488497
self._device_sample_counts.clear()
489498
return self.H
490499

@@ -556,7 +565,7 @@ def quantize(
556565
Q = torch.zeros_like(W)
557566

558567
damp = percdamp * torch.mean(torch.diag(H))
559-
diag = torch.arange(self.columns, device=self.dev)
568+
diag = torch.arange(self.columns, device=H.device)
560569
H[diag, diag] += damp
561570
threshold_raw, is_percent = resolve_threshold(self.fallback, self.expected_nsamples)
562571
fallback_configured = threshold_raw is not None

tests/test_qqq.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
import tempfile
1111
import unittest
1212

13+
import torch
14+
1315
from datasets import load_dataset
1416
from models.model_test import ModelTest
1517
from parameterized import parameterized
1618
from transformers import AutoTokenizer
1719

1820
from gptqmodel.nn_modules.qlinear.qqq import QQQLinear
1921
from gptqmodel.quantization import FORMAT, METHOD, QUANT_CONFIG_FILENAME
22+
from gptqmodel.quantization.qqq import QQQ
2023
from gptqmodel.utils.torch import torch_empty_cache
2124

2225

@@ -107,3 +110,57 @@ def assert_qqq_linear(self, model):
107110
has_qqq = True
108111
break
109112
self.assertTrue(has_qqq)
113+
114+
115+
class TestQQQHessian(unittest.TestCase):
116+
"""Verify QQQ Hessian accumulation matches the closed-form reference."""
117+
118+
def _reference_hessian(self, *tensors):
119+
"""Compute 2/N * (X^T X) for one or more (B, S, C) activation tensors."""
120+
target = tensors[0].device
121+
x = torch.cat([t.to(target) for t in tensors], dim=0).reshape(-1, tensors[0].shape[-1]).float()
122+
return (2.0 / x.shape[0]) * x.t().matmul(x)
123+
124+
def test_single_batch_matches_reference(self):
125+
torch.manual_seed(42)
126+
layer = torch.nn.Linear(16, 8, dtype=torch.float32)
127+
qqq = QQQ(layer)
128+
x = torch.randn(4, 8, 16, dtype=torch.float32)
129+
qqq.add_batch(x, None)
130+
H = qqq.materialize_hessian()
131+
H_ref = self._reference_hessian(x)
132+
self.assertTrue(torch.allclose(H, H_ref, rtol=1e-4, atol=1e-5))
133+
134+
def test_multiple_batches_matches_reference(self):
135+
torch.manual_seed(42)
136+
layer = torch.nn.Linear(16, 8, dtype=torch.float32)
137+
qqq = QQQ(layer)
138+
batches = [torch.randn(2, 8, 16, dtype=torch.float32) for _ in range(3)]
139+
for x in batches:
140+
qqq.add_batch(x, None)
141+
H = qqq.materialize_hessian()
142+
H_ref = self._reference_hessian(*batches)
143+
self.assertTrue(torch.allclose(H, H_ref, rtol=1e-4, atol=1e-5))
144+
145+
def test_multi_device_matches_reference(self):
146+
if not torch.cuda.is_available():
147+
self.skipTest("CUDA not available")
148+
torch.manual_seed(42)
149+
layer = torch.nn.Linear(16, 8, dtype=torch.float32, device="cpu")
150+
qqq = QQQ(layer)
151+
x_cpu = torch.randn(2, 8, 16, dtype=torch.float32, device="cpu")
152+
x_gpu = torch.randn(2, 8, 16, dtype=torch.float32, device="cuda:0")
153+
qqq.add_batch(x_cpu, None)
154+
qqq.add_batch(x_gpu, None)
155+
H = qqq.materialize_hessian()
156+
H_ref = self._reference_hessian(x_cpu, x_gpu)
157+
self.assertTrue(torch.allclose(H, H_ref, rtol=1e-4, atol=1e-5))
158+
159+
def test_empty_batch_is_noop(self):
160+
torch.manual_seed(42)
161+
layer = torch.nn.Linear(16, 8, dtype=torch.float32)
162+
qqq = QQQ(layer)
163+
qqq.add_batch(torch.zeros(0, 8, 16, dtype=torch.float32), None)
164+
H = qqq.materialize_hessian()
165+
self.assertEqual(H.shape, (16, 16))
166+
self.assertTrue(torch.allclose(H, torch.zeros_like(H)))

0 commit comments

Comments
 (0)