diff --git a/npu/pack_import.cpp b/npu/pack_import.cpp index 492a7d494..e9e81082a 100644 --- a/npu/pack_import.cpp +++ b/npu/pack_import.cpp @@ -637,6 +637,15 @@ const Fconst19Rule* select_rule_in(const Fconst19Rule* rules, int count, int hid // The header a 1.9 block carries: 00 00 7f ff once per head. Its length being the head count is what makes // it a test rather than a magic number. 0x00007fff is the big-endian reading of those four bytes; the bytes // are what is compared, because the little-endian reading of the same bytes is 0xff7f0000. +} // namespace (closed so prefer_residue_headers has external linkage: the arm drives it directly, engine#1986) +bool prefer_residue_headers(int64_t vote_h1, size_t vote_count, const std::vector& residue) { + if (vote_count == 0 || residue.empty()) return false; + if (residue.size() <= vote_count) return false; // the vote already has the longer list + return std::find(residue.begin(), residue.end(), vote_h1) != residue.end(); +} + +namespace { + std::vector fconst19_header(int heads) { std::vector h(size_t(heads) * 4, 0); for (int i = 0; i < heads; ++i) { @@ -1809,21 +1818,33 @@ 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. + // engine#1985: a vote that succeeded can still be SHORT, and the two witnesses answer different questions -- the vote + // settles the STRIDE (it needs a dominance test), the residue settles the COUNT (a real section boundary repeats in + // every block). So the residues are consulted first: when they contain the vote's offset and say there are more, they + // win (`prefer_residue_headers`, seven arms in the test). When they do NOT settle it, main's answer from engine#1995 + // stands rather than a guess: 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 refused naming both witnesses. + const std::vector pos = eps_header_positions(tok, size_t(1) << 30, dims.eps_bits); + const std::vector residue = fconst_block_headers(pos, stride8, 3); 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; + if (prefer_residue_headers(g.h1, g.hdr.size(), residue)) { + FconstGeom better = g; + if (fconst_set_block_headers(better, residue)) { + std::fprintf(stderr, + "note: the epsilon vote recorded %d in-block header(s) at %lld B stride, the anchor residues " + "repeat at %d; using the residue list (%d headers per block)\n", + int(g.hdr.size()), (long long)g.stride, int(residue.size()), better.period); + g = better; + } + } else if (const std::string why = header_witness_refusal(int(g.hdr.size()), dims.use_qk_norm); !why.empty()) { + g.why = why; // engine#1995's answer when the residues do not settle it 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). - const std::vector inner = fconst_block_headers(pos, stride8, 3); + const std::vector& inner = residue; if (!fconst_set_block_headers(g, inner)) { g.why = "the 1.9 header gives stride " + std::to_string(stride8) + ", but the epsilon anchors repeat at " + std::to_string(inner.size()) + " in-block offset(s), and a layer block has 1 (2-header) or 3 (4-header); " diff --git a/npu/pack_import.h b/npu/pack_import.h index dc62f27ff..71b6aba8a 100644 --- a/npu/pack_import.h +++ b/npu/pack_import.h @@ -702,6 +702,13 @@ std::vector fconst_block_headers(const std::vector& pos, int64 // engine#1831: set `hdr`/`h1`/`period` from that list the way locate_fconst() does -- one inner header is a 2-header block, // three is a 4-header block. Returns false, and changes nothing, for any other count. bool fconst_set_block_headers(FconstGeom& g, const std::vector& inner); +// engine#1985: which in-block header list to keep, given the epsilon vote's and the anchor residues'. The two witnesses answer +// DIFFERENT questions -- the vote settles the STRIDE (it needs a dominance test), the residue settles the COUNT (a real section +// boundary repeats in every block) -- so a vote that found only part of the list is not wrong, just short, and the longer +// consistent list wins. `residue` wins only when it CONTAINS the vote's first offset and reports more; an outright disagreement +// is not a licence to take the longer list, so the vote stands. Pure and exported so this decision table is pinned with no pack, +// no file and no configure (engine#1986 review). +bool prefer_residue_headers(int64_t vote_h1, size_t vote_count, const std::vector& residue); // One measured Ryzen AI 1.9 Token .fconst block shape (engine#1503). Every field is measured against the 1.8 pack of // the same model by tools/fconst19_measure.py; none of it is derivable from the pack, which is why a pack whose shape diff --git a/tests/npu_pack_import_test.cpp b/tests/npu_pack_import_test.cpp index 9087bb95e..054624114 100644 --- a/tests/npu_pack_import_test.cpp +++ b/tests/npu_pack_import_test.cpp @@ -5196,6 +5196,28 @@ void test_fconst_variable_stride(const std::string& root) { // engine#1740 (W1a): the tail-head placement arithmetic, pinned without a pack, a device or a fixture. The rows, the // tile size and the envelope are the hidden-896 pack's, so the numbers here are the real ones: 1,856 B envelope, // 2,374 tiles of 122,880 B, tail 1,856 + 2,374 x 122,880 = 291,653,952 B. +// engine#1986 review: the arbitration between the two witnesses, pinned as a decision table. Twenty-four lines of "which list +// wins" had no arm, and both directions matter: taking the residue list too eagerly is a wrong layout on a pack the vote got +// right, and never taking it is engine#1985 (Qwen3-1.7B refused with 64 tiles where the block has 96). +void test_prefer_residue_headers() { + using V = std::vector; + // the vote found ONE in-block offset at 640 B; the residues repeat at three -- engine#1985's shape + check(pi::prefer_residue_headers(640, 1, V{640, 1280, 1920}), "prefer residue: a short vote yields to the residue list"); + // ...and the same three offsets with the vote's one among them in a different position + check(pi::prefer_residue_headers(1920, 1, V{640, 1280, 1920}), "prefer residue: position in the list does not matter"); + // equal lists: nothing to arbitrate, and no churn in the note + check(!pi::prefer_residue_headers(640, 3, V{640, 1280, 1920}), "prefer residue: an equal-length list keeps the vote"); + // the vote found MORE than the residues: the vote wins, because the residues lost a boundary + check(!pi::prefer_residue_headers(640, 4, V{640, 1280, 1920}), "prefer residue: a longer vote keeps the vote"); + // OUTRIGHT DISAGREEMENT: the residue never mentions the vote's offset, so this is not "short", it is "different" + check(!pi::prefer_residue_headers(704, 1, V{640, 1280, 1920}), + "prefer residue: a residue list that does not contain the vote's offset keeps the vote"); + // degenerate inputs: an empty vote is handled by the caller's own branch, an empty residue never wins + check(!pi::prefer_residue_headers(640, 0, V{640, 1280, 1920}), "prefer residue: an empty vote never prefers the residue"); + check(!pi::prefer_residue_headers(640, 1, V{}), "prefer residue: an empty residue list never wins"); + std::printf(" prefer residue: 7 arms (short vote yields, equal/longer/disagreeing keep the vote, degenerate inputs)\n"); +} + void test_tail_head_placement() { // The real hidden-896 geometry: group_bytes(896, 64) = 8 x 68 x 64 = 34,816 B per head tile, V/64 = 2,374 tiles, // 1,856 B envelope, so the tail is 1,856 + 2,374 x 34,816 = 82,655,040 B -- which is the 0.5B pack's measured tail @@ -5446,6 +5468,7 @@ int main(int argc, char** argv) { test_q4_writer_code_range(); test_qk_norm_refusal(); test_qk_gain_offsets(); + test_prefer_residue_headers(); test_tail_head_placement(); const std::string golden_dir = argc > 1 ? argv[1] : ""; const std::string pack = argc > 2 ? argv[2] : "";