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)

Open
#29,849 3 comments 0 reactions 0 assignees View on GitHub
quantization stale
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.