diff --git a/npu/pack_import.cpp b/npu/pack_import.cpp index d1e844bc7..01bcc0e67 100644 --- a/npu/pack_import.cpp +++ b/npu/pack_import.cpp @@ -498,6 +498,11 @@ const uint8_t* at(const Fconst& f, int64_t off, int64_t len) { // field, the readout and the note all have to agree on it: three copies of 0.99 is how they drifted apart before. constexpr double kTieCorr = 0.99; +// engine#1503: tied AND not mis-scaled. Correlation alone passes a head that is a scaled copy of the embedding -- two +// packs on this box report corr 0.9915/0.9961 with rel_err 0.095/0.136 and serve garbage, while the pack that serves +// correctly reports rel_err 0.000000. One predicate, so the four sites that set selfcheck_tied cannot drift apart. + + // Every entry was measured by diffing the 1.9 pack against the 1.8 pack of the SAME model: the bundle's tile shape and // its rotation are not derivable from the pack, and neither is `prefix`. It was tempting to read the extra bytes as // Qwen3's per-head QK norm gains, but Llama-3.2-1B -- which has no QK norms at all -- carries the same +4,352 at hidden @@ -5011,6 +5016,8 @@ Report import_pack(const std::string& pack_dir, const std::string& out_dir, cons // `tied` is false there while the head plainly reproduces its embedding, and the readout used to call those // packs untied while the container it wrote said tied. Same threshold the container's own decision uses. r.selfcheck_tied = r.lmhead_corr > kTieCorr; + + r.selfcheck_scaled = r.selfcheck_tied && !selfcheck_head_ok(r.lmhead_corr, r.lmhead_rel); const Q4 swap = decode_matmulnbits(bytes_of(wt, qw_need), scales.data(), zp, int(mm.N), int(mm.K), mm.block, /*swap_nibbles*/ true, false); const Q4 nozp = decode_matmulnbits(bytes_of(wt, qw_need), scales.data(), nullptr, int(mm.N), int(mm.K), @@ -5098,6 +5105,8 @@ Report import_pack(const std::string& pack_dir, const std::string& out_dir, cons } r.selfcheck_reference = true; r.selfcheck_tied = c32 > kTieCorr; + + r.selfcheck_scaled = r.selfcheck_tied && !selfcheck_head_ok(r.lmhead_corr, r.lmhead_rel); double sxy = 0, sx = 0, sy = 0, sxx = 0, syy = 0; const int nsh = std::min(2048, int(V) - 1); for (int rr = 0; rr < nsh; ++rr) @@ -5311,6 +5320,8 @@ Report import_pack(const std::string& pack_dir, const std::string& out_dir, cons r.lmhead_corr = corr(q, embed_tbl, q.N); r.selfcheck_reference = true; r.selfcheck_tied = r.lmhead_corr > kTieCorr; + + r.selfcheck_scaled = r.selfcheck_tied && !selfcheck_head_ok(r.lmhead_corr, r.lmhead_rel); double sxy = 0, sx = 0, sy = 0, sxx = 0, syy = 0; const int nsh = std::min(2048, q.N - 1); for (int rr = 0; rr < nsh; ++rr) @@ -5423,6 +5434,8 @@ Report import_pack(const std::string& pack_dir, const std::string& out_dir, cons r.lmhead_corr = corr(q, embed_tbl, q.N); r.selfcheck_reference = true; r.selfcheck_tied = r.lmhead_corr > kTieCorr; + + r.selfcheck_scaled = r.selfcheck_tied && !selfcheck_head_ok(r.lmhead_corr, r.lmhead_rel); // The control this path can actually run: the same tiles and the same decoder, with each // head row matched against the NEXT embedding row. If a misaligned head still correlated, // the number above would be measuring the vocabulary's mean brightness rather than the @@ -6020,6 +6033,20 @@ int run(int argc, char** argv) { } const Report r = import_pack(dir, out, Options{allow, layout}); + // engine#1503: a head that correlates but is mis-scaled imports cleanly and answers nonsense on the device (two + // packs measured: rel_err 0.095 and 0.136 against 0.000000 on the pack that serves). `--allow-incomplete` is not an + // override here: that flag means "write the tensors I could not represent", and this is "the tensors I wrote do not + // decode", which no amount of writing fixes. + if (r.selfcheck_scaled) { + std::fprintf(stderr, + "1bit npu-pack: refusing to import %s: the head correlates with the pack's own embedding (%.6f) but " + "its relative error is %.6f, which is a scaled or partially decoded head rather than a rounding " + "difference (the threshold is %.3f). A pack in this state imports cleanly and answers garbage on " + "the device; --allow-incomplete does not override this. The partial output this run already\n" + "wrote under %s is not servable -- delete it before retrying.\n", + dir.c_str(), r.lmhead_corr, r.lmhead_rel, kSelfcheckRelMax, out.c_str()); + return 3; + } if (!r.refused.empty() && !allow) { std::fprintf(stderr, "1bit npu-pack: refusing to import %s: %zu of the pack's tensors cannot be " @@ -6273,5 +6300,9 @@ void sweep_fconst_scratch_dirs(const std::vector& dirs, const std:: } } +// engine#1503: the tied-head rule, defined here so it is the namespace-scope function the header declares (a file-local +// copy is ambiguous with it at every call site). Correlated AND not a scaled copy of the embedding -- the two states this +// box measures are rel_err 0.000000 (serves) and 0.095/0.136 (imports cleanly, answers garbage). +bool selfcheck_head_ok(double corr, double rel) { return corr > kTieCorr && rel <= kSelfcheckRelMax; } } // namespace onebit::npu::pack_import diff --git a/npu/pack_import.h b/npu/pack_import.h index 513025dbf..48a47a6bc 100644 --- a/npu/pack_import.h +++ b/npu/pack_import.h @@ -442,6 +442,11 @@ struct Report { // check; an untied pack's head and embedding are separate matrices by construction, and the same number is // meaningless -- reported as such rather than left looking like a failed import. bool selfcheck_tied = false; + // engine#1503: tied, correlated -- but MIS-SCALED. Correlation alone passes a head that is a scaled copy of the + // embedding: two packs measured on this box report corr 0.9915/0.9961 with rel_err 0.095/0.136 and serve garbage + // ("myself" forever), while the pack that serves correctly reports rel_err 0.000000. A pack in this state imports + // cleanly and answers nonsense, so it is refused rather than reported. + bool selfcheck_scaled = false; double lmhead_corr = 0, lmhead_rel = 0, ctrl_corr_nibble = 0, ctrl_corr_nozp = 0; // The tail (.fconst) path has no MatMulNBits bytes to mis-decode, so its control is an ALIGNMENT one: the same // tiles, the same decoder, rows matched against the neighbouring embedding row. A correct decode correlates ~1 @@ -772,6 +777,12 @@ bool fconst_scratch_ready(const std::string& dir); const Fconst19Rule* select_fconst19_rule_from(const Fconst19Rule* rules, int count, int hidden, int heads, int layers, std::string* why); +// engine#1503: the tied-head rule, as ONE predicate so the four call sites cannot drift: the head must reproduce the +// embedding (corr) AND not be a scaled copy of it (rel_err). The constant is the measured gap between the two states -- +// 0.000000 for the pack that serves, 0.095/0.136 for the two that do not -- so 0.02 sits far from both. +inline constexpr double kSelfcheckRelMax = 0.02; +bool selfcheck_head_ok(double corr, double rel); + int64_t attn_tile_bytes(int64_t k); // Pearson correlation between a quantised head and a bf16 embedding table (engine#1690; the self-check's own statistic, // exported so a test can pin the buffer width it decodes). diff --git a/tests/npu_pack_import_test.cpp b/tests/npu_pack_import_test.cpp index 57e3e8862..6adfb4230 100644 --- a/tests/npu_pack_import_test.cpp +++ b/tests/npu_pack_import_test.cpp @@ -5665,7 +5665,23 @@ void test_header_witness_refusal() { std::printf(" header witness: 8 arms (one-header vote vs declaration refuses; 3/1-without-declaration/0 allowed)\n"); } +// engine#1503: the tied-head rule as ONE predicate -- correlated AND not mis-scaled. Two packs on this box correlate +// (0.9915 / 0.9961) while being scaled copies of their own embedding (rel_err 0.1361 / 0.0949): they import cleanly and +// serve garbage, where the pack that serves correctly reports rel_err 0.000000. Correlation alone cannot tell them apart, +// which is why the correlation-only check passed both and cost two device tickets. +static void selfcheck_head_rule_arms() { + check(pi::selfcheck_head_ok(0.996901, 0.000000), "head rule: the pack that serves correctly passes (corr .9969, rel 0)"); + check(!pi::selfcheck_head_ok(0.991539, 0.136140), "head rule: a correlated but scaled head is refused (corr .9915, rel .1361)"); + check(!pi::selfcheck_head_ok(0.996053, 0.094874), "head rule: the second measured pack is refused too (corr .9961, rel .0949)"); + check(pi::selfcheck_head_ok(0.996053, 0.000000), + "head rule: correlated and unscaled passes -- the rule is not 'refuse everything'"); + check(!pi::selfcheck_head_ok(0.50, 0.000000), "head rule: an uncorrelated head is still refused on correlation"); + check(pi::selfcheck_head_ok(0.996053, pi::kSelfcheckRelMax), "head rule: exactly at the threshold passes (inclusive bound)"); + std::printf(" head rule: 6 arms (two measured bad packs, the serving one, the unscaled control, no-corr, boundary)\n"); +} + int main(int argc, char** argv) { + selfcheck_head_rule_arms(); // engine#1907: every crafted 1.9 import writes a canonical copy into the scratch cache. Without this the suite left 0-byte // copies in the user's real ~/.cache/1bit/fconst19 and in /tmp on every run. Give the run its own directory, removed at exit. const fs::path run_scratch = ::onebit::test::tmp_root() / ("1bit-pack-test-scratch-" + std::to_string(::getpid()));