Skip to content

Commit 3c9c4b4

Browse files
Copilotjustinchuby
andauthored
Add QDQ build feature
Co-authored-by: justinchuby <11205048+justinchuby@users.noreply.github.com>
1 parent 3832f73 commit 3c9c4b4

7 files changed

Lines changed: 68 additions & 8 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ mobius build --model openai/whisper-tiny output_dir/
113113
```
114114

115115
Build-mode toggles use the cargo-style `--features` option. Available features
116-
are `static-cache`, `fp8-kv-cache`, `prune-lm-head`, and `text-only`. Pass them
117-
as a comma-separated list or repeat the option:
116+
are `static-cache`, `fp8-kv-cache`, `prune-lm-head`, `qdq`, and `text-only`.
117+
Pass them as a comma-separated list or repeat the option:
118118

119119
```sh
120120
mobius build --model meta-llama/Llama-3.2-1B output_dir/ \

docs/cli_reference.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ option. Pass a comma-separated list (and/or repeat the flag):
176176
```
177177
--features fp8-kv-cache,static-cache
178178
--features prune-lm-head
179+
--features qdq
179180
--features text-only
180181
```
181182

@@ -186,6 +187,7 @@ Available features:
186187
| `static-cache` | Pre-allocate fixed-size KV cache buffers using `TensorScatter` (pair with `--max-seq-len N`). Requires `DecoderLayer` / `MoEDecoderLayer` models. Cannot combine with `--task`. |
187188
| `fp8-kv-cache` | Store the `GroupQueryAttention` KV cache as `FLOAT8E4M3FN` (per-tensor E4M3), halving KV-cache memory. Requires a GQA build (e.g. `--ep cuda --dtype f16`) and an ORT runtime with the FP8 KV-cache kernel (SM89+). Pair with `--kv-cache-scale-file` for calibrated scales. |
188189
| `prune-lm-head` | Select the final hidden-state position before the LM-head projection and emit logits shaped `[B, 1, vocab]`. Supported by models using the base `CausalLMModel.forward()` path; unsupported custom forwards fail explicitly. Use only when the downstream workflow does not need per-token logits. |
190+
| `qdq` | Lower quantized `com.microsoft::MatMulNBits` weights to standard ONNX QDQ form (`DequantizeLinear` + `MatMul`) even when the selected EP supports the native contrib op. |
189191
| `text-only` | Export the text backbone of a multimodal checkpoint as a standalone decoder-only LLM (see below). |
190192

191193
The legacy boolean flags `--static-cache`, `--fp8-kv-cache`, and
@@ -200,6 +202,9 @@ mobius build --model Qwen/Qwen2.5-0.5B output/ \
200202

