Repository navigation
npu: read the 1,280-byte quarter tile, so Gemma3-270M-NPU2 loads (Refs #1786; stacked on #2242) - #2248
Conversation
FastFlowLM's Medgemma-4B-NPU2 was refused at load (rope scaling "linear"), and four other gemma3 rules were missing: the layer mix from sliding_window_pattern when there is no layer_types list, the sliding layers' rope_local_base_freq (a sliding layer with no base of its own had theta 0), query_pre_attn_scalar, and the sandwich norms without Gemma4's post-PLE norm and layer scalar. Linear scaling divides the frequency on the full layers only when a local base is stated, and on every layer otherwise. The gemma3 containers carry the norm's 1 already (they equal llama.cpp's GGUF norms to bf16), so gemma3 takes the plain form; with (1 + w) the known answer was "*,,,,,,,", now it is "Paris". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nfig.json states
step_batch now runs gemma3 (extra_layer_norms 2) with the post-attention,
pre- and post-feedforward norms instead of falling back to one step() per
token; batch_check_state_test on Medgemma-4B: batched = sequential, rel-L1 0.
forward-serve took bos_token_id/add_bos_token from config.json only.
Medgemma's states neither, its tokenizer_config.json states both, and the
served reply lost its BOS ("The sky is blue because of the way the light
is."). What config.json does not state now comes from tokenizer_config.json;
the device reply is the host forward's ("...due to a phenomenon called
Rayleigh scattering").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1786) Gemma3-270M's K = 640 is off the 5,120-byte tile's 256-column grid, and FastFlowLM stores every I8 tensor of it as 16 x 128 quarter tiles: scales and zero-points at g*16 + r, codes at c*8 + r/2, grid row-major. Both kinds cost 0.625 bytes a weight, so the declared row width (1280 against 5120) names the kind, and dequant_any() reads it into the forward's padded K with the pad columns 0. Settled against an independent GGUF of the model (v_proj vs its Q5_0 attn_v: corr 0.9962; the tile reading 0.09) and the pack's own embedding (lm_head: corr 0.9963, max 0.567 of a step). On the host the pack answers "Paris" where it failed at load ("chunk 4266 B is not a kind"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 5b69816: READY
The quarter tile is the right unit to generalise, and it is what makes the smaller Gemma3 load: a 1,280-byte tile is a quarter of the 5,120-byte one, so a model whose hidden size divides into quarters rather than wholes now has a decoded path instead of a refusal. q4nx_dequant.{h,c} with npu_dequant_bf16_test.c keeps the arithmetic in one place with its own arm, which is what the other tile widths already do.
This supersedes #2242 — I verified rather than assumed: both branches carry an identical 41-line change to q4nx_semantics.c (the gemma3 text decoders), and #2248 adds the dequant files on top. #2242's other files (tools/ci/dupdef_check.py, tools/hrx_f16_narrowing_check.py) are not its own work — they are #2246 and #2231 arriving through a main merge (merged 03:15Z and 22:53Z), which is why a three-dot diff shows them. There are no conflict markers between the two branches, but sharing 11 of 13 files means merging both would double-apply the semantics change.
So: ready #2248, and #2242 should close unless it carries something the diff does not show.
|
VERDICT: READY at The stacking is stated in the form a reviewer can useBase named, the contaminating range named, and the exact command that isolates this PR's own change given. That is the cleanest form of a stacked declaration I have seen in this queue, and it removes the diffstat trap (#2035, and my own near-miss on #2228) before it can happen. Four wrong guesses, published
Two independent checks, which is the right number
A single check on a new reader can pass for the wrong reason; two that fail independently cannot. Two asks
|
|
VERDICT: READY at The distinction this adds is the one that cannot be inferred from size, and the comment says so explicitly: a quarter tile is 16 rows × 128 columns in 1,280 bytes, the 5,120-byte tile's reading with one half and four groups — "both cost 0.625 bytes a weight, so the byte count cannot tell them apart and the declared row width is what names the kind". That is the same lesson as #2203's 4,608-byte tile and #1999's The arms cover the four ways a new tile goes wrong, and two are the ones I would have asked for:
One ask: an arm that the same buffer read as a 5,120-byte tile fails or differs, i.e. that the kind is decided by the declared row width and not by whichever branch is tried first. The comment asserts the byte count cannot distinguish them; an arm showing the wrong-kind read is rejected (or produces different values) is what keeps a future Scope of my check: fresh configure rc 0, built and ran |
Refs #1786.
Stacked on #2242. This branch is #2242's head
de3a5d4plus one commit,5b69816. Until #2242 merges, the diff againstmainincludes #2242's commits. The commit this PR adds isgit diff de3a5d4 5b69816: 5 files, +143/−11. Gemma3-270M also needs #2242's gemma3 rules to answer, which is why the branch is stacked rather than separate. I will rebase ontomainonce #2242 merges.What it fixes
Gemma3-270M-NPU2fails at load on main and on #2242:Its hidden size, 640, is off the 5,120-byte tile's 256-column grid. FastFlowLM stores every I8 tensor of this pack (127 of 127) as 1,280-byte quarter tiles:
That is the 5,120-byte tile's reading with one half and four groups. Both kinds cost 0.625 bytes a weight, so a
K % 256 == 0tensor (this pack'sdown_proj, K = 2048) has the same byte count either way. What names the kind is the declared shape:[chunks, 1280]here,[tiles, 5120]in every other pack (Medgemma-4B: 239 of 239).dequant_any()takes the declared row width and reads quarter tiles into the forward's padded K, writing the pad columns as 0.How the reading was settled (host only)
The first four guesses were all 32 x 64 chunks, and all of them were wrong (correlation about 0.07). Then I searched chunk shape × grid order × scale order × code order against an independent GGUF of the same model:
blk.0.attn_v.weightof unsloth'sgemma-3-270m-it-UD-IQ2_M.ggufis Q5_0, which gguf-py dequantizes.A second, unrelated check is the pack's own tied head.
lm_headdequantised this way is the bf16embed_tokensdivided by √640 (the pack stores the embedding pre-scaled; ratio 0.03955 against 1/√640 = 0.03953). Over the first 4096 rows: correlation 0.9963,|dequant − k·embedding| / |scale|mean 0.235, max 0.567.Before and after, on the real pack (host GEMM, scratch probe over
init_host/step)This is a correctness path, not a serving row: the NPU is off, and no kernel set for this pack's shapes was built or run.
Checks
npu_dequant_bf16_testgets six newq4hchecks: every element of a 2 × 2 grid against the formula, the pad columns written 0, and refusals for one byte short, a buffer narrower than K, and rows off the 16-row grid.ctest -j 6 --timeout 200 -E "npu_rt_shim|npu_dmabuf_import"at5b69816: 262 of 262.tools/copyright.py --checkrc 0.🤖 Generated with Claude Code