Skip to content

Commit 9435118

Browse files
committed
[llama4] Add weight-only int4 linear quantization for backbone export
Pull Request resolved: #20644 Adds a `--weight-only-linear` export option that quantizes the TISO backbone's linears to weight-only int4 (dequantized int4 weights, fp activations) instead of the default 8da4w (dynamic int8 activations + int4 weights). Adds a `WeightOnlyInt4Linear` module and `_replace_linear_with_linear_int4_weight_only_for_pre_quantization` in `examples/models/llama/source_transformation/pre_quantization.py`. It uses the same buffer layout as `Int8DynActInt4WeightLinear` (int8 `weight` holding int4 values, per-group `scales` / `zeros`), so an existing QAT checkpoint loads unchanged; its forward dequantizes the weight and runs a plain `F.linear` with no per-token activation quantization. `transform_linear_for_pre_quantization` gains a `weight_only` flag, threaded through the llama4 backbone build (`backbone/model.py`) and exposed as `--weight-only-linear` on `export_llm_backbone.py`. Motivation: the 8da4w dynamic-activation-quant path triggers a Mali-GPU-specific numerical bug in the ET-VK `linear_dq8ca_q4gsw` kernel (negative per-token activation zero-point), producing garbled, runaway generation on Mali-G710 / G715. Weight-only int4 lowers to `et_vk.linear_q4gsw` (fp activation x int4 weight), bypassing the dq8ca path entirely and running correctly on Mali. This is a working Mali path / client workaround while the dq8ca kernel bug is fixed separately. Accuracy note: the TISO checkpoint was QAT-trained for 8da4w, so weight-only inference may be slightly less accurate than an accuracy-optimal weight-only-QAT model. ghstack-source-id: 403780868 @exported-using-ghexport Differential Revision: [D110111051](https://our.internmc.facebook.com/intern/diff/D110111051/)
1 parent 8ac30df commit 9435118

1 file changed

Lines changed: 143 additions & 7 deletions

File tree

examples/models/llama/source_transformation/pre_quantization.py

Lines changed: 143 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,137 @@
1111
from typing import Any, Optional
1212

1313
import torch
14+
import torch.nn.functional as F
1415
from torch import nn
1516

1617
from torchao.quantization.linear_quant_modules import (
1718
_check_linear_int4_k,
1819
Int8DynActInt4WeightLinear,
1920
)
2021
from torchao.quantization.quant_api import _replace_with_custom_fn_if_matches_filter
22+
from torchao.quantization.quant_primitives import dequantize_affine
2123

2224
from .quantize import Int8DynActInt8WeightLinear, QuantizedGroupEmbedding
2325

2426

