Skip to content

Commit 1ab1661

Browse files
justinchubyCopilot
andauthored
Emit MatMulNBits accuracy_level=4 (int8 path) — 2.3x CPU decode (#402)
**Root cause of onnx-genai being ~3.6x slower than llama.cpp on CPU: MatMulNBits was emitted with no `accuracy_level`**, so ORT's MLAS kernel ran the fp32 dequant+GEMM path instead of the int8 dynamic-quant + int8 dot-product path (ARM SDOT / x86 AVX-VNNI) — the same class of kernel llama.cpp uses. ORT's parity claim holds; we just weren't opting in. `default_int4_accuracy_level=4` already existed on the cpu/webgpu `EpCapabilities` but was **dead config**. This plumbs it via a new `_accuracy_level_attrs()` helper into both `MatMulNBits` emission sites in `components/_quantized_linear.py` (Q/K/V/O, MLP, tied + non-tied head). Emits when `>0`, omits at `0` (portable default preserved). ### Measured (Qwen2.5-0.5B Q4, CPU EP, decode tok/s, all coherent) | config | tok/s | |---|---:| | baseline (accuracy_level missing) | 39.3 | | **accuracy_level=4 (int8)** | **91.8** (2.33x) | | quantized head + acc4 | **194.7** | | ref: LM Studio CPU | 157 | fp16/bf16 levels regress on M1 (no native GEMM). End-to-end verified: `--ep cpu` stamps all 168 nodes with accuracy_level=4; `--ep default` omits it. lintrunner clean; pytest 518 passed (+2 tests). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 57177b7 commit 1ab1661

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

src/mobius/components/_quantized_linear.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import onnx_ir as ir
1616
from onnxscript import OpBuilder, nn
1717

18+
from mobius._build_context import ep_capabilities
19+
1820
# MatMulNBits packs weights into uint8 blobs. The packed shape depends
1921
# on bits and block_size:
2022
# packed_weights: [N, n_blocks, blob_size] (uint8)
@@ -26,6 +28,29 @@
2628
_MICROSOFT_DOMAIN = "com.microsoft"
2729

2830

31+
def _accuracy_level_attrs(bits: int) -> dict[str, int]:
32+
"""Return the ``accuracy_level`` attribute for ``MatMulNBits``, if any.
33+
34+
Only emitted for 4-bit weights: ``accuracy_level`` is sourced from
35+
``EpCapabilities.default_int4_accuracy_level`` and its int8-accumulation
36+
semantics are defined for INT4 ``MatMulNBits``. For 2-bit / 8-bit weights the
37+
attribute is omitted so those paths keep ORT's default behavior.
38+
39+
ORT's MLAS CPU ``MatMulNBits`` kernel selects its compute path from the
40+
``accuracy_level`` attribute: unset/0 keeps the highest-precision fp32
41+
dequant + fp32 GEMM path, while ``4`` dynamically quantizes activations to
42+
int8 and uses int8 dot-products (SDOT/NEON on ARM, AVX-VNNI on x86) — the
43+
same class of kernel llama.cpp uses, and typically 2-4x faster on CPU with
44+
no observable quality loss for Q4 weights. The value is sourced from the
45+
active EP's :attr:`EpCapabilities.default_int4_accuracy_level` (4 for CPU /
46+
WebGPU). When it is 0 the attribute is omitted so ORT keeps its default.
47+
"""
48+
if bits != 4:
49+
return {}
50+
level = ep_capabilities().default_int4_accuracy_level
51+
return {"accuracy_level": level} if level else {}
52+
53+
2954
class QuantizedLinear(nn.Module):
3055
"""Linear layer backed by the MatMulNBits custom op.
3156
@@ -126,6 +151,7 @@ def forward(self, op: OpBuilder, x: ir.Value) -> ir.Value:
126151
N=self._n,
127152
bits=self._bits,
128153
block_size=self._block_size,
154+
**_accuracy_level_attrs(self._bits),
129155
_domain=_MICROSOFT_DOMAIN,
130156
)
131157
if self.bias is not None:
@@ -309,6 +335,7 @@ def forward(self, op: OpBuilder, x: ir.Value) -> ir.Value:
309335
N=self._n,
310336
bits=self._bits,
311337
block_size=self._block_size,
338+
**_accuracy_level_attrs(self._bits),
312339
_domain=_MICROSOFT_DOMAIN,
313340
)
314341

src/mobius/components/_quantized_linear_test.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import pytest
1111

12+
from mobius._build_context import build_context
13+
from mobius._execution_providers import ep_registry
1214
from mobius._testing import (
1315
count_op_type,
1416
create_test_builder,
@@ -125,7 +127,60 @@ def test_matmulnbits_attributes(self):
125127
assert attrs["N"] == OUT_FEATURES
126128
assert attrs["bits"] == 4
127129
assert attrs["block_size"] == 32
130+
# No active build context -> default EP (accuracy_level 0),
131+
# so the attribute is omitted and ORT keeps its default path.
132+
assert "accuracy_level" not in attrs
128133
break
134+
else:
135+
pytest.fail("MatMulNBits node not found")
136+
137+
def test_no_accuracy_level_without_context(self):
138+
"""Default EP omits accuracy_level (ORT default / highest precision)."""
139+
ql = QuantizedLinear(IN_FEATURES, OUT_FEATURES, bits=4, block_size=32)
140+
b, op, graph = create_test_builder()
141+
x = create_test_input(b, "x", [1, 4, IN_FEATURES])
142+
result = ql(op, x)
143+
b._adapt_outputs([result], "")
144+
for node in graph:
145+
if node.op_type == "MatMulNBits":
146+
attrs = {a.name: a.value for a in node.attributes.values()}
147+
assert "accuracy_level" not in attrs
148+
break
149+
else:
150+
pytest.fail("MatMulNBits node not found")
151+
152+
def test_cpu_ep_emits_accuracy_level_4(self):
153+
"""CPU EP context stamps accuracy_level=4 (int8 MLAS path)."""
154+
with build_context(ep_registry.require("cpu")):
155+
ql = QuantizedLinear(IN_FEATURES, OUT_FEATURES, bits=4, block_size=32)
156+
b, op, graph = create_test_builder()
157+
x = create_test_input(b, "x", [1, 4, IN_FEATURES])
158+
result = ql(op, x)
159+
b._adapt_outputs([result], "")
160+
for node in graph:
161+
if node.op_type == "MatMulNBits":
162+
attrs = {a.name: a.value for a in node.attributes.values()}
163+
assert attrs["accuracy_level"] == 4
164+
break
165+
else:
166+
pytest.fail("MatMulNBits node not found")
167+
168+
def test_cpu_ep_omits_accuracy_level_for_non_int4(self):
169+
"""accuracy_level is INT4-specific: 8-bit weights keep ORT's default."""
170+
with build_context(ep_registry.require("cpu")):
171+
ql = QuantizedLinear(IN_FEATURES, OUT_FEATURES, bits=8, block_size=32)
172+
b, op, graph = create_test_builder()
173+
x = create_test_input(b, "x", [1, 4, IN_FEATURES])
174+
result = ql(op, x)
175+
b._adapt_outputs([result], "")
176+
for node in graph:
177+
if node.op_type == "MatMulNBits":
178+
attrs = {a.name: a.value for a in node.attributes.values()}
179+
assert attrs["bits"] == 8
180+
assert "accuracy_level" not in attrs
181+
break
182+
else:
183+
pytest.fail("MatMulNBits node not found")
129184

130185
def test_3_inputs_without_zero_points(self):
131186
ql = QuantizedLinear(IN_FEATURES, OUT_FEATURES)

0 commit comments

Comments
 (0)