Repository navigation
tools: q4nx_diff names the layout that matches, instead of calling a good container BAD (#1110) - #1116
Conversation
…good container BAD `q4nx_diff.py decode --layout <flm|ours>` compares a container against its source GGUF, but it compares *one* layout chosen by the caller, and its pass condition was "no element differs anywhere". Both halves mislead: * a container packed from a Q4_K_M GGUF into q4_1 is a REQUANTIZATION, so exact equality is impossible -- the tool reported every honest container as `BAD` (measured: a correct Qwen3-0.6B pack shows worst max|err| 4.1e-2, and our own ZR1 pack 5.2e-3); * picking the wrong layout turns that into a confident "BAD" for a good container while another layout would have matched -- which is how the converter mismatch behind engine#1110 stayed invisible: the same container reads 4.1e-2 under `--layout ours` and 1.6 under `--layout flm`. So `--layout both` tries each layout, and the verdict names the one that matched (or says none did, and which was closest). "Matches" is a tolerance, not equality: worst max|err| <= 0.1 (the Q4_K_M -> q4_1 requantization error measured on this box) AND at least 3x better than the rival layout. `--self-test` pins the verdict on six synthetic result sets without touching a model. good container -> "matches layout ours (worst max|err| 4.130e-02, next layout flm 1.638e+00)" rc 0 bad converter -> "matches NO layout: closest is flm ... the container's tiles are not these weights" rc 1 our own pack -> "matches layout ours (worst max|err| 5.239e-03, next layout flm 2.001e+00)" rc 0 The two decoders are unchanged, `--layout flm|ours` still works exactly as before, and this is the check engine#1110 proposes to run against a converter: it needs no engine rebuild and no device.
3ed5914 to
1158836
Compare
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 1158836c — the verdict line does what it claims, and I ran it against a real container rather than a synthetic pair.
$ q4nx_diff.py --self-test
self-test passed: the verdict names the matching layout, accepts a lossy-but-correct pack, and refuses tiles from other weights
$ q4nx_diff.py decode --gguf ~/models/zyphra-zr1/ZR1-1.5B-bf16.gguf \
--q4nx ~/models/npu/ZR1-1.5B-npu-q4km-v2 --layout both
layout=ours: exact=0 inexact=197 skipped=58 padded=0 worst max|err| 7.617e-02
matches layout ours (worst max|err| 7.617e-02, next layout flm worst max|err| 3.199e+00) rc 0
Two things in that output are the argument for the PR. First, the honest container now gets a verdict with a margin — 7.6e-2 for the layout that matches against 3.2e+0 for the one that does not, a factor of ~42 — so "which layout is this" is answered by the data instead of by the flag the operator guessed. Second, printing the runner-up is what makes the wrong-guess failure mode impossible to hide: in the failing case the two numbers will be indistinguishable or inverted, and a reader sees that rather than a bare BAD.
The per-tensor BAD lines still print, and I think that is right: the tensor threshold is a lossy-repack reality (Q4_K_M → q4_1 cannot be element-exact), and the new summary is the thing that decides. The apparent contradiction — 197 "BAD" rows and rc 0 — is exactly the confusion this PR removes, and the verdict line resolves it in one sentence.
Ordering note: #1119 also touches tools/q4nx_diff.py and predates this one (its --layout has only flm|ours, no both). So land #1116 first, then #1119 rebased on it — the other order drops the both option, which is the piece that made the #1110 mismatch visible.
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
Towards #1110 (this is the check, not the converter fix — no
closes).Scope note (2026-11-05 11:4x): the padded-grid decode half I first put on this branch now lives in
#1119 (pi-npufeat,
tools/q4nx_diff.py+tests/q4nx_diff_grid_test.py+ oneadd_test()), so only one PRcarries that logic and it gets a CI test. This PR is now exactly the verdict change below:
--layout both, thetolerance, the verdict line and
--self-test.q4nx_diff.pyis touched by both PRs in the same function(
_decode_pass/decode_cmd): whichever lands second needs a small rebase, and I will rebase this one onto #1119when it merges — the two changes are independent in behaviour.
q4nx_diff.py decode --layout <flm|ours>compares a container against its source GGUF, but it compares one layout chosen bythe caller, and its pass condition was "no element differs anywhere". Both halves mislead:
reported every honest container as
BAD(measured: a correct Qwen3-0.6B pack shows worstmax|err|4.1e-2, our own ZR1pack 5.2e-3);
which is exactly how the converter mismatch behind npu: a container whose q4_1 tile order differs from ours decodes as noise with no error, and no check catches it #1110 stayed invisible: the same container reads 4.1e-2 under
--layout oursand 1.6 under--layout flm.What changes
--layout bothruns each layout and the verdict names the one that matched — or says none did and which was closest."Matches" is a tolerance, not equality: worst
max|err|≤ 0.1 (the Q4_K_M → q4_1 requantization error measured on this box)and at least 3× better than the rival layout.
--self-testpins the verdict on six synthetic result sets, no model andno device needed.
Measured (this box, today)
~/wt/npu-rt-753-pr/…)matches layout ours (worst max|err| 4.130e-02, next layout flm 1.638e+00)~/amd-oss/FLM_Q4NX_Converter(d1d5232)matches NO layout: closest is flm with worst max|err| 1.149e+00 (> 1.000e-01, or not 3x better than ours) - the container's tiles are not these weightsZR1-1.5B-npu-q4km-v2packmatches layout ours (worst max|err| 5.239e-03, next layout flm 2.001e+00)So it separates the two converter generations by ~50× on the same model, weights and prompt-free path, and it is what a
converter sanity gate needs: no engine rebuild, no device, no
npu-hold.Scope
The two tile decoders are unchanged and
--layout flm|oursstill behaves exactly as before; this only addsboth, the verdictline, the tolerance and the self-test.
tools/copyright.py --checkrc 0.Which layout is which — corrected by pi-npufeat's measurement (see #1110), because my first wording ("
flmlooks like an oldergeneration") was too simple. Repacking the same GGUF with both converters and trying every variant:
--layout ours--layout flmthird_party/npu-rt-753-pr(the one that works)npu/forwardreads~/amd-oss/FLM_Q4NX_Converter(d1d5232)So
d1d5232's tile order depends on the source quant type (or changed mid-way) — there is more than one difference, no single"flm order" describes it, and a guard that tries to detect the flm order would be guessing. That is why the gate this PR adds
matters in the shape it has: it names the layout that matches or says none does, and the durable fix for #1110 is a declared
tile order in the container, not another decoder guess. I did not touch
decode_flm; #1119's second commit (5576165) makes thetool's docstring say per layout what is validated against what.