27+
class WeightOnlyInt4Linear(torch.nn.Module):
28+
"""Weight-only int4 per-group linear that reuses the 8da4w checkpoint layout.
29+
30+
Stores the SAME buffers as ``Int8DynActInt4WeightLinear`` (int8 ``weight`` holding
31+
int4 values, per-group ``scales``/``zeros``) so a pre-quantized QAT/PTQ checkpoint
32+
loads unchanged. The difference is forward: the activation is left in floating
33+
point (no per-token dynamic quant), so the traced graph is
34+
``dequantize_affine(weight) -> F.linear`` with no activation quant. This lowers to
35+
the ET-VK weight-only ``et_vk.linear_q4gsw`` path instead of the dynamic-activation
36+
``et_vk.linear_dq8ca_q4gsw`` path.
37+
"""
38+
39+
__constants__ = ["in_features", "out_features"]
40+
41+
in_features: int
42+
out_features: int
43+
weight: torch.Tensor
44+
45+
def __init__(
46+
self,
47+
in_features: int,
48+
out_features: int,
49+
bias: bool = True,
50+
device: torch.device | None = None,
51+
groupsize: int = 256,
52+
precision: torch.dtype = torch.float32,
53+
scales_precision: torch.dtype = torch.float32,
54+
) -> None:
55+
super().__init__()
56+
assert (
57+
in_features % groupsize == 0
58+
), f"require in_features:{in_features} % groupsize:{groupsize} == 0"
59+
self.in_features = in_features
60+
self.out_features = out_features
61+
self.groupsize = groupsize
62+
self.precision = precision
63+
self.register_buffer(
64+
"weight",
65+
torch.zeros((out_features, in_features), dtype=torch.int8, device=device),
66+
)
67+
self.register_buffer(
68+
"scales",
69+
torch.zeros(
70+
(out_features, in_features // groupsize),
71+
dtype=scales_precision,
72+
device=device,
73+
),
74+
)
75+
self.register_buffer(
76+
"zeros",
77+
torch.zeros(
78+
(out_features, in_features // groupsize),
79+
dtype=scales_precision,
80+
device=device,
81+
),
82+
)
83+
if bias:
84+
self.register_buffer(
85+
"bias", torch.zeros(out_features, dtype=precision, device=device)
86+
)
87+
else:
88+
self.bias = None
89+
90+
def forward(self, input: torch.Tensor) -> torch.Tensor:
91+
input = input.to(self.precision)
92+
n_bit = 4
93+
quant_min = -(2 ** (n_bit - 1))
94+
quant_max = 2 ** (n_bit - 1) - 1
95+
block_size = (1, self.groupsize)
96+
w_dq = dequantize_affine(
97+
self.weight,
98+
block_size,
99+
self.scales,
100+
self.zeros,
101+
torch.int8,
102+
quant_min,
103+
quant_max,
104+
output_dtype=self.precision,
105+
)
106+
return F.linear(input, w_dq, self.bias)
107+
108+
109+
def _replace_linear_with_linear_int4_weight_only_for_pre_quantization(
110+
module: torch.nn.Module,
111+
checkpoint: Any,
112+
group_size: int,
113+
precision: torch.dtype,
114+
scales_precision: torch.dtype,
115+
):
116+
def filter_fn(child: torch.nn.Module, cur_fqn: str) -> bool:
117+
scales_key = f"{cur_fqn}.scales"
118+
if isinstance(child, nn.Linear) and scales_key in checkpoint:
119+
assert _check_linear_int4_k(child.in_features, group_size)
120+
assert checkpoint[f"{cur_fqn}.weight"].dtype == torch.int8
121+
assert checkpoint[scales_key].dtype == scales_precision
122+
return True
123+
return False
124+
125+
def replacement_fn(child: torch.nn.Module) -> torch.nn.Module:
126+
new_linear = WeightOnlyInt4Linear(
127+
# pyre-fixme[6]
128+
child.in_features,
129+
# pyre-fixme[6]
130+
child.out_features,
131+
bias=child.bias is not None,
132+
device=child.weight.device,
133+
groupsize=group_size,
134+
precision=precision,
135+
scales_precision=scales_precision,
136+
)
137+
# Symmetric int4: zero point is 0 (matches the 8da4w pre-quant path, which
138+
# zeros this buffer rather than trusting the checkpoint's zeros key).
139+
new_linear.zeros = torch.zeros_like(new_linear.zeros)
140+
return new_linear
141+
142+
_replace_with_custom_fn_if_matches_filter(module, replacement_fn, filter_fn)
143+
144+
25145
def _replace_linear_with_linear_8da4w_for_pre_quantization(
26146
module: torch.nn.Module,
27147
checkpoint: Any,
@@ -65,24 +185,40 @@ def transform_linear_for_pre_quantization(
65185
checkpoint: Any,
66186
group_size: int,
67187
dtype: torch.dtype,
188+
weight_only: bool = False,
68189
) -> torch.nn.Module:
69190
"""
70191
Transform the model to be able to load pre-quantized checkpoints that
71192
are quantized with the given group size and quantization mode for
72193
linear layers.
194+
195+
When ``weight_only`` is True, linears are swapped for ``WeightOnlyInt4Linear``
196+
(float activation x dequantized int4 weight) instead of the default
197+
``Int8DynActInt4WeightLinear`` (dynamic per-token int8 activation). The
198+
checkpoint buffers (int4 weight, per-group scales/zeros) are identical, so the
199+
same pre-quantized checkpoint loads either way.
73200
"""
74201

75202
if group_size not in [32, 64, 128, 256]:
76203
raise ValueError(
77204
f"Group size {group_size} is not supported for pre-quantized checkpoint."
78205
)
79-
_replace_linear_with_linear_8da4w_for_pre_quantization(
80-
module,
81-
checkpoint,
82-
group_size,
83-
dtype,
84-
dtype,
85-
)
206+
if weight_only:
207+
_replace_linear_with_linear_int4_weight_only_for_pre_quantization(
208+
module,
209+
checkpoint,
210+
group_size,
211+
dtype,
212+
dtype,
213+
)
214+
else:
215+
_replace_linear_with_linear_8da4w_for_pre_quantization(
216+
module,
217+
checkpoint,
218+
group_size,
219+
dtype,
220+
dtype,
221+
)
86222
return module
87223

88224

0 commit comments

Comments
 (0)