microsoft / microsoft/onnxruntime
[CPU] MatMulNBits accuracy_level=4 (int8 activation quant) selects wrong argmax token on massive-activation LLMs (Qwen3-0.6B, Phi-3.5-mini)
- Dominant language
- C++
- Stars
- 21.9k
- Forks
- 4.2k
- Avg merge
- 4d 11h
- Merged PRs (30d)
- 184
Description
### Describe the issue
On the **CPU EP**, `com.microsoft::MatMulNBits` with **`accuracy_level=4`** (which int8-quantizes the **activation** `A` before an int8 matmul) can select a **different, incorrect argmax** for the output logits compared to `accuracy_level` 0/1/2/3, on transformer LLMs that have large-magnitude ("massive activation" / outlier) channels.
Because the ORT-GenAI model builder exports these models with `accuracy_level=4` **by default**, greedy decoding on CPU produces a **wrong token stream** relative to a higher-precision reference — silently. The first higher-precision level that agrees is `accuracy_level<=3`; only level 4 diverges.
We isolated this by building an fp32 oracle: take the exported `model.onnx`, rewrite **every** `MatMulNBits.accuracy_level` to `0`, and greedy-decode. Levels `0/1/2/3` all agree with each other; only the default `4` flips the token. The flip happens at **near-tie logits**, consistent with int8 activation quantization being too coarse for channels with large dynamic range.
### Two reproducible cases (fp32 oracle vs `accuracy_level=4`)
| Model (ORT-GenAI export) | Node width | Greedy token that flips | fp32 / acc 0-3 (correct) | acc=4 (default, wrong) |
|---|---|---|---|---|
| **Qwen3-0.6B** (`qwen3-0.6b-generic-cpu`) | has **8-bit** MatMulNBits | **decode index 0** (first token!) | `1479` | `3988` |
| **Phi-3.5-mini-instruct** (`generic-cpu`, int4 block-32) | 4-bit | decode index 65 | `263` | `6455` |
For Qwen3-0.6B the **very first generated token** is wrong under the default export. Both were reproduced independently by more than one person, and cross-checked: `accuracy_level` 1 (fp32), 2 (fp16), and 3 (bf16) all pick the correct token (`1479` / `263`); only `4` (int8 activation) picks the wrong one.
### Root-cause hypothesis
`accuracy_level=4` per-row/per-tensor int8-quantizes the activation vector `A` before the int8 GEMM. On these models a few channels carry very large magnitudes ("massive activations"), so an int8 activation scale loses the precision needed to resolve near-tie output logits, flipping the argmax. Keeping activations in fp32 (or int16) yields the correct token. Notably this bites the **8-bit-weight** nodes hardest (Qwen3-0.6B first token).
### Reproduction sketch
```python
import onnx, numpy as np, onnxruntime_genai as og # or drive the graph directly
# 1) fp32 oracle: rewrite all MatMulNBits accuracy_level -> 0
m = onnx.load("model.onnx") # keep external data
for node in m.graph.node:
if node.op_type == "MatMulNBits":
for attr in node.attribute:
if attr.name == "accuracy_level":
attr.i = 0
onnx.save(m, "model_acc0.onnx", save_as_external_data=True, location="model_acc0.onnx.data")
# 2) greedy-decode the SAME prompt with the original (accuracy_level=4) model and model_acc0.onnx
# on the CPU EP, compare the generated token ids.
# Observed: they diverge — original picks the wrong token, acc0 matches the fp32 reference.
# (Repeat rewriting accuracy_level to 1/2/3 -> all agree with 0.)
```
Using ORT-GenAI or a raw greedy loop over the decoder graph both reproduce it. Prompt used for Phi-3.5 was a short "Hello"-style prompt; the divergence is deterministic (greedy).
### Urgency
Medium/High for correctness-sensitive CPU inference: default-exported quantized LLMs decode **incorrect greedy tokens** on CPU, differing from the fp32/fp16/bf16 reference, with no warning.
### Suggested direction
- Consider a higher-precision **int16 activation** path for `accuracy_level=4` (preserves the correct token in both cases above while staying faster than fp32), and/or
- per-channel activation scales for large-dynamic-range channels, and/or
- document that `accuracy_level=4` can change argmax on massive-activation models and pick a safer default for greedy/correctness-sensitive use.
### Platform / version
- onnxruntime **1.27.0**, CPU Execution Provider, Linux x86-64
- Models: ORT-GenAI model-builder exports of Qwen3-0.6B and Phi-3.5-mini-instruct (generic-cpu)
Contributor guide
Research direction
Start with the CPU EP implementation and entry points for com.microsoft::MatMulNBits, then reproduce the issue using the provided model.onnx rewrite and greedy decoding comparison. Check accuracy levels 0–4 on the Qwen3-0.6B and Phi-3.5-mini cases; done means the incorrect argmax is addressed or the behavior is explicitly documented with a safer default or supported precision path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, python
- Domain
- machine-learning
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100