Skip to content

[IE] Mark constant 2-bit weights for the DynDeq -> FakeQuantize bridge in multi-row graphs - #390

Open
ryugyosoft-max wants to merge 2 commits into
openvinotoolkit:developfrom
ryugyosoft-max:fix-u2-const-multirow
Open

ryugyosoft-max wants to merge 2 commits into
openvinotoolkit:developfrom
ryugyosoft-max:fix-u2-const-multirow

Conversation

@ryugyosoft-max

Copy link
Copy Markdown

Details

Fixes the multi-row 2-bit weights bug of openvinotoolkit/openvino#39010 (also reported as #355).

WeightsDequantizeToDynamicDequantize marks the new IE.DynamicDequantize for the DynDeq -> FakeQuantize bridge pass
only for the KV-cached (one-row) pattern when the weights are 2-bit. A constant 2-bit weight used by a MatMul /
FullyConnected with M >= 2 rows (the prompt / prefill graph of an LLM) is therefore left unmarked and is dequantized
by the DynamicDequantize SW kernel, which returns zeros for these weights on NPU4000 and NPU5010: the layer output is
wrong (max. relative error 1.0 to over 20000 with the reproducer in #39010), while the same weights in a one-row graph are correct.

With this change constant 2-bit weights are marked in the multi-row pattern as well, so they take the same path as in
the one-row graphs (FakeQuantize -> group unroll -> palletized weights on the DPU; the weights stay 2-bit). The
weights-as-inputs (block argument) prefill pattern is not changed (DontBlockArgPrefillPatternToDynamicDequantize
still holds).

Tests

  • weights_dequantize_to_dynamic_dequantize.mlir: U2WeightsConstPrefillPatternToDynamicDequantize now expects the
    marker; new I2WeightsConstPrefillPatternToDynamicDequantize covers signed 2-bit weights without a zero point.
  • LIT, run by hand on a Windows Release build (vpux-opt + FileCheck, the RUN line of each file):
    weights_dequantize_to_dynamic_dequantize.mlir passes for NPU3720, NPU4000 and NPU5010, and
    weights_dequantize_to_dynamic_dequantize_50XX+.mlir passes for NPU5010. The same file fails with the unpatched
    pass (both 2-bit const prefill tests unmarked). It also fails if every 2-bit DynamicDequantize is marked, because
    that breaks DontBlockArgPrefillPatternToDynamicDequantize; this PR therefore limits the change to constant
    weights.

The branch is based on 20e7912, one commit behind develop. The commit it is missing (#386) only changes CI files:
our token cannot push workflow files to the fork.

Validation

Tickets

🤖 Generated with Claude Code

ryugyosoft-max and others added 2 commits October 28, 2026 15:46
…multi-row graphs

finishDynamicDequantizeRewrite() leaves a 2-bit DynamicDequantize unmarked outside the KV-cached (one-row) pattern,
so constant 2-bit weights of a multi-row (prefill) MatMul / FullyConnected are dequantized by the DynamicDequantize
SW kernel, which returns zeros for them on NPU4000 / NPU5010 (openvinotoolkit/openvino#39010). Marked, they take the
FakeQuantize -> group unroll -> palletized path of the one-row graphs (weights still stored as 2 bits). The
weights-as-inputs (block argument) prefill pattern is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
U2WeightsConstPrefillPatternToDynamicDequantize now expects vpux.weights_import_dyn_dequant; the new
I2WeightsConstPrefillPatternToDynamicDequantize covers signed 2-bit weights without a zero point.
DontBlockArgPrefillPatternToDynamicDequantize is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nikolaygorb

Copy link
Copy Markdown
Contributor

I read openvinotoolkit/openvino#39010 and I'd like to proceed with internal testig with your changes.

Just curious: how it was discovered? User-scenario I mean.

Thank you for your PR!

@ryugyosoft-max

Copy link
Copy Markdown
Author

Thanks for picking it up!

Some background: the existing LLM stacks for NPUs tend to lag behind the newest models, so I have been writing my own
apps with Claude that run recent LLMs entirely on the NPU, and publishing them on Hugging Face:

A big goal is Ternary Bonsai 2 (4B / 9B / 27B, ternary weights stored as u2 with a scale per group of 128). I think
models like this can make the NPU much more useful in the age of agentic AI: even if it is slower than a GPU, it runs at
very low power, and for jobs that run overnight the speed hardly matters.

On AMD we program the NPU at a low level, so the Bonsai port went smoothly. On Intel the model has to go through the
compiler, and on the way we hit a few problems - the weights ending up twice in memory, slow compiles on Panther Lake,
and this one.

How it showed up: token-by-token decoding (one-row graphs) was fine, but when we added a prompt (prefill) graph that
handles 64 tokens at once, the output became garbage on NPU 4000 / NPU 5010, while NPU 3720 was fine. We compared the
prefill logits against decoding the same tokens one by one, then narrowed it down layer by layer to a single
FullyConnected with u2 weights and M >= 2 rows - the reproducer in #39010.

For now onw ships its own compiler build with this fix for NPU 4000+, so having it upstream would let the 2-bit models
use the stock compiler again.

@nikolaygorb

Copy link
Copy Markdown
Contributor

Thank you for sharing detailed info.

Could you please share a more details there?
Reproducer profiler details? Benchmark details, arguments, output?

Kindly appreciated.

@ryugyosoft-max

Copy link
Copy Markdown
Author

Sure. Each of the other problems has its own report with measurements, reproducers or profiles, and a reference patch
(all patches and scripts: https://gist.github.com/ryugyosoft-max/bb2f223987db3dae11d680bf5421cd50).

Weights twice in memory (a 1-token decode graph and a 64-token prefill graph over the same weights)

Slow compiles on Panther Lake (Bonsai 2 4B first load ~25 min with the pip compiler -> 285 s with our fixes; one
64-token part 335 s -> ~20 s; blobs byte-identical)

This PR: python repro_u2_mgt1.py (the gist, also inline in #39010) prints the device / compiler / driver
versions and the max. relative error for M = 1, 2, 4, 16; the table in #39010 is its output on Panther Lake before and
after the fix. For the model check we compared the 64-token prefill logits against token-by-token decoding of the same
256 positions (mean KL 0.0005, top-1 agreement 98.4 %, same NLL).

If you meant something else by "there" - or want a specific log or profile attached - just tell me which, and I will
add it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants