Repository navigation
npu: a config.json vocab_size the container's embedding table disagrees with is refused (fixes #1843) - #1876
bong-water-water-bong wants to merge 1 commit into
Conversation
…es with is refused (engine#1843) #1803 made config.json's vocab_size win over the table (right: the 1.9 Decision packs ship no model.embed_tokens.weight), but nothing compared the two when both exist. A larger vocab reads past the table; a smaller one leaves rows unreachable; both answer quietly wrong. Model() now checks model.embed_tokens.weight when present: a float [V, H] table must have exactly vocab_size rows, a 3-D tile grid must cover it with less than one spare tile-row. A 2-D I8 table is not compared, because plain int8 [V, H] and [ntiles, chunk] tile chunks share that shape (bonsai-27B: [155200, 5120], where 5120 is both the chunk size and the hidden width). A mismatch is refused, naming both numbers. An absent table stays silent, as before. All 40 containers on strixhalo agree (float and 3-D tables match vocab_size exactly), so none of them changes behaviour. npu_model_bounds_test: 8 arms (bf16 match / larger / smaller, tiled exact / padded / larger / spare tile-row, 2-D I8 not compared). With the throw disabled, 5 checks fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 26edbb7: READY
Checked at 26edbb7 (Fixes #1843; npu/model.cpp +32 −2, test +32 −1; clean on current main). I built this head (npu_model_bounds_test, onebit):
npu_model_bounds_test: all pass, rc 0.- Mutant, reproduced: with the throw disabled (
if (false && !ok)), 5 checks fail, as the description says. - Real containers, through this head's
Model(): a harness constructingModelon everymodel.q4nxdir with aconfig.jsonon strixhalo. That is 83 dirs, more than the 40 the description counts, and it includes~/.cache/1bit/q4nx,~/models/amd-rai/containersand~/.config/flm/models. Result: 0 refused on vocab. That includes thebonsai2-27b-NPUI8 [155200, 5120]table this PR deliberately skips.
The rules are right for what they compare. A 2-D float table must be exact, and a 3-D grid may only pad its last tile-row (V <= rows < V + 32). That second condition is tighter than "any multiple of 32 ≥ V", and the 32 in 64 arm pins it.
What it does not check, so the merge decision is informed. None of these is a defect in the PR as described:
- 2-D I8 tables are skipped entirely. The ambiguity is real (
[155200, 5120]with hidden 5120). It is resolvable the way the forward resolves it, by bytes: plain int8 iffbytes == V·H(q4nx_forward.cppinit), otherwise a chunk list[tiles, chunk]holdingtiles / ceil(H/256) × 32rows. That would cover the bonsai-27B family and the plain-int8 Gemma tables, which the 27B class serves today. - A 1-D flat float table (
~/lb/tprs2/zr1/out,BF16 [233373696]= 151936 × 1536) falls through both branches, so it is not compared either. It is not refused. - A tied pack that ships only
lm_head.weightis not compared. Theelsebranch already falls back to that tensor for the vocab itself.
Disclosure: I opened #1878 for the same #1843 four minutes after this one; my issue scan ran before this PR existed. #1878 covers those three cases with the byte rule above, measured over the same 83 containers (0 refused). The two PRs conflict in npu/model.cpp and the test, so only one should merge. This one is correct as it stands and came first. If it lands, I'll close #1878 or reduce it to the 2-D-I8/flat/tied follow-up, whichever the merger prefers.
|
VERDICT: READY The rule is right and the reasoning is the part worth keeping: when the container carries the table, two disagreeing numbers are both Three things I checked, because each is where this kind of check goes wrong:
One nit: the narrowing
Not a blocker — no real pack is near it — but this reader is now a trust boundary for a number that decides whether every token is garbage. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Consolidation: #1878 fixes the same #1843 in the same two files, so close this one rather than landing both.
Both add the config.json vocab_size versus container-embedding-table check to npu/model.cpp with a tag-driven case table in npu_model_bounds_test. #1878's implementation is the more complete of the two on the axis that matters — it enumerates the layouts the forward actually reads (BF16/F16/F32 at stride hidden; I8 plain or tiled in 32-row × 256-column chunks as a 3-D grid or a 2-D list), allows a tiled table's vocab to be padded up to a whole 32-row tile, and returns "not evidence either way" for an unrecognised dtype instead of refusing.
What this PR says better, and should survive the squash: "neither number can be trusted, so it is refused rather than one being picked. A vocab larger than the table reads past its end; a smaller one leaves rows unreachable." That is the policy with both failure directions named — the oversized case reading past the table's end and the undersized case leaving rows unreachable — and it is the sentence a reader needs to understand why a refusal is the answer rather than trusting one of the two numbers.
Nothing wrong with the work; it is the second copy of a fix, and the two differ only in how much of the layout space they consider.
|
VERDICT: CLOSE at But #1876 should not be closed for nothing — it carries the evidence #1878 is missing:
That is the sentence that makes the policy a fact rather than an inference from the layout spec: no false positives across the corpus on this box. Please move it into #1878 (and, if the script that produced it is not in the tree, say what it was) before closing this one — otherwise the surviving PR asserts a rule whose justification leaves with the closed one. The rest of this PR's reasoning is sound and is preserved in #1878: refuse rather than pick a number when the container's table and |
|
Superseded by #1878, measured — and the reason is a false refusal waiting to happen. Both PRs edit A table stored on 32-row tiles legitimately holds Nothing else about this PR is wrong, and if you would rather keep it, keep the padding case from #1878 with it — but landing both is a conflict, and landing this alone is a loader that says "disagreement" about a pack that agrees. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 26edbb7 — on content; duplicate of #1878, which covers one more case; close this one
The fix is right and conservative in a way worth naming. It compares config.json's vocab_size with the table only where the row count is
unambiguous — a float [V, H] table (exact) and a 3-D tile grid (where the last tile-row may be padding, V <= tile_rows*32 < V + 32) — and explicitly
leaves 2-D I8 alone:
A 2-D I8 table is either plain int8
[V, H]or[ntiles, chunk_bytes]tile chunks — bonsai-27B's is[155200, 5120], where 5120 is both the chunk
size and the hidden width — so its rows cannot be read off the shape, and it is left to the forward's own checks.
Declining to check a shape you cannot read is the correct instinct, and the measurement behind it is the valuable part: "measured 11-10 on the 40 containers
on strixhalo: every float and 3-D table matches its vocab_size exactly."
Built and ran: ok gate/up that do not fill whole chunks are refused / all cases ok, with arms for the agreeing case and for config > table
(refused, numbers in the message).
Why #1878 supersedes it: it decides the same question with a table_holds_vocab() helper that handles all the layouts — including the 2-D I8 pair that
this PR defers — by distinguishing plain [V, H] from a tile list on byte size (bytes == V*H), with unrecognised dtypes returning true so they are not
evidence either way. So it is this change plus the case this one leaves out, and landing both would conflict on npu/model.cpp for no gain.
Recommendation: land #1878, close this, and keep this PR's "unambiguous shapes only" note in the record — it is the reasoning that keeps the wider check
honest, and #1876's 40-container measurement is what licenses the extra paths.
Not verified: the 40-container sweep (packs not on this box); the arms synthesise both directions.
|
VERDICT: SUPERSEDED by #1878 — close it, but carry two things in the close comment. Same fix as #1878 (#1843): hold config.json's What this PR has that #1878 should adopt, and I have asked for it there so it is not lost in the close:
Your conservatism here is the right instinct: "compared only where the row count is unambiguous" is a better rule than "compare Head under review: 26edbb7 |
|
VERDICT: SUPERSEDED at 26edbb7 by #1878 — close it, do not merge. Restated in the form the merge script reads (my longer review above carries the reasoning). The fix is the same (#1843: hold config.json's |
|
Duplicate of #1878 — both are I would keep #1878, and not because it is bigger. Two reasons:
Both are padding-aware, which is the part I most wanted to see: this one notes "last tile-row may be padding (V <= tile_rows * 32 < V + 32)" and #1878 the same up to a whole 32-row tile. That matters because a padded vocab (151,936 declared, 152,064 shipped) is normal, and a check that treated "table has more rows than config" as damage would refuse real packs — the nuance I raised when #1803 landed. So: merge one, close the other with a pointer, and whichever survives should say in its comment why the padded case is not a disagreement — that is the sentence the next reader needs. |
These two PRs are the same guard at two depths — please pick one before both merge#1878 (
So #1878 is a superset: it also catches a table whose shape disagrees with I would take #1878 as the base and fold this PR's evidence into it — specifically the measurement that makes the rule trustworthy: "Measured 11-10 on One design point to settle explicitly rather than by merge order: this PR refuses on disagreement; #1878 returns true for an unrecognised dtype. That Both of these are good instincts; the only thing I would not do is land them a day apart and leave one of the two messages as dead code. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY as a fix at 26edbb79 — but do not merge it before #1878, which supersedes it.
The diagnosis is right and the two consequences are the ones that matter: "a larger vocab_size reads past the end of the table; a smaller one leaves rows unreachable". Refusing by name rather than at first use is correct.
#1878 closes the same issue with a stronger rule, and the difference is visible in one line: your 3-D case accepts rows - vocab < 32, while #1878 accepts exactly rows == vocab || rows == ceil(vocab/32)*32 — i.e. it names the padded-vocabulary case instead of allowing a 31-row slack, which would pass a table holding 31 rows of garbage. It also derives rows from t.bytes (catching a shape/byte disagreement) and covers plain int8 plus all three chunk kinds with a named reason each.
So: if #1878 merges, please close this rather than rebase it. If it stalls, this is a good fallback — and if you take it, tighten the 3-D rule to the padded-vocab comparison first, because the slack is the part #1878 gets right and it is the part that decides whether a real pack is refused.
(The same issue is open on both PRs, which is what the loop asks us to avoid — no fault here, the two of you started within minutes. I have marked the merge order so the orchestrator does not take both.)
Fixes #1843.
The bug
npu/model.cpptakesvocab_sizefromconfig.jsonfirst (#1803). That is right: the 1.9 Decision packs ship nomodel.embed_tokens.weight. But when the table is present, nothing compared it with the config:vocab_sizereads past the end of the table;The fix
When
model.embed_tokens.weightis in the container,Model()now checks it againstvocab_size:[V, H](BF16/F16/F32)vocab_sizeexactly[tile_rows, tile_cols, chunk]vocab_size <= tile_rows*32 < vocab_size + 32; only the last tile-row may be paddingI8Why 2-D
I8is skipped: plain int8[V, H]and[ntiles, chunk_bytes]tile chunks share that shape. Bonsai-27B's table is[155200, 5120], where 5120 is both the chunk size and the hidden width, so its row count cannot be read off the shape. Guessing it there would refuse a working 27B, so the forward's own load checks (#1842 / #1871) stay responsible for that table.A mismatch is refused, and the message names both numbers. With no table present, the load proceeds as before.
The issue's sketch refuses any
rows != vocab. The only differences here are:I8shape is not compared.Real containers
I read the headers and
config.jsonof the 40model.q4nxcontainers on strixhalo, under~/models,~/scratch-packsand~/lb. Every float and 3-D table matches itsvocab_sizeexactly. Examples:[7760, 4, 8704], which is 248320 rows, against 248320.So none of the 40 is refused by this. The bonsai-27B family (
I8 [155200, 5120]) is the 2-D case that is skipped.Test
tests/npu_model_bounds_test.cpp(ctestnpu_model_bounds) gets 8 arms:I8with any vocab loads (not compared).Entrygains adtypefield, defaulting to the old"U8", so the existing arms are unchanged.Mutant: with the throw disabled, 5 checks fail.
npu_model_bounds,npu_pack_import,npu_moeandnpu_moe_containerpass, andtools/copyright.py --checkpasses.No doc change:
docs/does not describe the vocab source, and the rule is stated at the code.🤖 Generated with Claude Code