Skip to content

Commit 260d6b0

Browse files
authored
Merge branch 'main' into feat/onnx-genai-metadata-export
2 parents 541b8a7 + 1ab1661 commit 260d6b0

14 files changed

Lines changed: 1002 additions & 220 deletions

src/mobius/__main__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,10 @@ def _cmd_build_gguf(args: argparse.Namespace) -> None:
363363
raise SystemExit(1)
364364

365365
if args.keep_quantized:
366-
print("Quantized mode: preserving GGUF quantization as MatMulNBits...")
366+
print(
367+
"Quantized mode: preserving GGUF quantization as "
368+
"MatMulNBits/GatherBlockQuantized..."
369+
)
367370

368371
gguf_path = args.gguf_path
369372
output_dir = args.output or os.path.splitext(gguf_path)[0] + "_onnx"
@@ -617,7 +620,10 @@ def main(argv: list[str] | None = None) -> None:
617620
gguf_parser.add_argument(
618621
"--keep-quantized",
619622
action="store_true",
620-
help="Preserve quantization via MatMulNBits (Q4_0/Q4_1/Q8_0).",
623+
help=(
624+
"Preserve supported projection, output-head, and embedding "
625+
"quantization via MatMulNBits/GatherBlockQuantized."
626+
),
621627
)
622628
gguf_parser.add_argument(
623629
"--dtype",

src/mobius/_configs/_quantization.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ class QuantizationConfig:
2727
# (e.g. Tencent SEQ uses 1.5).
2828
float_zero_point: bool = False
2929
# When True, the input embedding table is block-wise quantized and is
30-
# looked up with GatherBlockQuantized instead of a plain Gather. Set by
31-
# Olive RTN exports that pass ``embeds: true``.
30+
# looked up with GatherBlockQuantized instead of a plain Gather. Used by
31+
# Olive RTN exports and quantized GGUF imports.
3232
quantize_embeddings: bool = False
3333
# When True, the LM head projection is block-wise quantized (MatMulNBits).
34-
# Set by Olive RTN exports that pass ``lm_head: true``.
34+
# Used by Olive RTN exports and quantized GGUF imports.
3535
quantize_lm_head: bool = False
3636
# When True, the input embedding and LM head share one weight table. Olive
3737
# RTN records this in its own config (``tie_word_embeddings``) and may clear

src/mobius/components/_quantized_linear.py

Lines changed: 33 additions & 1 deletion
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:
@@ -186,6 +212,7 @@ def __init__(
186212

187213
self._bits = bits
188214
self._block_size = block_size
215+
self._embedding_dim = embedding_dim
189216
self.padding_idx = padding_idx
190217

191218
n_blocks = embedding_dim // block_size
@@ -219,14 +246,18 @@ def forward(self, op: OpBuilder, input_ids: ir.Value) -> ir.Value:
219246
if self.zero_points is not None:
220247
inputs.append(self.zero_points)
221248

222-
return op.GatherBlockQuantized(
249+
result = op.GatherBlockQuantized(
223250
*inputs,
224251
bits=self._bits,
225252
block_size=self._block_size,
226253
gather_axis=0,
227254
quantize_axis=1,
228255
_domain=_MICROSOFT_DOMAIN,
229256
)
257+
result.dtype = self.scales.dtype
258+
if input_ids.shape is not None:
259+
result.shape = ir.Shape([*input_ids.shape, self._embedding_dim])
260+
return result
230261

231262

232263
class TiedQuantizedLMHead(nn.Module):
@@ -304,6 +335,7 @@ def forward(self, op: OpBuilder, x: ir.Value) -> ir.Value:
304335
N=self._n,
305336
bits=self._bits,
306337
block_size=self._block_size,
338+
**_accuracy_level_attrs(self._bits),
307339
_domain=_MICROSOFT_DOMAIN,
308340
)
309341

src/mobius/components/_quantized_linear_test.py

Lines changed: 57 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)
@@ -299,6 +354,8 @@ def test_graph_has_gather_block_quantized_node(self):
299354
result = qe(op, ids)
300355
b._adapt_outputs([result], "")
301356
assert count_op_type(graph, "GatherBlockQuantized") == 1
357+
assert result.dtype == ir.DataType.FLOAT
358+
assert result.shape == ir.Shape([1, 4, self.DIM])
302359

303360
def test_node_domain_and_attributes(self):
304361
import onnx_ir as ir

0 commit comments

Comments
 (0)