OnnxKQuantQuantization skips per-expert MatMuls in static-unroll MoE blocks
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 2.4k
- Forks
- 315
- Avg merge
- 4d 11h
- Merged PRs (30d)
- 16
Description
Summary
OnnxKQuantQuantization only quantizes MatMul nodes whose second input is a 2-D static initializer (kquant_quantization.py:303-308). This is the correct fast path for dense transformer weights, but it leaves a whole class of MatMul nodes unquantized: the per-expert MatMuls in fallback/static-unroll Mixture-of-Experts (MoE) blocks, where the B input is dynamic.
Concrete impact: on a google/gemma-4-26b-a4b-it export from mobius (onnxruntime/mobius#324) the decoder has 7680 expert MatMul nodes (30 layers × 128 experts × 2 matmuls per expert). The k-quant pass quantizes only the 236 dense MatMuls (attention projections, lm_head, …) and leaves all 7680 expert MatMuls at fp16. Resulting Q4_K_M model is only ~6% smaller than the fp16 source (47 GB vs 51 GB).
Pattern
Static-unroll MoE typically packs all experts' weights into a single 3-D initializer [E, fc1_inter, hidden] (matching HF's experts.gate_up_proj) and then dispatches per-expert at runtime:
W_all = [E, K, N] ← static initializer
↓
W_expert = Gather(W_all, [expert_idx], axis=0) # [1, K, N]
W_2d = Squeeze(W_expert, [0]) # [K, N]
y = MatMul(x, W_2d) # ← B input is *not* an initializer
K-quant skips that MatMul because node.inputs[1].is_initializer() is False. The "weight" is effectively a fixed value at runtime but the static-analysis sees a Squeeze output.
Two reasonable fixes
(A) Pattern-aware quantization in OnnxKQuantQuantization. When MatMul.input[1] traces back to a Gather(3D_initializer, axis=0) → (optional) Squeeze → MatMul, slice the 3-D initializer along axis 0 and quantize each [K, N] 2-D plane independently. Reassemble into a 3-D MatMulNBits-style packed initializer indexed by the same expert id. This is the optimal solution for size but invasive: needs a new packed 3-D MatMulNBits op (or GroupedMatMulNBits) that ORT can dispatch.
(B) Pre-pass that unstacks 3-D MoE weights. Add a separate pass (StackedMatMulUnstack or similar) that runs before OnnxKQuantQuantization:
- Find
Gather(W_3d, expert_idx, axis=0) → Squeeze → MatMul(x, ...). - For each constant
expert_idxthat the model could pick (or unconditionally for all E), replace withIf(expert_idx == k, MatMul(x, W_3d[k]), ...). Or simpler: replace the dispatch withEparallel MatMuls and an aggregate, but only one is selected at runtime. - After this pass each per-expert weight
W_3d[k]is a separate 2-D initializer that K-quant picks up automatically.
Option B keeps OnnxKQuantQuantization focused on its single job and is straightforward to implement, at the cost of E times more weight initializers (manageable: 128 experts × 30 layers = 3840 extra inits for Gemma 4 26B, but each is only ~2.2 MB after Q4_K_M).
Workaround today
Drop the fused com.microsoft::MoE op (which is currently broken for standard SwiGLU — see microsoft/onnxruntime#28738) and accept the fp16-weight, large-ONNX, slow-session-load tradeoff. mobius onnxruntime/mobius#324 does this for Gemma 4.
Environment
- olive-ai latest main
- mobius onnxruntime/mobius#324 (Gemma 4 fallback MoE)
- ONNX Runtime 1.27.0
Related
- microsoft/onnxruntime-genai#2062 (original user-facing report)
- microsoft/onnxruntime#28738 (the kernel issue that forced the fallback path in the first place)
- onnxruntime/mobius#324 (the mobius PR exposing this gap)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in olive/passes/onnx/kquant_quantization.py around lines 303-308 and inspect how MatMul initializers are detected and quantized. Compare the two proposed approaches for static-unroll MoE weights, using the Gemma 4 fallback pattern described in the issue. Done means the per-expert MatMuls are quantized instead of remaining at fp16, with the resulting model and runtime behavior validated.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- machine-learning
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100