201203
mobius build --model meta-llama/Llama-3.2-1B output/ \
202204
--features prune-lm-head
205+
206+
mobius build --model meta-llama/Llama-3.2-1B output/ \
207+
--features qdq
203208
```
204209

205210
### Static Cache (`--features static-cache`)

src/mobius/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"fp8-kv-cache": "fp8_kv_cache",
4242
"prune-lm-head": "prune_lm_head",
4343
"text-only": "text_only",
44+
"qdq": "qdq",
4445
}
4546

4647

@@ -224,6 +225,7 @@ def _resolve_static_cache_task(model_type: str) -> ModelTask:
224225
# the --config and --model build paths can pass the same scales.
225226
fp8_kv_cache = getattr(args, "fp8_kv_cache", False)
226227
prune_lm_head = getattr(args, "prune_lm_head", False)
228+
qdq = getattr(args, "qdq", False)
227229
kv_cache_scales: dict[int, tuple[float, float]] | None = None
228230
scale_file = getattr(args, "kv_cache_scale_file", None)
229231
if scale_file is not None and not fp8_kv_cache:
@@ -314,6 +316,7 @@ def _resolve_static_cache_task(model_type: str) -> ModelTask:
314316
fp8_kv_cache=fp8_kv_cache,
315317
kv_cache_scales=kv_cache_scales,
316318
prune_lm_head=prune_lm_head,
319+
qdq=qdq,
317320
)
318321
for name, model in pkg.items():
319322
model.graph.name = f"{config_path}/{name}"
@@ -344,6 +347,7 @@ def _resolve_static_cache_task(model_type: str) -> ModelTask:
344347
fp8_kv_cache=fp8_kv_cache,
345348
kv_cache_scales=kv_cache_scales,
346349
prune_lm_head=prune_lm_head,
350+
qdq=qdq,
347351
)
348352

349353
_save_package(pkg, output_dir, args, optimize, component_filter)

src/mobius/_builder.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def build_from_module(
160160
fp8_kv_cache: bool = False,
161161
kv_cache_scales: dict[int, tuple[float, float]] | None = None,
162162
prune_lm_head: bool = False,
163+
qdq: bool = False,
163164
) -> ModelPackage:
164165
"""Build an ONNX :class:`ModelPackage` from a module instance and config.
165166
@@ -204,6 +205,10 @@ def build_from_module(
204205
via the causal-LM task so runtimes can avoid full prefill LM-head
205206
projection. Only supported by ``text-generation`` and
206207
``hybrid-text-generation`` tasks.
208+
qdq: When ``True``, lower quantized ``com.microsoft::MatMulNBits``
209+
weights to standard ONNX QDQ form (``DequantizeLinear`` +
210+
``MatMul``) even if the selected execution provider has a native
211+
``MatMulNBits`` kernel.
207212
208213
Returns:
209214
A :class:`ModelPackage` containing the built model(s).
@@ -256,6 +261,7 @@ def forward(self, op, input_ids, attention_mask,
256261
trace=trace_optimization,
257262
fp8_kv_cache=fp8_kv_cache,
258263
kv_cache_scales=kv_cache_scales,
264+
qdq=qdq,
259265
)
260266

261267
_maybe_apply_opset_lowering(pkg, execution_provider)
@@ -401,6 +407,7 @@ def build(
401407
fp8_kv_cache: bool = False,
402408
kv_cache_scales: dict[int, tuple[float, float]] | None = None,
403409
prune_lm_head: bool = False,
410+
qdq: bool = False,
404411
) -> ModelPackage:
405412
"""Build an ONNX :class:`ModelPackage` from a HuggingFace model ID.
406413
@@ -481,6 +488,10 @@ def build(
481488
(``[B, 1, vocab]``). This is intended for single-token
482489
autoregressive generation and is incompatible with workflows that
483490
need per-token logits.
491+
qdq: When ``True``, lower quantized ``com.microsoft::MatMulNBits``
492+
weights to standard ONNX QDQ form (``DequantizeLinear`` +
493+
``MatMul``) even if the selected execution provider has a native
494+
``MatMulNBits`` kernel.
484495
485496
Returns:
486497
A :class:`ModelPackage` containing the built model(s).
@@ -660,6 +671,7 @@ def build(
660671
fp8_kv_cache=fp8_kv_cache,
661672
kv_cache_scales=kv_cache_scales,
662673
prune_lm_head=prune_lm_head,
674+
qdq=qdq,
663675
)
664676

665677
for name, model in pkg.items():

src/mobius/_optimizations.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ def optimize_model(
371371
trace: bool = False,
372372
fp8_kv_cache: bool = False,
373373
kv_cache_scales: dict[int, tuple[float, float]] | None = None,
374+
qdq: bool = False,
374375
) -> None:
375376
"""Apply EP-aware optimization passes to *model* in-place.
376377
@@ -406,6 +407,9 @@ def optimize_model(
406407
per-tensor FP8 scales (from offline calibration). Only used when
407408
``fp8_kv_cache`` is ``True``; layers absent from the map use a unit
408409
scale of ``1.0``.
410+
qdq: When ``True``, force ``com.microsoft::MatMulNBits`` lowering to
411+
standard ONNX QDQ form (``DequantizeLinear`` + ``MatMul``),
412+
regardless of whether *ep* has a native ``MatMulNBits`` kernel.
409413
410414
Raises:
411415
ValueError: If *ep* is not a registered execution provider.
@@ -460,11 +464,13 @@ def _should_inline(func: ir.Function) -> bool:
460464
if func.domain == "com.microsoft" and func.name == "PackedMultiHeadAttention":
461465
return not caps.supports_packed_multi_head_attention
462466
# MatMulNBits (blockwise-INT4) → QDQ (DequantizeLinear + MatMul) for EPs
463-
# without a MatMulNBits kernel (QNN HTP). Supported EPs (CPU/CUDA/…) keep
464-
# the compact contrib op and its native kernel; the function body stays
465-
# registered but uninlined (kernels take precedence over local functions).
467+
# without a MatMulNBits kernel (QNN HTP), or when the user explicitly
468+
# requests standard QDQ operators via the qdq build feature. Supported
469+
# EPs (CPU/CUDA/…) keep the compact contrib op and its native kernel by
470+
# default; the function body stays registered but uninlined (kernels
471+
# take precedence over local functions).
466472
if func.domain == "com.microsoft" and func.name == "MatMulNBits":
467-
return not caps.supports_matmul_nbits
473+
return qdq or not caps.supports_matmul_nbits
468474
return False
469475

470476
inline_pass = common_passes.InlinePass(criteria=_should_inline)

src/mobius/functions/matmul_nbits_test.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def test_inline_matches_native_op_odd_blocks(self):
133133
class TestMatMulNBitsEpGating:
134134
"""A qnn build lowers MatMulNBits to QDQ; a cpu build keeps the contrib op."""
135135

136-
def _build(self, ep: str):
136+
def _build(self, ep: str, qdq: bool = False):
137137
import dataclasses
138138
from collections import Counter
139139

@@ -166,7 +166,11 @@ def _build(self, ep: str):
166166
)
167167
module = registry.get("qwen2")(cfg)
168168
model = build_from_module(
169-
module, cfg, task=_default_task_for_model("qwen2"), execution_provider=ep
169+
module,
170+
cfg,
171+
task=_default_task_for_model("qwen2"),
172+
execution_provider=ep,
173+
qdq=qdq,
170174
)["model"]
171175
return Counter(n.op_type for n in model.graph)
172176

@@ -179,3 +183,8 @@ def test_qnn_lowers_to_qdq(self):
179183
ops = self._build("qnn")
180184
assert ops.get("MatMulNBits", 0) == 0
181185
assert ops.get("DequantizeLinear", 0) > 0
186+
187+
def test_qdq_feature_lowers_to_qdq_on_cpu(self):
188+
ops = self._build("cpu", qdq=True)
189+
assert ops.get("MatMulNBits", 0) == 0
190+
assert ops.get("DequantizeLinear", 0) > 0

tests/cli_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,30 @@ def test_features_prune_lm_head_passed_through(self):
268268
)
269269
assert mock_build.call_args.kwargs.get("prune_lm_head") is True
270270

271+
def test_features_qdq_passed_through(self):
272+
"""--features qdq sets qdq on the build() call."""
273+
with (
274+
tempfile.TemporaryDirectory() as tmpdir,
275+
mock.patch(
276+
"mobius._diffusers_builder._load_diffusers_pipeline_index",
277+
return_value=None,
278+
),
279+
mock.patch("mobius.__main__.build", return_value=mock.MagicMock()) as mock_build,
280+
mock.patch("mobius.__main__._save_package"),
281+
):
282+
main(
283+
[
284+
"build",
285+
"--model",
286+
"some/model",
287+
tmpdir,
288+
"--no-weights",
289+
"--features",
290+
"qdq",
291+
]
292+
)
293+
assert mock_build.call_args.kwargs.get("qdq") is True
294+
271295
def test_features_comma_separated_multiple(self):
272296
"""A single --features accepts a comma-separated list."""
273297
with (

0 commit comments

Comments
 (0)