Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f46445f
Update
vkuzo Apr 20, 2026
3c92c1a
Update
vkuzo Apr 20, 2026
b513b61
Update
vkuzo Apr 21, 2026
a669b9e
Update
vkuzo Apr 21, 2026
53bd8d0
Update
vkuzo Apr 21, 2026
4c86363
Update
vkuzo Apr 21, 2026
3cc91ed
Update
vkuzo Apr 21, 2026
9b7dc74
Update
vkuzo Apr 21, 2026
d69b32a
Update
vkuzo Apr 21, 2026
294c9cc
Update
vkuzo Apr 21, 2026
65fae62
Update
vkuzo Apr 21, 2026
5ee2ad2
Update
vkuzo Apr 22, 2026
2adda75
Update
vkuzo Apr 22, 2026
6463808
Update
vkuzo Apr 22, 2026
d121bff
Update
vkuzo Apr 22, 2026
80421c8
Update
vkuzo Apr 22, 2026
d302888
Update
vkuzo Apr 22, 2026
9631b76
Update
vkuzo Apr 22, 2026
5fe6574
Update
vkuzo Apr 22, 2026
5292f2f
Update
vkuzo Apr 22, 2026
f679216
Update
vkuzo Apr 22, 2026
68dc794
Update
vkuzo Apr 23, 2026
3ffc619
Update
vkuzo Apr 23, 2026
2f0a3cf
Update
vkuzo Apr 23, 2026
fad1467
Update
vkuzo Apr 23, 2026
f668c26
Update
vkuzo Apr 23, 2026
522de32
Update
vkuzo Apr 23, 2026
f635432
Update
vkuzo Apr 23, 2026
31bcb11
Update
vkuzo Apr 23, 2026
75542fa
Update
vkuzo Apr 23, 2026
be9dc1b
Update
vkuzo Apr 23, 2026
f14cde0
Update
vkuzo Apr 23, 2026
83283cf
Update
vkuzo Apr 23, 2026
ed9e39f
Update
vkuzo Apr 23, 2026
2386670
Update
vkuzo Apr 23, 2026
c1da849
Update
vkuzo Apr 23, 2026
cdcd2b3
Update
vkuzo Apr 23, 2026
196d439
Update
vkuzo Apr 23, 2026
b0697ac
Update
vkuzo Apr 23, 2026
620250d
Update
vkuzo Apr 23, 2026
19bc5c8
Update
vkuzo Apr 23, 2026
61493d9
Update
vkuzo Apr 23, 2026
5a0db16
Update
vkuzo Apr 23, 2026
7794548
Update
vkuzo Apr 23, 2026
8f1f410
Update
vkuzo Apr 23, 2026
b664d8a
Update
vkuzo Apr 23, 2026
dc35c65
Update
vkuzo Apr 23, 2026
f23cf38
Update
vkuzo Apr 23, 2026
6650b1d
Update
vkuzo Apr 23, 2026
4d9b68f
Update
vkuzo Apr 23, 2026
8a21110
Update
vkuzo Apr 23, 2026
4a456ec
Update
vkuzo Apr 23, 2026
f8d1861
Update
vkuzo Apr 23, 2026
932677b
Update
vkuzo Apr 23, 2026
0c74af8
Update
vkuzo Apr 24, 2026
dd7c1ee
Update
vkuzo Apr 24, 2026
9efbf9f
Update
vkuzo Apr 24, 2026
5daac37
Update
vkuzo Apr 24, 2026
b3fba2e
Update
vkuzo Apr 24, 2026
0306ca4
Update
vkuzo Apr 27, 2026
98b4997
Update
vkuzo Apr 27, 2026
c16a8c2
Update
vkuzo Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions test/prototype/gptq/test_gptqv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,86 @@ def test_bmm_operation_with_observer(self):
observer_3d.total_batches[e : e + 1], observers_2d[e].total_batches
), f"Expert {e} total_batches mismatch"

@pytest.mark.skipif(not torch.cuda.is_available(), reason="Need CUDA available")
@pytest.mark.skipif(
not is_sm_at_least_100(),
reason="CUDA capability >= 10.0 required for _grouped_mm",
)
def test_grouped_mm_operation_with_observer(self):
"""Test torch._grouped_mm with GPTQObserverTensor updates per-expert Hessians correctly."""
num_experts = 4
n = 16
k = 12

weight = torch.randn(num_experts, n, k, dtype=torch.float32, device="cuda")

# 4 different per-expert token distributions. Several of these have
# experts that see 0 tokens, which exercises the empty-slice skip path.
m_per_group_list = [
[1, 3, 4, 16], # all experts active
[0, 3, 4, 13], # expert 0 sees 0 tokens
[5, 5, 0, 5], # expert 2 sees 0 tokens
[2, 0, 6, 4], # expert 1 sees 0 tokens
Comment thread
vkuzo marked this conversation as resolved.
[2, 3, 5, 0], # expert 3 sees 0 tokens
]

