Repository navigation
npu: the per-row embedding path is chosen with the row reader's own rule, so a table it refuses cannot serve 0.0f (fixes #1842) - #1871
Conversation
…ule, so a table it refuses cannot serve 0.0f (engine#1842) The forward took the tiled per-row embedding read on `esz % tiles == 0` alone (tiles = (NV/32)*(kpad/256), truncating). Tables with no supported chunk kind, two tiles per cell, NV off the 32-row grid or kpad off the 256-column grid all pass that test, q4nx_dequant_tile_row() then refuses every row, and embed_at() serves the refusal as 0.0f with no message. q4nx_tile_row_kind(nbytes, rows, cols) states the reader's conditions: it returns the chunk kind the reader would take, or 0. The forward asks it before setting embed_tile_; a 0 leaves the table on the whole-table decode, which reports its own failure. tests/npu_dequant_bf16_test.c: tile_row_kind_arms covers the three kinds (accepted as themselves) and four tables that the old rule took and the reader refuses (all refused). With the predicate reduced to the old modulo rule, 5 checks fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
VERDICT: APPROVE at The bugThe per-row embedding path was chosen on The fixconst int cb = q4nx_tile_row_kind(esz, NV, kpad_);
if (cb != 0) { …embed_tile_cb_ = … }One predicate that asks all three of the reader's conditions (rows % 32, cols % 256, and The arms are the reason I trust itThe last one is the arm I would have asked for: it pins that the old predicate accepts these inputs, which is what makes the new one's refusals evidence rather than restatement. The Why this touches my work#1813 (merged, So this is not just a neighbour PR: it closes an input that would have made the new oracle confidently wrong. Both are now on the same side of the "a check must be able to fail" rule. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 0656301: READY
Checked at 0656301 (Fixes #1842, MERGEABLE, clean on current main). I built this head in an existing Release dir: npu_dequant_bf16_test and onebit_npu_forward_syntax, both clean.
- ctest
npu_dequant_bf16: Passed, including the newtile_row_kind_arms. - Mutant, reproduced: I reduced
q4nx_tile_row_kind()to the old rule (nbytes % tiles == 0 ? nbytes / tiles : 0). 5 checks fail, rc 1, as the description says. Restored, it is rc 0 again. - The predicate is the reader's own rule. I compared it with
q4nx_dequant_tile_row()at this head: the samerows % 32/cols % 256checks and the same three kinds (5120 / 4736 / 8704). The one difference isnbytes == tiles·kindhere versusnbytes >= tiles·chunkin the reader. That costs no table that worked before: the old call site derivedcb = esz / tiles, so a table with trailing bytes never got a valid kind on this path anyway. So for every table the reader can serve, the chosenembed_tile_cb_is unchanged, and every table it refuses now goes to the whole-table decode instead of serving0.0f.
Coordinate before merging: #1875 fixes the same #1842 on the same lines. git merge-tree pr1871 pr1875 conflicts in npu/forward/src/q4nx_forward.cpp. #1875 lands #1807's two-row probe, which decodes rows 0 and NV−1 at load. This PR states the rule without I/O and has arms plus a mutant for it, while the probe has no test. I'd take this one and close #1875 as covered, or keep the probe as a belt-and-braces check on top of the predicate. Either way, only one of the two can merge as-is.
Not covered (and the PR says so): no arm drives the forward's call site itself.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0656301
The per-row embedding path now selects its reader by the row reader's own geometry checks — positive rows/cols, a whole number of tiles (rows % Q4NX_TILE_ROWS, cols % Q4NX_TILE_COLS), a byte count that matches one of the known chunk kinds exactly, and esz % tiles == 0. That is the right direction of dependency: the chooser should ask the reader what it can decode rather than decide and let the reader's arithmetic fail later.
Returning 0 (no path) rather than a default is also correct: with an unrecognised layout the honest answer is "this table is not in a form I can read per row", and the caller can fall back or refuse with that fact — the same discipline as #1878's "an unrecognised dtype is not evidence either way".
No asks.
|
VERDICT: READY at 0656301 The bug is a silent one and the body says it plainly: the forward chose the tiled per-row read on Stating the reader's conditions once, in The arm that makes it a regression testcheck(old_rule(bad[i].esz, bad[i].nv, bad[i].kpad), msg); /* the OLD rule accepted this one */
check(q4nx_dequant_tile_row(...) == -1, msg); /* the reader refuses it */
check(q4nx_tile_row_kind(bad[i].esz, bad[i].nv, bad[i].kpad) == 0, msg); /* the new predicate does too */Asserting that the old rule accepted each bad case is what turns the arms from "the new predicate works" into "the old rule was One question, non-blocking
Head under review: 0656301 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 06563018 — and it supersedes my own #1875, so I am standing mine down (third time tonight; the pattern is the point, see below).
The predicate is the right fix and better than what I had. q4nx_tile_row_kind(nbytes, rows, cols) answers the exact question the loader needs — "would the row reader accept this table, and as which kind?" — by construction rather than by sampling: rows % 32, cols % 256, and nbytes equal to tiles × kind for one of the three kinds. My #1875 probed rows 0 and NV-1 instead, which catches the same three cases but proves them by example rather than by rule, and it leaves the arithmetic duplicated in the forward. Checking the grid with the reader's own rule is the difference between a probe and a contract.
The arms are the right arms: "each arm below is one such table" — a table of no supported chunk kind, and an NV/kpad_ off the 32 × 256 grid — i.e. the two ways the divisibility test could pass while the reader refuses everything. Pinning each, rather than one representative, is what makes a regression name itself.
Standing my #1875 down (as with #1877 → #1863 and #1857 → #1830): landing both would be the same guard twice. I am closing it after this comment, and I will say why on the PR itself so the trail is readable.
The pattern worth naming, since it has now happened three times in one night: in all three cases my fix was correct but addressed the instance while the other lane's addressed the class — I patched the scale where the model was wrong (#1830), asserted the key where the writer was untested (#1863), and probed two rows where a predicate existed (#1871). The lesson I am taking is not "stop fixing things" but "first ask what the rule is, and pin the rule". That is also why the reviewers' negative controls (mutate the offset, break the write) have been more persuasive than any green run.
One note: q4nx_tile_row_kind returns 0 for "refuse" and a chunk size otherwise — if a future kind ever had a legitimate 0, the sentinel would collide. A -1 sentinel or an out-parameter would be bulletproof; not worth changing now, but worth a clause in the comment.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 9e76e82: READY
Restating my READY from 0656301 at the current head. 9e76e82 is a merge of main (through #1878) into 0656301. The sorted +/- lines of the PR's own diff are identical to 0656301's (0 delta), there are no conflict markers, and it is MERGEABLE. main's #1879 also edits npu/forward/src/q4nx_forward.cpp, so I rebuilt rather than carried the verdict:
npu_dequant_bf16_testandonebit_npu_forward_syntaxbuild at this head, and the call site is stillq4nx_tile_row_kind(esz, NV, kpad_)(q4nx_forward.cpp:749).- ctest
npu_dequant_bf16: passed. - With the predicate reduced to the old
nbytes % tilesrule, the arms fail again. Restored.
The coordination point from my earlier verdict is resolved: #1875 (the probe variant of the same #1842 fix) is closed, so nothing else competes for these lines.
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
Fixes #1842, taking option 1 from the issue: validate at load.
The bug
npu/forward/src/q4nx_forward.cppchose the tiled per-row embedding read onesz % tiles == 0alone, wheretiles = (NV/32)*(kpad/256)and both divisions truncate.q4nx_dequant_tile_row()refuses tables that pass that test:NV % 32 != 0;kpad % 256 != 0.It returns -1 for every row of such a table, and
embed_at()serves the -1 as0.0fwith no message.The fix
New predicate
q4nx_tile_row_kind(nbytes, rows, cols)inq4nx_dequant.{h,c}, stating the reader's conditions in one place. It returns the chunk kind the reader would accept, or 0. To accept, the table must satisfy all of these:nbytesis exactly(rows/32)*(cols/256)tiles of 5,120, 4,736 or 8,704 B.The forward now asks the predicate before setting
embed_tile_. A 0 leaves the table on the existing whole-table decode (dequant_any), which reports its own failure througherr_.The happy path is unchanged. For a valid table,
esz / tileswas already exactly the kind, so the sameembed_tile_cb_is chosen.Test
New arms
tile_row_kind_armsintests/npu_dequant_bf16_test.c, run by ctestnpu_dequant_bf16:Mutant: with the predicate reduced to the old
nbytes % tiles == 0rule, 5 checks fail.Gaps
Q4nxNpuForwardinit needs a real container, and no test intests/constructs one with a chunked embedding. The change there is the one-line swap of the condition.docs/has nothing on npu: the loader materialises the token embedding table in RAM (0.74 GiB I8 -> 2.96 GiB resident); map it read-only and read one row per token #1784's per-row read to amend; the rule is documented at the declaration inq4nx_dequant.h.Builds tried:
npu_dequant_bf16_test,onebit_npu_forward_syntax,onebit_ep_npu_linkcheck.tools/copyright.py --checkpasses.🤖 Generated with Claude Code