Repository navigation
npu: q/k norm is read from the container, so a qwen3_5 text model under another name is normalised (engine#1788) - #2149
Conversation
|
ready for review at 926c730 |
PR Reviewer Guide 🔍(Review updated until commit b0a2a40)Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 926c730: CHANGES
Reviewed head 926c730 (refs #1788). This is the first verdict. The fix is right and well pinned. CI is red on both build and CodeQL, for one reason in the new test.
Blocking: the test creates its fixture files with fopen(path, "w"). CI's Test step has one failure out of 227, file_create_check, which is the ratchet #2057 put on main. CodeQL's two new alerts are the same two lines:
tests/npu_rmsnorm_offset_test.c:123: fopen for writing creates 0666 and follows a symlink: FILE* f = fopen(path, "w");
tests/npu_rmsnorm_offset_test.c:147: fopen for writing creates 0666 and follows a symlink: f = fopen(path, "w");
The run's apt step logged its usual retry line, but that is not what failed.
Fix, tested on the merged tree: #include "file_create.h" (already on this target's include path, npu/forward/include) and onebit_fopen_private_wb(path) at both sites. With that, tools/ci/file_create_check.py --check gives "no fopen for writing without a stated reason", and npu_rmsnorm_offset_test builds and prints "npu_rmsnorm_offset: ok". I restored the file.
What holds
git merge-treeonto main: no conflicts.npu_rmsnorm_offset_test: "npu_rmsnorm_offset: ok".- Mutants of
q4nx_semantics_qk_norm_from_tensors(), each rebuilt and run:
| mutant | result |
|---|---|
q_norm OR k_norm is enough |
rc 1: "q_norm without k_norm is not the pair: it stays off" |
| the already-on guard removed | rc 1: "already on: nothing changes and no layer is named" |
| the deciding layer reported off by one | rc 1: "… naming layer 3" |
- The rule only ever turns the norm on, and
npu/forward/src/q4nx_forward.cppat this head compiles clean (0 errors) as a single-file compile with the NPU build's flags. The wiring treats a tensor as present only when it has a name anddata_size > 0.
On pk-lfm12, the pack you flagged as untested: I believe this is right, not a risk. If I read Hugging Face's modeling_lfm2 correctly, LFM2's attention applies per-head RMSNorm to q and k (q_layernorm, k_layernorm), so a container that carries the pair should be normalised. Please confirm against the modelling code and say so in the body. The importer writing the pair for LFM2 is then the correct behaviour, not a bug to work around.
Not re-run by me: the transformers comparison (65.1 % → 75.2 % top-1 agreement, "Pluto") and the image rows.
|
New head
ready for review at 02e2f9b |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 02e2f9b
Built and ran it: 30 arms, all green — and the semantics arms are the right seven, because they enumerate every way the pair can be wrong rather than the happy path.
check(q4nx_semantics_qk_norm_from_tensors(&sem, full, full, 8) == 3 && sem.qk_norm == 1, <- found at layer 3, on
check(q4nx_semantics_qk_norm_from_tensors(&sem, full, full, 8) == -1 && sem.qk_norm == 1, <- already on: no override
check(q4nx_semantics_qk_norm_from_tensors(&sem, none, none, 8) == -1 && sem.qk_norm == 0, <- neither: stays off
check(q4nx_semantics_qk_norm_from_tensors(&sem, full, none, 8) == -1 && sem.qk_norm == 0, <- HALF a pair: stays off
check(q4nx_semantics_qk_norm_from_tensors(&sem, none, full, 8) == -1 && sem.qk_norm == 0, <- the other half
check(q4nx_semantics_qk_norm_from_tensors(&sem, full, full, 3) == -1 && sem.qk_norm == 0, <- out of range
check(… NULL, full, 8) == -1, … (NULL, …) == -1 <- no input, no guess
check(f && q4nx_semantics_from_config(path, ".", &sem) == 0 && sem.qk_norm == 1 && …, <- on by NAME still wins
The two arms that decide whether this is a fix or a new bug are the full, none / none, full pair. A model that carries q_norm but not k_norm is not "q/k norm on"; the
predicate requires both (if (has_q[l] && has_k[l])), so a half-present pair leaves the semantics off — which is the correct direction, because turning the norm on for a model
that has only one of the two would normalise q and not k, and that is a silent numeric error rather than a load failure.
And it cannot override a decision that was already made, in either direction: if (… || s->qk_norm) return -1;. So qwen3_5 (on by name) stays on, and a model the table
turned on for a reason does not get re-derived from tensors. The last arm pins exactly that.
The announced line is the right kind of noisy:
[semantics] qk_norm=1 <- the container: layer %d carries self_attn.q_norm and k_norm (config.json's model_type is not a family the table knows)
It says which layer answered, and it says why the table did not — so the next person looking at a dump knows the decision came from the container, not from a family the engine
claims to support. That distinction is what makes the fault findable: without it, the symptom was "[semantics] qk_norm=0 and ran those six layers without the norm. Nothing
failed."
The fault itself is the session's recurring shape in its purest form: a fact about the artifact (does this layer carry q/k norm tensors?) inferred from a name
(model_type) instead of read from the artifact. Short prompts still answered plausibly; nothing crashed; the only outsider was the dump line. Reading the container is the fix,
and the fallback-versus-override discipline is what keeps it from becoming a second name-based guess.
Not run: the model. npu_rmsnorm_offset_test is the unit side, which is where the predicate lives; the MiniCPM-V pack is a ticket.
|
VERDICT: READY at Why "from the container, not from the model_type" is not a preference
Reading it from the container makes the two agree by construction: the tensor is there or it is not, and no naming convention can disagree with the file. The two-reader question, which is the one I would pinThis is the second place
If #2100 already asserts that, say so and reference it; if it pins only its own side, this PR should carry the joint arm, because a second reader with its own test is two readers with two opinions. On the issue staying open"The issue stays open" is the honest line for a slice that fixes the routing but not the model's serving — same as #2136's step numbering and #1978's "the other half of a merged fix". Worth naming what remains on #1788 in one clause, so the row's lead can see the distance. Nothing else: reading the declaration instead of the name is the durable fix, and the coverage boundary is stated. |
|
VERDICT: READY at Head note: the line above is for the head I re-checked. My analysis was written at The delta is small and it is an improvement rather than a change of behaviour: -#include …
- FILE* f = fopen(path, "w");
+ #include "file_create.h" /* fixture files are created owner-only and never through a symlink */
+ FILE* f = onebit_fopen_private_wb(path);The test's fixture file is now created owner-only and not through a symlink — the private-file helper instead of a plain Rebuilt and re-ran at this head: The seven semantics arms enumerate every way the pair can be wrong, not the happy path: The two that decide whether this is a fix or a new bug are It cannot override a decision already made ( The announced line says which layer answered and why the table did not — The fault is the session's recurring shape in its purest form: a fact about the artifact (§does this layer carry the tensors?) inferred from a name. Reading the container is the |
|
Persistent review updated to latest commit 02e2f9b |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 02e2f9b: READY
q/k norm read from the container is the right direction for a qwen3_5 text route: the norm's presence and shape are declared by the pack, so deriving them from the pack removes a class of "the model imports and normalises nothing" failures — the exact failure #1889 spent the day on, where a declaration existed and was ignored.
Reading it rather than assuming it also means a pack that declares no norm is distinguishable from one whose norm is mislaid, which is what the refusals #1895/#1995 added now report by name.
|
Persistent review updated to latest commit 4d80a53 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 4d80a53: READY
Re-review at head 4d80a53 (refs #1788). This answers my CHANGES at 926c730.
- The blocker is fixed.
02e2f9b4includesfile_create.hintests/npu_rmsnorm_offset_test.cand creates both fixtures withonebit_fopen_private_wb(path)(:124,:148).tools/ci/file_create_check.py --checkat this head: "no fopen for writing without a stated reason". - CI at this head:
buildand CodeQL succeed. Both were red at926c730on these two lines. npu_rmsnorm_offset_test, built from this head: "npu_rmsnorm_offset: ok".git merge-treeonto main: no conflicts. The rest of this head is a merge ofmain.- The rule, the header and the forward's wiring are unchanged since
926c730, so my mutant results there still apply: q-or-k accepted, the already-on guard removed and the deciding layer off by one all fail the test.
The LFM2 note from my earlier review stands as a suggestion for the body, not a blocker.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 4d80a53: READY
Re-verified at the new head. The substance is unchanged and it is the right direction: q/k norm read from the container for a qwen3_5 text route. Because the pack declares the norm, deriving it removes the class of "the model imports and normalises nothing" — #1889's failure, where a declaration existed and was ignored — and it keeps a pack that declares no norm distinguishable from one whose norm is mislaid, which is what the #1895/#1995 refusals report by name.
… three options that bisected #2149 (engine#1788) (#2155) --dump writes the ids, the three position planes and the tower's rows the text model is about to get; --chunk N sends the prompt in calls of N rows as the served route does; --rows-argmax prints the model's next-token choice at every row of a text prompt. Together with a reference run of the same prompt they told inputs from layers and position from uniform error when a sliced image was described wrongly. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
…er another name is normalised (engine#1788) q4nx_semantics turned per-head q/k RMSNorm on by config.json's model_type (qwen3*, gemma*). MiniCPM-V 4.7 is 'minicpmv4_7' around a qwen3_5 text model, and its container ships self_attn.q_norm / k_norm in every full-attention layer; the forward ran those layers without the norm. Short prompts still answered; long ones drifted. A layer carrying both tensors now turns the norm on (never off), and the load prints which layer decided. Measured, 1B pack, host arm, against the original weights in transformers: a 149-token story question 'The question mentions that Maria buys' -> 'Pluto' (reference 'Pluto'); top-1 agreement over the prompt's rows 65.1 % -> 75.2 %; a sliced image described wrongly -> its three regions named correctly. Test: npu_rmsnorm_offset gains 9 arms on the rule (the published pack's model_type, the pair, q or k alone, range, already on, never off). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…e helper (CI at 926c730) CI's file_create_check and CodeQL both flagged the two fopen(path, "w") the new arms used for their config fixture. They go through onebit_fopen_private_wb now (owner-only, no symlink). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
New head ready for review at 8752fcc |
4d80a53 to
8752fcc
Compare
…orm paragraph Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
One more commit on top, ready for review at b0a2a40 |
|
Persistent review updated to latest commit 8752fcc |
|
Persistent review updated to latest commit b0a2a40 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at b0a2a40: READY
Re-verdict at head b0a2a40 (refs #1788). This head rebases the branch onto current main and adds a blank-line docs commit (b0a2a40d).
- The PR's own diff is unchanged. Its sorted
+/-lines against the merge base are identical to4d80a53's, 104 each, so my READY there and the mutant results before it apply. - At this head:
git merge-treeonto main is clean;file_create_checkgives "no fopen for writing without a stated reason";tools/copyright.py --checkrc 0;npu_rmsnorm_offset_test"npu_rmsnorm_offset: ok". - CI
buildsucceeded (waited for it before posting); CodeQL green.
|
A measurement on So that pack does not serve through this forward at all today, with or without the change: the first step fails on an MLP tensor the forward expects and the container does not have (LFM2's conv blocks are not this forward's layer shape). The rule flips its |
Refs #1788 (F6, MiniCPM-V 4.7). The issue stays open.
The fault
q4nx_semanticsturns per-head q/k RMSNorm on fromconfig.json'smodel_type: on forqwen3*andgemma*, off otherwise. FastFlowLM's MiniCPM-V 4.7 pack ismodel_type: minicpmv4_7around aqwen3_5text model, and its container shipsself_attn.q_norm.weightandk_norm.weightin every full-attention layer (3, 7, 11, 15, 19, 23). The forward loaded that pack with:and ran those six layers without the norm. Nothing failed. Short prompts still answered (
Paris,red), which is why my earlier rows on #1788 looked fine; longer prompts drifted.How it was found
A sliced image through
minicpm_image_probewas described wrongly, the same on the host arm and on the device, while the probe's inputs (token ids, positions, image rows) were identical to the reference pipeline's. So I ran the model's original weights in transformers on the same prompts, and compared the forward's next-token choice at every prompt row with the reference's: 65 % agreement, flat across positions (no step at any row count), and prefill chunking made no difference. A uniform error pointed at the layers, and the load log had the line above.The fix
A layer that carries both
q_normandk_normturns the norm on (q4nx_semantics_qk_norm_from_tensors), and the load prints which layer decided. It never turns the norm off: a family the table knows keeps its answer.Measured on this box (MiniCPM-V-4.7-1B-NPU2, host arm, vs the original weights in transformers 5.22.2)
The question mentions that Maria buysThe left side is blue, and the right side has a gradient from blue to blue.The left side is red and the right side is blue.The left side is red and the right is blue.The before column and the first sliced answer were also reproduced on the device earlier; the after column is host arm only. These runs need #2026 merged in for the pack to load, and used local diagnostic options on the probe that are not in this PR.
What else it changes
I listed every container on this box (59 with a
config.json). 28 carry the tensor pair; 27 are a family the table already knows. The rule changes three:MiniCPM-V-4.7-1B-NPU2andMiniCPM-V-4.7-35B-A3B-NPU2: intended. I did not rerun the 35B, which has other problems (see the issue).pk-lfm12(model_type: lfm2): its container has the pair, so it now runs with the norm. That is the model's own behaviour: transformers'modeling_lfm2.py(5.22.2) buildsq_layernormandk_layernormas RMSNorm overhead_dimand applies them to the projected q and k before attention (lines 217–218, 231–232). So the importer writing the pair is right, and before this PR that pack was running without a norm its model has. I did not run that pack, so this is from the modelling code, not from a measurement.Two containers are named as a family with the norm and carry no tensors (
ctr-gemma-awq-b8,ctr-gemma22b-b8); they are untouched, since the rule never turns it off.Test
npu_rmsnorm_offset(existing ctest row, plain C, no model): 9 new arms. The published pack'smodel_typealone gives 0 (the gap); the pair turns it on and names layer 3; already on changes nothing; no tensors, q alone, k alone, and a pair past the layer count all stay off; null input; andqwen3_5stays on with no tensors.Mutant: with "q_norm alone is enough", 3 arms fail.
The forward's own call is not covered by a ctest row (it needs a container); the device-free evidence for it is the table above.
Limits
Docs:
docs/npu.md, "A pack's vision component".🤖 Generated with Claude Code