From 3d2889aab2e721c09e15f247a96a1a610650feec Mon Sep 17 00:00:00 2001 From: t Date: Tue, 10 Nov 2026 14:32:38 -0400 Subject: [PATCH] npu-pack: refuse a one-header vote on a pack that declares per-head q/k gains, naming both witnesses (engine#1985) A regression, not a new bug. `Qwen3-1.7B` imported with **0 gaps** on yesterday's main and refuses with **280** on today's, on the same complete pack: yesterday attention_payload {o_proj_k: 2048, qk_norm: true, tiles_measured: 64} today attention_payload {o_proj_k: 0, qk_norm: false, tiles_measured: 0} first refusal: model.layers.0.self_attn.q_proj.weight: attention payload holds 64 tiles; q/k/v/o at 64 ... rows each need 96 `qk_norm: true -> false` is the tell: the pack has the two per-head q/k sections between q/k/v and o_proj, so its block is a 4-header one -- and it is now read as 2-header, which takes the 2-header arithmetic and refuses every attention projection. The cause is #1838's early return: `if (!g.hdr.empty()) return g;` trusts the epsilon-anchor vote's list, which for this pack recorded ONE in-block header. That guard was right for a vote that found the right list and wrong for a vote that found only one on a block that has three -- the #1831 defect coming back through the vote instead of through the residue step. Neither witness wins, so the reader refuses and names both: the epsilon-anchor vote recorded ONE in-block header, and this pack declares use_qk_norm, whose per-head q/k gains live in the two sections a 2-header block does not have (engine#1985). The two witnesses disagree and neither is being preferred `header_witness_refusal(vote_hdrs, declared_qk)` is the whole decision in one testable place, and the three cases it must NOT fire on are as important as the one it must: a three-header vote (a correct 4-header pack), a one-header vote on a pack that declares nothing (a genuine 2-header family), and **no vote at all** (0 -- that is the residue step's case, not a disagreement, and refusing there would take `Llama-3.2-1B` out). Arms: 8, host-only. Proved they can fail -- making the predicate always return "" right-fails 5 arms. Suite 191/191. (The first run after the rebase showed 3 "Not Run" -- `serve_degenerate`, `q4nx_cache`, `npu_answer_parser` -- targets an incremental build had not built; building them gives 100%. That is the fourth time tonight a suite result was about the build rather than the code, and the reason "1 failed" must always be read as "which target, and was it built".) This does not *fix* Qwen3-1.7B: it stops the silent misclassification and says both witnesses. The 4-header read itself is #1934/#1969's measurement (what the 640 B `qk_rms_packed` gap actually holds), and the Qwen3-4B/8B rows should be re-run against this head rather than trusted from before the window. Refs #1985, #1831, #1838, #1889. --- npu/pack_import.cpp | 19 ++++++++++++++++++- npu/pack_import.h | 13 +++++++++++++ tests/npu_pack_import_test.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/npu/pack_import.cpp b/npu/pack_import.cpp index ad46af92a..c0cfb3dce 100644 --- a/npu/pack_import.cpp +++ b/npu/pack_import.cpp @@ -1650,7 +1650,17 @@ FconstGeom locate_fconst_with_header(const std::string& tok, const std::string& // engine#1831: only when the vote did NOT record the block's headers. When it did, its list is the measurement // (`plan()` reads 2-header vs 4-header from `hdr.size()`, and `post_attn_hdr()` is `hdr.back()`), and rewriting it // to one entry turned every Qwen3 4-header 1.9 block into a 2-header one whose "post-attention" header was k_norm's. - if (!g.hdr.empty()) return g; + if (!g.hdr.empty()) { + /* engine#1985: the vote's list is a measurement, but it is not the only witness. A pack that declares per-head q/k + * gains cannot have a 2-header block, so a one-header vote on such a pack is the #1831 defect coming back through + * the vote instead of through the residue step -- measured: Qwen3-1.7B read as 2-header, qk_norm true -> false, 280 + * refused projections, where main imported it with 0 the day before. Refuse naming both rather than pick. */ + if (const std::string why = header_witness_refusal(int(g.hdr.size()), dims.use_qk_norm); !why.empty()) { + g.why = why; + g.stride = 0; + } + return g; + } const std::vector pos = eps_header_positions(tok, size_t(1) << 30, dims.eps_bits); // The scan window is the first GiB (about 8 blocks at h3584), so a real section boundary repeats ~8 times against // 1-2 for a payload coincidence; 3 separates them (requiring `layers/2` was too strict and dropped the real one). @@ -2779,6 +2789,13 @@ QkGainOffsets qk_gain_offsets(int64_t gap_start, int64_t head_dim, int64_t gap_b return o; } +std::string header_witness_refusal(int vote_hdrs, bool declared_qk) { + if (!declared_qk || vote_hdrs != 1) return std::string(); + return "the epsilon-anchor vote recorded ONE in-block header, and this pack declares use_qk_norm, whose per-head q/k " + "gains live in the two sections a 2-header block does not have (engine#1985). The two witnesses disagree and " + "neither is being preferred"; +} + std::string qk_norm_refusal(bool declared, bool found) { if (declared && !found) return "this pack declares use_qk_norm and its per-head q/k gains are not where the block layout says " diff --git a/npu/pack_import.h b/npu/pack_import.h index dcd0aa86a..cf875f969 100644 --- a/npu/pack_import.h +++ b/npu/pack_import.h @@ -96,6 +96,19 @@ struct QkGainOffsets { QkGainOffsets qk_gain_offsets(int64_t gap_start, int64_t head_dim, int64_t gap_bytes); std::string qk_norm_refusal(bool declared, bool found); +// Why a pack's two layout witnesses cannot be reconciled, or "" when they can (engine#1985). +// +// `vote_hdrs` is the number of in-block headers the epsilon-anchor vote recorded (0 when it could not win); `declared_qk` +// is config.json's `use_qk_norm`. A pack that states per-head q/k gains keeps them in the two sections between q/k/v and +// o_proj, which a 2-header block does not have -- so a ONE-header vote on such a pack is self-contradictory, and engine#1985 +// is what trusting it costs: `Qwen3-1.7B` read as a 2-header block, `qk_norm` flipped true -> false, and every attention +// projection was refused (280 gaps) where yesterday's main imported the same pack with 0. +// +// Neither witness wins: the vote measured where anchors repeat, and the declaration states what the block must contain. +// The reader refuses and names both, because picking either one silently is the defect. Exposed so the three cases can be +// pinned without a 2.6 GB pack. +std::string header_witness_refusal(int vote_hdrs, bool declared_qk); + // A hybrid pack's embedding table lives in its own graph (`embedding.onnx` + `.data`, gathered with // GatherBlockQuantized). Returns [V, H] f32 row-major, empty when the pack has no such component, with `why` naming the // reason. Exposed so the dequantisation can be tested against known codes and scales instead of only on a 3 GB pack. diff --git a/tests/npu_pack_import_test.cpp b/tests/npu_pack_import_test.cpp index 1a91f2cf9..3971eca9c 100644 --- a/tests/npu_pack_import_test.cpp +++ b/tests/npu_pack_import_test.cpp @@ -4267,7 +4267,31 @@ void test_qk_gain_offsets() { check(n4.k_norm == 0 && n4.q_norm == 0, "qk gains: the byte count alone is not enough, head_dim has to agree"); } +/* engine#1985: a REGRESSION, not a new bug -- `Qwen3-1.7B` imported with 0 gaps on yesterday's main and refused with 280 + * on today's, because #1838's early return trusted an epsilon-anchor vote that had recorded ONE in-block header on a block + * that has three. The tell in the report is `qk_norm: true -> false`: a pack that declares per-head q/k gains keeps them in + * the two sections a 2-header block does not have, so the two witnesses disagree and neither may be preferred silently. + * + * The arms below pin the refusal and, just as importantly, the three cases it must NOT fire on: a vote that found three + * headers (a correct 4-header pack), a genuine 2-header pack that declares nothing, and no vote at all (0), which is the + * residue step's case and not a disagreement. */ +void test_header_witness_refusal() { + const std::string a = pi::header_witness_refusal(1, true); + check(!a.empty(), "header witness: a one-header vote on a pack declaring use_qk_norm is refused"); + check(a.find("engine#1985") != std::string::npos, "header witness: the refusal carries its issue"); + check(a.find("use_qk_norm") != std::string::npos, "header witness: it names the declaration it contradicts"); + check(a.find("ONE in-block header") != std::string::npos, "header witness: and what the vote measured"); + check(a.find("neither is being preferred") != std::string::npos, + "header witness: and that no witness is being chosen, which is the point"); + check(pi::header_witness_refusal(3, true).empty(), "header witness: a three-header vote is consistent with the declaration"); + check(pi::header_witness_refusal(1, false).empty(), "header witness: a 2-header pack that declares nothing is fine"); + check(pi::header_witness_refusal(0, true).empty(), + "header witness: NO vote is not a disagreement (the residue step decides it)"); + std::printf(" header witness: 8 arms (one-header vote vs declaration refuses; 3/1-without-declaration/0 allowed)\n"); +} + int main(int argc, char** argv) { + test_header_witness_refusal(); test_encode_tile_code_range(); test_qk_norm_refusal(); test_qk_gain_offsets();