diff --git a/npu/pack_import.cpp b/npu/pack_import.cpp index 29b1decb6..81ba399dd 100644 --- a/npu/pack_import.cpp +++ b/npu/pack_import.cpp @@ -2508,18 +2508,6 @@ std::string Plan::manifest() const { return j.dump(2); } -// engine#1889: where the per-head QK gains sit when the attention section carries them as the 640 B between the last -// qkv tile and o_proj. Split out and exposed because this is the arithmetic a wrong answer silently mis-rotates every -// Hunyuan pack with: a right offset reads a gain (~1.0-2.0), a wrong one reads tile bytes as bf16 and the gains come out -// wild, and the test drives both shapes without a pack. -QkGainOffsets qk_gain_offsets(int64_t gap_start, int64_t head_dim, int64_t gap_bytes) { - const int64_t per_head = head_dim * 2; - if (gap_bytes != 2 * (kNormOff + per_head)) return QkGainOffsets{}; - QkGainOffsets o; - o.k_norm = gap_start + kNormOff; // block 0 is k, as the separate-section family - o.q_norm = gap_start + (kNormOff + per_head) + kNormOff; // block 1 is q - return o; -} std::string qk_norm_refusal(bool declared, bool found) { if (declared && !found) @@ -2848,10 +2836,15 @@ Plan plan(const std::string& pack_dir) { // use_qk_norm, gains not located") into an import; the values are the check (a wrong offset // reads tile bytes as bf16 and the gains come out wild, a right one gives the 1.0-2.0 a norm // gain has). - const QkGainOffsets qkg = - qk_gain_offsets(attn_off + p.attn_tiles * tile_bytes, int64_t(d.head_dim), rem); - p.knorm_off = qkg.k_norm; - p.qnorm_off = qkg.q_norm; + // engine#1934: NOT claimed. The pack's own per-op metadata names this gap as ONE tensor -- + // `attn.q_norm.layernorm.weight.qk_rms_packed_26_0`, size 640, shape [320] -- and #1928's + // two-window split read the SAME bytes twice (the structure repeats with a period of 320 B), + // so it wrote two copies of one vector as `q_norm`/`k_norm`. Neither is the pack's gain, and + // a value check cannot tell: both windows are plausible gains, which is exactly the trap + // #1889 warned about. Until the `qk_rms_packed` packing is known the honest state is to read + // nothing here; a pack that DECLARES the gains then meets #1895's refusal by name instead of + // importing two tensors that are not them. + p.qk_packed_gap_bytes = rem; static bool said2 = false; if (!said2) { said2 = true; diff --git a/npu/pack_import.h b/npu/pack_import.h index bb50c25ab..58b5bc4d7 100644 --- a/npu/pack_import.h +++ b/npu/pack_import.h @@ -86,10 +86,6 @@ struct Dims { // REFUSAL (importing without the gains is the silent-garbage path), found-and-not-declared is allowed only with the // measurement behind it (Qwen3 packs ship the sections without the key), and neither is the ordinary case. Exposed // so the three can be pinned without a 4 GB pack. -struct QkGainOffsets { - int64_t k_norm = 0, q_norm = 0; // 0,0 = the gap is not the gains' shape -}; -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); // A hybrid pack's embedding table lives in its own graph (`embedding.onnx` + `.data`, gathered with @@ -138,6 +134,10 @@ struct Plan { // none. Identified and verified byte-exact against the public Qwen3-1.7B/4B checkpoints (0 of 128 // values differ on either pack, and the swap differs on 127-128 of 128). int64_t knorm_off = 0, qnorm_off = 0; + // engine#1934: the bytes between the last qkv tile and o_proj when they are the per-head gains in the PACKED form + // (`…q_norm.layernorm.weight.qk_rms_packed_26_0`, one bf16 tensor of 320 values). Recorded, not claimed: the two-window + // split #1928 used read one window twice, and a value check cannot tell the difference. + int64_t qk_packed_gap_bytes = 0; int64_t mlp_off = 0; // the MLP section starts just past the block's post-attention norm // What the pack's *graph* carries, reported so `npu-pack info` can answer a pack's genre without a // third-party parser: the rai_1.3/1.6/1.7.1 line ships its 4-bit weights as ordinary ONNX initializers, while diff --git a/tests/npu_pack_import_test.cpp b/tests/npu_pack_import_test.cpp index b40a08fa1..469f1cf0b 100644 --- a/tests/npu_pack_import_test.cpp +++ b/tests/npu_pack_import_test.cpp @@ -3856,27 +3856,8 @@ void test_qk_norm_refusal() { std::printf(" qk norm: 7 arms (declared+not-found refuses; found-only and neither are allowed)\n"); } -// engine#1889: the arithmetic that claims the per-head QK gains out of the 640 B Hunyuan-7B carries between its last qkv -// tile and o_proj. A wrong offset there reads tile bytes as bf16 and mis-rotates every pack of the family, so both the -// right shape (2 * (64 + head_dim*2) = 640 at head_dim 128) and the neighbours that must NOT be taken for it are driven. -void test_qk_gain_offsets() { - const pi::QkGainOffsets o = pi::qk_gain_offsets(1000, 128, 640); - check(o.k_norm == 1064, "qk gains: block 0 starts one kNormOff (64 B) after the gap"); - check(o.q_norm == 1384, "qk gains: block 1 starts after block 0 (64 B header + a 256 B bf16 [128] gain)"); - check(o.q_norm - o.k_norm == 320, "qk gains: the two blocks are 320 B apart, the same stride as the separate family"); - const pi::QkGainOffsets n1 = pi::qk_gain_offsets(1000, 128, 639); - const pi::QkGainOffsets n2 = pi::qk_gain_offsets(1000, 128, 641); - const pi::QkGainOffsets n3 = pi::qk_gain_offsets(1000, 128, 512); // just the two gains, no headers - const pi::QkGainOffsets n4 = pi::qk_gain_offsets(1000, 256, 640); // the right byte count at the wrong head_dim - check(n1.k_norm == 0 && n1.q_norm == 0, "qk gains: a gap one byte short is not the gains' shape"); - check(n2.k_norm == 0 && n2.q_norm == 0, "qk gains: a gap one byte long is not either"); - check(n3.k_norm == 0 && n3.q_norm == 0, "qk gains: two bare gains with no headers are not this shape"); - check(n4.k_norm == 0 && n4.q_norm == 0, "qk gains: the byte count alone is not enough, head_dim has to agree"); -} - int main(int argc, char** argv) { test_qk_norm_refusal(); - test_qk_gain_offsets(); test_tail_head_placement(); const std::string golden_dir = argc > 1 ? argv[1] : ""; const std::string pack = argc > 2 ? argv[2] : "";