offs_list = [
torch.tensor(
[sum(m_per_group[: i + 1]) for i in range(num_experts)],
device="cuda",
dtype=torch.int32,
)
for m_per_group in m_per_group_list
]

inputs = [
torch.randn(sum(m_per_group), k, dtype=torch.float32, device="cuda")
for m_per_group in m_per_group_list
]

# 3D path: single observer with _grouped_mm
observer_3d = GPTQObserverTensor.from_hp(weight)
for x, offs in zip(inputs, offs_list):
torch._grouped_mm(x, observer_3d.transpose(-2, -1), offs=offs)

# 2D path: per-expert observers with F.linear
observers_2d = [
GPTQObserverTensor.from_hp(weight[e]) for e in range(num_experts)
]
for x, offs in zip(inputs, offs_list):
prev_end = 0
for e in range(num_experts):
end = offs[e].item()
if end > prev_end:
F.linear(x[prev_end:end], observers_2d[e])
prev_end = end

# Verify per-expert hessians match bitwise to calculating each expert's
# hessian individually
for e in range(num_experts):
assert torch.equal(observer_3d.hessian[e], observers_2d[e].hessian), (
f"Expert {e} hessian mismatch"
)
assert torch.equal(
observer_3d.total_batches[e : e + 1], observers_2d[e].total_batches
), f"Expert {e} total_batches mismatch"

# Verify total_batches matches an independent count derived directly
# from the offsets: each non-empty forward pass contributes 1 per
# active expert (each expert's 2D slice has len(shape) == 2, so n=1).
expected_total_batches = torch.tensor(
[
sum(1 for m_per_group in m_per_group_list if m_per_group[e] > 0)
for e in range(num_experts)
],
dtype=torch.int64,
device="cuda",
)
assert torch.equal(observer_3d.total_batches, expected_total_batches), (
f"total_batches {observer_3d.total_batches.tolist()} "
f"does not match expected {expected_total_batches.tolist()}"
)

@pytest.mark.skipif(not torch.cuda.is_available(), reason="Need CUDA available")
@pytest.mark.parametrize(
"base_config",
Expand Down
30 changes: 30 additions & 0 deletions torchao/prototype/gptq/observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ def __init__(self, hp_data: torch.Tensor, total_batches, hessian=None):
if isinstance(total_batches, torch.Tensor):
self.total_batches = total_batches
elif len(self.hp_data.shape) == 3:
# TODO(future PR): audit whether we need to change this
# from `total_batches` (current) to something like `total_tokens`,
# to ensure that each token is weighted equally in the 3d case.
self.total_batches = torch.zeros(
self.hp_data.shape[0], dtype=torch.int64, device=self.hp_data.device
)
Expand Down Expand Up @@ -98,6 +101,23 @@ def update_3d(self, input: torch.Tensor):
total_batches = self.total_batches[e_idx : e_idx + 1]
self._update_single_hessian(x_cur, h_cur, total_batches)

def update_3d_with_offs(self, input: torch.Tensor, offs: torch.Tensor):
x = input.float().to(self.hp_data.device)
# offs is cumulative end indices; expert e gets rows [prev_end : offs[e]]
# Pull offs to CPU once to avoid a GPU->CPU sync per expert.
# TODO(future PR): optimize if this is too slow
offs_cpu = offs.tolist()
prev_end = 0
for e_idx in range(self.hessian.shape[0]):
end = offs_cpu[e_idx]
if end == prev_end:
continue
x_cur = x[prev_end:end]
h_cur = self.hessian[e_idx]
total_batches = self.total_batches[e_idx : e_idx + 1]
self._update_single_hessian(x_cur, h_cur, total_batches)
prev_end = end

@classmethod
def from_hp(cls, hp_tensor):
return GPTQObserverTensor(hp_tensor, 0, None)
Expand Down Expand Up @@ -145,3 +165,13 @@ def _(func, types, args, kwargs):
)
weight_tensor.update_3d(input_tensor.detach())
return func(input_tensor, weight_tensor.hp_data)


@implements([aten._grouped_mm.default])
def _(func, types, args, kwargs):
mat_a, mat_b = args[0], args[1]
offs = args[2] if len(args) > 2 else kwargs.get("offs", None)
assert offs is not None, "offs is required for grouped_mm"
assert isinstance(mat_b, GPTQObserverTensor)
mat_b.update_3d_with_offs(mat_a.detach(), offs)
return func(mat_a, mat_b.hp_data, offs)
Loading