Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions npu/pack_import.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -6273,5 +6300,9 @@ void sweep_fconst_scratch_dirs(const std::vector<std::string>& 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
11 changes: 11 additions & 0 deletions npu/pack_import.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
16 changes: 16 additions & 0 deletions tests/npu_pack_import_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
Loading