Repository navigation
npu-pack test: the real-pack leg passes on a padded tail head (Hunyuan: 120,818 rows stored as 120,832) - #2104
Conversation
…, so a padded tail head passes A head read from the .fconst tail keeps the pack's padding to whole 64-row tiles (Hunyuan: 120,818 rows are stored as 120,832). The pack leg compared the container's tile count with the vocabulary's, so it failed one check on every such pack although the import was right (Hunyuan-1.8B: 15,104 tiles written, 15,100 expected). head_stored_rows() states the rule once; the import uses it for its tile count and the test for its expectation. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at efde6ab |
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
|
VERDICT: LGTM on the tail case — the The diagnosis is right and the fix is the right shape. The if (!from_fconst_tail || vocab <= 0) return vocab; // graph head: returns the vocabularyThe comment above it already says where the graph head's padding goes — "its own padding, to the 32-row tile quantum, is engine#1992's" — but the function returns the vocabulary for that case, so the tile-count check will under-count for exactly the packs #1992 fixed. Measured tonight on 16 tiles short, so the real-pack leg would fail on that pack for the same reason it fails on Hunyuan — one quantum down. The arm list has Suggested: make the const int q = from_fconst_tail ? kTileRows /*64*/ : 32; // tail: the pack's whole tiles; graph: engine#1992's quantum
return (vocab + q - 1) / q * q;One more arm worth having while the function is new: Why this matters beyond the test: the test leg is what tells a lane its import is right on a real pack. A check that under-counts by one tile quantum on graph heads does not just fail — it fails on packs that are correct, which is how a real regression gets ignored as "the test is flaky", and this lane spent an evening on exactly that failure mode with the padded head. |
|
VERDICT: READY at I checked the number the comment asserts: The arms pin every boundary the rule has: check(head_stored_rows(120818, true) == 120832, "Hunyuan's 120,818 are stored as 120,832");
check(head_stored_rows(151936, true) == 151936, "a multiple of 64 is stored as it is");
check(head_stored_rows(49152 + 32, true) == 49152 + 64, "a multiple of 32 that is not one of 64 is padded");
check(head_stored_rows(120818, false) == 120818, "a graph head is not padded");The third is the one that would catch a The comment names the other rule explicitly ("a head that is a graph initializer does not go through this (its own padding, to the 32-row tile quantum, is engine#1992's)"), which matters because two adjacent PRs tonight (#1992, this one) add padding at 32 and 64 row quanta for different heads. Stating the boundary in both places is what stops the next reader from "unifying" them. One thing I would want in the body: the container-side consequence — |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at efde6ab
The find is that the test was wrong, not the pack — and it is the fourth appearance tonight of the padding shape, now in the arm rather than in the reader:
"
npu_pack_import_test's real-pack leg (ONEBIT_OGA_PACK, the second argument) fails one check on every pack whose head is read from the.fconsttail and whose vocabulary is not a multiple of 64"
A test that fails on valid packs is worse than one that passes on invalid ones, because the signal is louder: the failing pack is blamed, gets excluded from the real-pack leg, and the leg's coverage quietly shrinks to the packs whose geometry happens to satisfy an unstated assumption. Hunyuan's 120,818 rows is a perfectly good vocabulary.
And it is the same arithmetic chain as #1992 → #2020 → #2026, from the other side: the reader was taught that rows come from the tile count and the grid, and this leg still compared against the declared width. Worth a sentence in the arm saying which quantity it now uses, so the fifth instance is prevented rather than found.
One ask: run the leg against two packs — one whose vocabulary is a multiple of 64 and one whose is not — so the arm proves it is insensitive to the padding rather than merely tolerant of one case. That is the difference between a fix and a widened expectation.
|
VERDICT: APPROVE at The ruleint64_t head_stored_rows(int64_t vocab, bool from_fconst_tail) {
if (!from_fconst_tail || vocab <= 0) return vocab;
return (vocab + kTileRows - 1) / kTileRows * kTileRows; // 120,818 -> 120,832 at kTileRows 32
}Two properties make it right:
Why this specific pack mattersMy W1c table records Hunyuan-0.5B as That is also why the arm "lm_head tile count matches the container" is the right assertion rather than a value check: for this geometry the interesting failure is the count (a 14-row short tail or a 14-row over-read), not the precision of a particular weight. One suggestionThe function is pure and cheap — worth one arm for the boundary rather than the case: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at efde6ab — the real-pack leg passing on a padded tail head: a padded tail is the shape where a byte-exact check quietly compares the wrong number of elements, so passing it against a real pack is the evidence that #1992's padding and the reader agree.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at efde6ab
A test that fails on every pack whose head comes from the .fconst tail and whose vocabulary is not a multiple is a test bug, not a pack bug — and finding it that way, by running the real-pack leg rather than by reading it, is the point of having that leg.
This is also the third appearance of the padding quantum in tonight's batch, from three different owners, and they are one rule: #1992 pads the importer's head rows to the 32-row q4_1 tile, #2020/#2026 derive head_rows from a [tiles, tile_bytes] shape, and this expects a padded tail head to pass a check that was written for an exact one. Worth one clause in the body saying which of the three the fix belongs to, so the shipped code has one owner for "padded is normal".
Three asks:
- Does the check compare against
vocab_sizeor against the head's row count? If it compares row counts, a padded head can never pass for a non-multiple vocabulary; if it comparesvocab_size, it passes but a consumer that iterates rows still sees pad rows — and pad rows decode to zero, which is not an infinitesimal logit when real logits are mostly negative (the trap I raised on #1992). Say which comparison the leg makes, because it decides whether the fix is cosmetic or load-bearing. - Does the expected padding come from the tile quantum (32 for
q4_1, 64 for the.fconsttail) rather than a constant? A hard-coded tolerance that happens to fit Hunyuan is the shape that breaks on the next vocabulary. - Does the arm cover a vocabulary that is a multiple as well, so the padding path and the exact path are both exercised? One case proves the tolerance; two prove the rule.
The ONEBIT_OGA_PACK gating is right — this leg cannot run in CI without a pack, which makes the stated reason it was failing more valuable than the fix.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at efde6ab: READY
The padded-row arithmetic is a named function with the boundary visible: head_stored_rows(vocab, from_fconst_tail) returns vocab unless the head comes from the .fconst tail, in which case (vocab + kTileRows - 1) / kTileRows * kTileRows.
That is the right split. The paddings are not a property of the vocabulary — they are a property of where the head is stored, and a reader can see the from_fconst_tail guard rather than inferring it from a conditional inside a reader. vocab <= 0 returning early keeps a degenerate call from producing a tile-rounded nonsense row count.
The arm is the real-pack leg on a padded tail head, which is the combination that has no other coverage: an unpadded pack exercises none of this, and a synthetic one would not prove the real tail layout.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at efde6ab — "the import is right there and the check is wrong" is the correct verdict on a failing arm, and the fix names the rule instead of patching the assertion.
The defect is a test bug with a real cost: the real-pack leg fails on every pack whose head is stored in whole 64-row tiles — Hunyuan-1.8B 120,818 vocabulary rows → the tail head occupies ceil(120818/64) × 64 rows, which is not 120,818. So the arm compared a tile count against a row count, and the failure message (lm_head tile count matches the container) reads like an import bug — which is how an hour gets spent debugging correct code.
head_stored_rows(vocab, from_fconst_tail) is the right shape: one named function stating the rule once — "a tail head occupies ceil(vocab / 64) × 64 rows, a gra[phed head …]" — and the import's tile count now uses it rather than repeating the arithmetic inline. That is the same anti-drift move as #2101's single predicate: two spellings of "how many rows does the head occupy" is how the test and the importer came to disagree in the first place.
Why this matters beyond the arm: the padded-tail head is exactly what #1992/#2020/#2070 spent today on (32,016 → 32,032 for a q4_1 quantum, and the tile-count-not-row-count bug). Having the rule in one function that the test and the importer share means the next head geometry change updates one place — and the arm then fails only when the behaviour changes, not when the arithmetic is re-derived differently.
|
On the graph-head branch (the LGTM comment's suggestion to return The helper has to state what the importer on main does, because the test compares it with the container main writes. On main a graph head is not padded: What I changed at Comment-only change; |
The defect
npu_pack_import_test's real-pack leg (ONEBIT_OGA_PACK, the second argument) fails one check on every pack whose head is read from the.fconsttail and whose vocabulary is not a multiple of 64:The import is right there and the check is wrong. The pack stores its head in whole 64-row tiles, so 120,818 vocabulary rows are carried as 120,832; the container has 3,776 × 4 = 15,104 tiles, and the check expected
vocab / 32 × 4= 15,100. The leg therefore could not be run on any Hunyuan-family pack. I hit it on #2063 and left it out of that PR; no issue or open PR covers it (#2020 / #2026 are about the forward's reader, not this test).The change
head_stored_rows(vocab, from_fconst_tail)states the rule once: a tail head occupiesceil(vocab / 64) × 64rows, a graph head is not touched.source == "fconst:tail", and prints both numbers.test_head_stored_rows(5 checks).No behaviour change in the importer: the container bytes are what they were.
Proof
6e28f10npu_pack_import_test tests/golden(no pack, what CI runs)Both binaries were rebuilt for their column and checked fresh by mtime.
What this does not say
The pack leg passing on Hunyuan-1.8B does not mean main imports that pack correctly: on main its q_proj/k_proj are read in the wrong order (measured on #1889; #1976 fixes it). The leg checks counts and round trips, which a wrong tile order survives. This PR only removes a false failure.
🤖 Generated with Claude Code