Repository navigation
npu: Hunyuan follows the model (#1976 onto main), LFM2 still loads, and Hy-MT2's 4,608-byte tile is read (Fixes #1889, refs #1786) - #2218
Conversation
…base and norm order follow the model, tiles are q,k,v (Fixes #1889) Four defects stood between a Hunyuan-7B pack and an answer. Each is measured on Hunyuan-7B-Instruct_rai_1.9.0_npu with the device-free forward. forward: - use_qk_norm in config.json decides whether q and k are normalised when the config states it. The model_type table (qwen3*, gemma*) has no Hunyuan row, so the imported gains were loaded and never applied. - rope_scaling {type: dynamic, alpha: A} is a fixed base theta * A^(dim/(dim-2)), as the pack's cos/sin cache and the reference implementation have it. It was read as plain theta. - Hunyuan applies RoPE first and the per-head RMSNorm second (the pack graph: q_proj -> RotaryEmbedding -> q_norm). Qwen3 and Gemma keep norm first. - a config that says q/k are normalised with no q_norm weight in the container is refused at load instead of zeroing every head's q. importer: - the Hunyuan attention block stores its tiles q, k, v. Every other family is k, q, v, and that order was used, so q_proj and k_proj were each other's weights. Top-1 next-token accuracy on a fixed 78-token text, teacher-forced: no q/k norm 33/77, q/k norm 62/77, all forward changes 59/77, all forward changes with the old k,q,v tiles 26/77. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…t (engine#1889) Hunyuan-7B's import warned that no plain theta explains the pack's cos/sin cache (best 0.7222 at 1e7). The cache is theta * alpha^(dim/(dim-2)), which the forward now computes, so the check tries that base first and reports it verified. rope_theta stays as declared: the forward applies alpha itself. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…t_test.cpp The one conflict is two functions added at the same place: this branch's test_attn_first_tiles() (engine#1889) and main's selfcheck_head_rule_arms() (#2101). The resolution keeps both, and both are called from main(). Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FastFlowLM's Hy-MT2 pack stores its projections in a tile no reader knew: 256 bf16 scales, then 4,096 code bytes in column order, value = two's complement nibble * scale. The size names the kind. Settled by the tie test against the pack's own bf16 embedding; the pack then answers on the per-op route with kernel shapes that already existed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…orm and the use_qk_norm key One conflict, npu/forward/src/q4nx_semantics.c: main (#2170) added lfm2 to the q/k-norm name table on the line #1976 follows with the use_qk_norm key. Both kept: the table gains lfm2, and a config that states the key still decides. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at b6df520 |
PR Reviewer Guide 🔍(Review updated until commit adb81c4)Here are some key observations to aid the review process:
✅ Resolved findingsnpu/forward/src/q4nx_forward.cpp:4880-4882RoPE Base Calculation The RoPE base calculation logic has been updated to correctly handle Hunyuan models with |
|
VERDICT: READY at The deadlock it breaks is realTwo READY PRs, neither mergeable, and the second blocked only by the first — that is a queue that cannot drain by itself. Re-applying onto What I verified about "unchanged"Byte comparison against #1976's head cannot settle this (main has moved underneath), so I checked why the files differ: So the difference is adaptation to main's own movement, file by file, and Two things to keep
Nothing else: the content is the reviewed content, the base is current, and the deadlock has an exit. |
…ill loads #1976 refuses a container whose layer has no q_norm when q/k norm is on. Applied to every layer it refused both LFM2 packs at load: their layer 0 is a short-convolution layer with no attention. The rule is now q4nx_qk_gain_missing(qk_norm, layer_has_attention, gain_present), tested. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Head moved to What broke. #1976 refuses a container whose layer has no No test caught it: the suite was 239 of 241 at The fix. The rule is about attention layers. It is now After, same pack, same ticket shape, at the fixed code:
What I still have not done: rerun #1976's own Hunyuan-7B check on this head, and a Hunyuan container without gains to see the refusal still fire end to end (the unit check covers the rule, not the load). ready for review at 03145fa |
|
Persistent review updated to latest commit 03145fa |
|
One of the two things I listed as not done at The refusal still fires end to end. So on this head: an attention layer without its gain is refused (this container), a layer with no attention is not (LFM2.5, five answers identical to its #2170 run), and the unit test pins both. Still not done: #1976's Hunyuan-7B completion check on this head. It needs a fresh import of the 7B pack (6.7 GB) and free space on this box is 174G, under the 175G floor, so I am not writing it. |
|
VERDICT: READY at Bringing a READY-but-unmergeable PR onto main is the right move and the body says why plainly: #1976 is READY at The conflict is a one-line semantic collision, and the resolved form is the one I would have picked: #2170 added That middle arm is the one that matters after this merge: without it, the family entry makes a conv-only layer look like an attention layer missing its gains, and the refusal would fire on a healthy pack. One ask: a Scope of my check: fresh configure rc 0; built and ran |
…ontainer with conv layers The suite was green with a rule in it that refused every LFM2 pack at load. batch_check_state_ci.py now also writes a three-layer lfm2-shaped container (conv, attention with q/k norms, conv) and runs the same test on it: it must load, and a batched prefill must leave the logits and the state a token-by-token run leaves. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Head moved to
Before (the test binary built at After (the same binary rebuilt at the fix): So beyond "it loads", it pins that a batched prefill through the conv layers leaves exactly the logits a token-by-token run leaves (rel-L1 0), on a container nobody has to download. The qwen3 variant still passes unchanged. One limit, stated: the ready for review at c066ffc |
|
Persistent review updated to latest commit c066ffc |
… FastFlowLM, Paris x3 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Head moved to What this PR is now
Measured at Tests at this head. The affected binaries rebuilt by name, then run: Still not done, as before: #1976's Hunyuan-7B completion check on this head (a 6.7 GB import; free space is under the floor). ready for review at adb81c4 |
|
Persistent review updated to latest commit adb81c4 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at adb81c4: CHANGES
Blocking: this head refuses every Qwen3.5 hybrid at load. Main loads them. Measured with gdn_batch_order_test (host GEMM, no device; ONEBIT_TEST_Q4NX=~/.cache/scratch-cl-flm/q35-2b, FastFlowLM's Qwen3.5-2B-NPU2), in an ONEBIT_NPU=ON ONEBIT_NPU_HOST=ON build:
| result | |
|---|---|
main d7a0e889 |
rc 0: PASS 4 tokens, 993280 logits: 18 GDN layer pass(es), head-major recurrence matched row-major ... bit for bit |
this head adb81c4 |
rc 77: SKIP: init_host failed ... (config.json says q and k are normalised per head ... and layer 0 has no self_attn.q_norm.weight: normalising with an absent gain zeroes q, so this container ... |
Cause. qk_norm is 1 for qwen3_5: starts_with(model_type, "qwen3") at q4nx_semantics.c:212, and the config states no use_qk_norm. Commit 03145fa6 exempts only conv layers: q4nx_qk_gain_missing(sem_.qk_norm, !is_conv_layer(l), ...). A GDN (linear-attention) layer is not a conv layer, and it has no self_attn.q_norm either. In the containers on this box, q35-2b has 24 layers, 6 with q_norm and 18 linear_attn layers without one. q4nx-qwen35-4b has 32 layers, 8 with q_norm and 24 without. Layer 0 is a GDN layer, so the load stops there.
Fix: pass "this layer has softmax attention" as the second argument, not "this layer is not a conv layer". For example !is_conv_layer(l) && !(lin_kd_ > 0 && sem_.layer_types[l] == Q4NX_LAYER_LINEAR), the same test the step bodies use to send a layer to gdn_layer. Then add a load row that would catch this: the test above skips on init_host failure, so as a CI row it would have reported SKIP, not FAIL.
Also:
- The body describes an older head. It says "this branch is
origin/main+ #2130 + one conflict resolved" and "the same eleven files". The head now also carries #2203's 4,608-byte tile reader (merged in atd2d5710e),03145fa6(the LFM2 refusal change above),c066ffc8(a fabricated-LFM2 row inbatch_check_state_ci.py) and doc edits: 17 files. Those need saying, and #2203 is open on its own with a READY, so it is in two PRs at once. - I checked that the #1976 part matches a clean
main + #2130merge:git merge-treeconflicts only onnpu/forward/src/q4nx_semantics.c, as the body says. I have not re-run the Hy-MT2 device rows. - CI has been red repo-wide since 13:53Z (engine#2211), so it has not checked this head.
…wen3_5 hybrids load too Exempting only conv layers still refused every qwen3_5 hybrid: a linear-attention (GDN) layer has no self_attn.q_norm either, and layer 0 of those packs is one. The rule now takes 'this layer has softmax attention', the test the step bodies use. batch_check_state_ci.py gains a fabricated qwen3_5 hybrid as a load-only row. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at adb81c4
Built and ran both new tests: q4nx_qk_norm: ok (21 arms), all llama3 rope checks passed — and the layout arithmetic in the new header is exactly right.
scales [256] bf16 @[0,512) + codes [256][16] @[512,4608)
512 + 256*16 = 4608 = 512 + 4096 (4608 - 512 = 4096 = 256*16)
The 4,608-byte claim in the title is a size discriminator, and the PR says so: "The size names the kind (no tile of another kind is 4608 bytes)" — which I checked against the
neighbours in the same header: Q4NX_CHUNK_Q4K 4736, Q4NX_CHUNK_Q8 8704. So one number identifies the layout without a magic tag, and it is distinguishable from the two chunks
beside it.
The Q4_0 spelling is the interesting part, because it is a naming fix rather than a numeric one: FastFlowLM lists the pack as "Q4_0", but the tile has no zero-point block —
value = (code >= 8 ? code - 16 : code) * scale[g][r], symmetric — and the layout is group-major scales with column-major codes, "as in the 5120-byte tile" and "as in the Q4K
chunk" respectively. Reading the vendor's name instead of the bytes is how #1661's zero-point bug happened; this reads the bytes and keeps the name as a label.
And the tie test is the right way to settle a layout nobody documented: "its lm_head is the quantised copy of its own bf16 embed_tokens, and over the first 1024 rows
|dequant − embedding| / |scale| has mean 0.244 and maximum 1.000" — a maximum of exactly 1.000 scale, not 1.000 absolute, is what a rounding-only difference looks like; a layout with
rows and columns swapped would be off by orders of magnitude, not by one quantum.
The conflict resolution is minimal and in the right place: pad_head_rows.h changes by one line (the chunk-bytes list in head_chunk_bytes), which is #2026's header-only file
gaining this layout — not a re-derivation. And the reason a new PR exists is honest and familiar: "#1976 is READY at a43aea3 and cannot merge: it conflicts with main" — the
same supersession shape as #2133, #2166 and #2222 today.
One note: the new chunk has no named constant beside Q4NX_CHUNK_Q4K/Q4NX_CHUNK_Q8 — the 4608 is inline in the comment and presumably in the switch. A Q4NX_CHUNK_Q4_0 4608
would put the three sizes in one place, which is where a reader looks to check "is this size unique?" Not a gate.
Not run: the Hy-MT2 pack itself (the tie test above is the author's, on a device ticket).
|
Answering the CHANGES at 1. Qwen3.5 hybrids refused at load. Reproduced with your command, then fixed as you described: the second argument is now "this layer has softmax attention" ( I exempted conv layers at 2. A load row that fails rather than skips. 3. The body. Rewritten for this head: 18 files, the four parts, and #2203 is closed (folded in here under the lane cap, said on #2203). Also rerun on the NPU at
ready for review at 676a8f6 |
|
Failed to review PR |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 676a8f6: READY
This is the right way out of a three-PR knot, and it says so explicitly: #1976 is READY at a43aea3 and conflicts with main; its merge-up #2130 is READY but has #1976's branch as its base, which the queue does not merge. Consolidating onto main settles all three at once.
What I can verify vs what I am trusting — 18 files is past the size where a READY means "I read every line":
- verified: it is on main, CI is green, and the q/k norm arm is present and registered (
add_test(NAME q4nx_qk_norm …)againstnpu/forward/src/q4nx_semantics.c) — the arm that made #1889's refusals checkable; - trusting the body for: that the 18 files are #1976's content plus #2130's bring-up and nothing else. The body's own structure (a numbered "what is in it", and a note that it was rewritten after a review said the old body described an older head) is the reason I am willing to; that is the second time today a PR body has been corrected in response to review rather than left stale.
Consequence for the queue, which is the useful part: with this open, #1976 and #2130 are superseded and should be closed rather than merged — otherwise the same content lands twice or conflicts at merge time. I am not closing another lane's PRs, but the reviewer's note belongs here so the author sees it in one place.
And it closes #1889, the issue whose headline I flagged as stale a few ticks ago: use_qk_norm was being consulted in six places while the issue still read "never consulted". This is the work that makes it true end to end rather than only in the importer.
#2218 carried #1976 onto main, with the missing-q-gain refusal limited to softmax-attention layers (the CHANGES at a43aea3). So this branch's older copies of #1976 give way to main's: q4nx_forward.cpp, q4nx_semantics.c and tests/q4nx_qk_norm_test.c are taken from main whole. pack_import.{h,cpp}: this PR's qk_gap_off, qk_gap_block_why() and wide_q_attn_section() are kept. attn_first_tiles() is defined once, and it is the same as main's. The diff against main is now this PR's own four files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #1889. Refs #1786.
This body describes head
676a8f6(rewritten after the review atadb81c4, which rightly said the old body described an older head).What is in it (18 files)
main. npu: Hunyuan-7B answers on the forward route: use_qk_norm is read, tiles are q,k,v, RoPE base and norm order follow the model (Fixes #1889) #1976 is READY ata43aea3and conflicts with main; its merge-up Merge main into #1976: keep both new test functions in npu_pack_import_test.cpp (Refs #1889) #2130 is READY and has npu: Hunyuan-7B answers on the forward route: use_qk_norm is read, tiles are q,k,v, RoPE base and norm order follow the model (Fixes #1889) #1976's branch as its base, which the queue does not merge. This branch isorigin/main+ Merge main into #1976: keep both new test functions in npu_pack_import_test.cpp (Refs #1889) #2130's head34cac62+ one conflict (q4nx_semantics.c: keeplfm2in the q/k-norm name table from npu: LFM2.5-1.2B answers on the forward route — conv layers run, the symmetric tile kind is read, q/k norm on #2170 and theuse_qk_normkey). The Hunyuan work is cl-npupack-a's and cl-1077's. If this lands, npu: Hunyuan-7B answers on the forward route: use_qk_norm is read, tiles are q,k,v, RoPE base and norm order follow the model (Fixes #1889) #1976 and Merge main into #1976: keep both new test functions in npu_pack_import_test.cpp (Refs #1889) #2130 are superseded.q_normwhile q/k norm is on. Applied to every layer, it refuses packs that load on main:qwen3_5hybrid (layer 0 is a linear-attention layer, 18 of Qwen3.5-2B's 24 have noself_attn.q_norm): found by the reviewer atadb81c4, after I had exempted conv layers only.The rule is now
q4nx_qk_gain_missing(qk_norm, layer_has_softmax_attention, gain_present), called with!is_conv_layer(l) && !gdn_l, wheregdn_lis the test the step bodies use to send a layer togdn_layer().tests/batch_check_state_ci.pyfabricates a container at test time; it now also writes an LFM2 hybrid (batch_check_state_lfm2, full comparison) and aqwen3_5hybrid (batch_check_state_qwen35_load, load only). The suite was green with each of the two refusals above in it.Q4NX_CHUNK_Q4S, soHy-MT2-1.8B-NPU2loads. Its proof is the tie test against the pack's own bf16 embedding (mean 0.244, max 1.000 of a step; the eleven other readings are 3 to 8 steps off) and main's decoder returning -1 on the pack's real tiles; details in npu: the 4,608-byte symmetric chunk is read, so Hy-MT2-1.8B-NPU2 serves on the forward route #2203's body.The refusal, before and after, on real containers
On the device, at
a7bbf94(the code of this head;676a8f6merges main on top)Binary checked before each ticket (
nm build/1bit | grep -c q4nx_dequant_tile_q4s= 1).One thing seen and not explained: in the first ticket at this code the Qwen3.5 row ran, and then the Hy-MT2 and LFM2.5 servers started in the same ticket exited after their first ELF loaded, with nothing in their logs. Each came up normally in the next two tickets with the same binary (the rows above are from those). It looks like the device rather than this change, but I have not shown that.
Tests
q4nx_qk_norm_test: the four cases of the rule.batch_check_state_ci.py:lfm2ALL PASS (batched = sequential, rel-L1 0);qwen3_5PASS (load only); the original qwen3 arm unchanged.npu_dequant_bf16(8q4schecks),npu_design_grid,q4nx_layer_kinds,lfm2_shortconv,npu_rope_llama3,npu_pack_import,npu_moe_container,gdn_batch_order.ctest -j 6after a fresh configure: 239 of 241; the two arenpu_rt_shimandnpu_dmabuf_import, which do not link in my build directory.git merge-treeof main + Merge main into #1976: keep both new test functions in npu_pack_import_test.cpp (Refs #1889) #2130 conflicts only inq4nx_semantics.c(the reviewer checked the same), and noadd_testname on main is missing here.Limits, stated
qwen3_5row is load only, and not by choice. On that fabricated model the full comparison fails its last check: the batched last chunk's logits against the sequential reference, rel-L1 0.71 with the same argmax (0.45 with two GDN layers and no attention layer, 0 with attention layers only). FastFlowLM's real Qwen3.5-2B passes the same binary with rel-L1 0, andNPU_GDN_CHECKreports both of the GDN path's internal comparisons identical on the fabricated model. Whether the fabrication or the batched path is at fault is not established; the row claims the load and says so in the script's header. This is a finding inside npu-pack: use_qk_norm is never consulted, so Hunyuan-7B imports with its q/k norm omitted and answers gibberish #1889/npu: every FastFlowLM/*-NPU2 model source imports and serves on --device npu (FLM 1.0.12 model sources, 59 repos) #1786's scope that I have not resolved.add_testrows sit in the block CI configures (ONEBIT_NPU_HOST AND NOT ONEBIT_NPU). My build is the other configuration, so I ran the script by hand; CI will be the first to run the registrations.🤖 Generated with Claude Code