Repository navigation
npu plan: the generator writes the design set, and a parity test holds it to the engine's derivation (A2a2 of #803) - #1333
Conversation
…s it to the engine's derivation (A2a2 of #803) A2a taught the plan to carry a per-role kernel set; a section is only useful if something writes it, and only safe if it names what the engine derives. Both halves here, and neither is assumable: tools/npu_plan.py: designs_from_config() ports the four dense-family rules from q4nx_derive_designs() (QKV with the attn_output_gate doubling on N only, O at the undoubled q width, GU or G+U, D) onto the same pad_k/pad_n grids and the same artifact names, and the plan carries them when -- and only when -- the tool can state the WHOLE set. Families it does not model yet (MLA, a GDN layer mix, Gemma4's second attention type / per-layer embeddings / double-wide MLP) emit NO section: the check is both-ways, so a partial set would be refused at load, and absent means the engine keeps deriving. Emitting nothing is the honest answer there, not a smaller section. tests/npu_derive_designs_dump + tests/npu_plan_designs_parity_test.py: the engine's own q4nx_derive_designs() printed as 'ROLE M K N elf' lines, and a matrix of six dense configs (the 0.6B's dims, a padded hidden 896/1024, q|gate doubling, split gate/up, a derived head_dim, a _b8 family) where the generator's set must equal the engine's exactly -- plus five families that must emit no section. The parity test earned its place immediately. It caught two real disagreements in my first port: O's K is the UNDOUBLED q width (q4nx_derive_designs keeps q_rows = NH*HD for O and doubles only QKV's N), and the _b8 suffix the forward applies to every artifact name is not part of the derivation. It also forced the name suffix and attn_output_gate into the dump instrument, because a dump that cannot represent those would 'agree' with a wrong generator. Also moved A2a's check in the forward to just before the context-creation loop: the pb16i family appends its split designs and the b8 family rewrites every name between the derivation and there, so that is the first point at which designs_[] is what a target would actually open.
|
#1333 ready for review at One line each on why it is worth reading: the generator ports four geometry rules from It paid for itself immediately, and both catches are in the body: O's K is the undoubled q width (my first port doubled it under End-to-end in one ticket: the generated plan loads with the same four init lines and no refusal, and perturbing one artifact in the generated plan is refused by name ( |
|
Preparing review... |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 6d1cf0a6cde894dd5001afe6043879ac9ecb3cb5. The generator is held to the engine by a parity test that bites on the exact bug it was written to catch, and the check now runs on the set a target would actually open. (Reviewed as its own delta over #1330's head 6c5b369f: one commit, 5 files, +305/−26. Merge #1330 first, as the body says.)
Run here (Release, host, under thermal-run.sh)
tests/npu_plan_designs_parity_test.py tools/npu_plan.py build/npu_derive_designs_dump(the ctest invocation) →npu_plan_designs_parity: ok.npu_plan_test→npu_plan: ok.npu_design_grid_test→all cases ok.copyright.py --checkrc 0.- Mutation, the body's own first bug: with
Obuilt frompad_k(qkv_rows)(the gate-doubled width) instead ofpad_k(q_rows)indesigns_from_config(), parity failsFAIL attn_output_gate: generator set == engine set, rc 1. Restored, it passes. So the doubled-O case is pinned by the test, not by a hand check. - The moved check:
q4nx_derive_designs()atq4nx_forward.cpp:940, the pb16i split designs appended at:966-972, the_b8rename at:1076-1085,designs_agreeat:1113, and the firstadopt_geometry/make_ctxat:1124/:1149. That is the first point wheredesigns_[]names what the target opens, so a b8 plan is compared against suffixed names (the false refusal the body describes, at #1330's position, is gone).
Design choice, endorsed: emitting no section for the families the port doesn't model (MLA, the GDN mix, Gemma4's second attention, PLE and double-wide MLP) is right. The engine's check runs both ways, so a partial section would refuse at load, while absent means "derive".
Note for A2b, not this PR: the section and its check cover the M=128 set. The M=16 decode twins (designs16_, built later at make_ctx16) are neither written nor checked. When A2b has the target open the plan's set, the twins need either their own rows or an explicit "derived from the M=128 set" rule.
Not re-run: the one-ticket end-to-end (generated plan accepted, perturbed artifact refused).
744ba21
into
1bit/npu-p4-a2-plan-designs
…efuses to disagree with it (A2a of #803) (#1330) * npu plan: the plan can name the per-role kernel set, and the engine refuses to disagree with it (A2a of #803) A2 (the setup side of P4) needs `plan.json` to carry the kernel set the target opens, and it did not: the plan had model dims, layout, fused_gu, tag, prefill/decode M, tile order, attention mode, the KV contract and per-layer roles, but nothing that says which ELF a role runs. That is still derived in the forward (q4nx_derive_designs() plus the filename probes) -- the thing A2 exists to delete. So: an optional `designs` section (role, M, K, N, elf), parsed with the rest of the plan's no-defaults rule -- an empty role/elf and a non-positive M/K/N are refusals, the latter named with the index because a plan can carry a dozen. Absent means the engine keeps deriving, which is what every plan written before this field means. `role` is the UNIQUE design name (q4nx_design_role_name(): QKV, QKVLIN, QA, GB, ...), not the forward's short log tag. The two differ exactly where a model carries roles that read alike: a qwen3_5 pack derives an attention QKV and a GDN QKVLIN and the log prints both as "QKV". A plan field that the target will open by name cannot be ambiguous, so the unique name lives beside the geometry it belongs to (q4nx_pack.c, next to q4nx_derive_designs) and the log keeps its tag. And the half that makes it safe: Plan::designs_agree(). When the section is present the forward compares it against what the container derives -- same roles, same M/K/N, same artifact, BOTH directions -- and refuses by naming the role and both sides. A plan that names a different ELF than the weights need is the silent divergence the plan layer exists to remove, and this is the point where it can be caught: before a single launch. The comparison is a multiset of (role, M, K, N, elf) and not a role-keyed lookup, so a duplicated or mispaired entry is a refusal rather than a match. No device for the tests: tests/npu_plan_test.cpp carries the parse cases, the agreement cases (same set, reordered set, two entries sharing a tag with different geometries, wrong artifact, wrong geometry, a derived role the plan omits) and the refusals. The generator emitting the section is A2a2, so nothing in the tree writes it yet. * npu plan: the generator writes the design set, and a parity test holds it to the engine's derivation (A2a2 of #803) (#1333) A2a taught the plan to carry a per-role kernel set; a section is only useful if something writes it, and only safe if it names what the engine derives. Both halves here, and neither is assumable: tools/npu_plan.py: designs_from_config() ports the four dense-family rules from q4nx_derive_designs() (QKV with the attn_output_gate doubling on N only, O at the undoubled q width, GU or G+U, D) onto the same pad_k/pad_n grids and the same artifact names, and the plan carries them when -- and only when -- the tool can state the WHOLE set. Families it does not model yet (MLA, a GDN layer mix, Gemma4's second attention type / per-layer embeddings / double-wide MLP) emit NO section: the check is both-ways, so a partial set would be refused at load, and absent means the engine keeps deriving. Emitting nothing is the honest answer there, not a smaller section. tests/npu_derive_designs_dump + tests/npu_plan_designs_parity_test.py: the engine's own q4nx_derive_designs() printed as 'ROLE M K N elf' lines, and a matrix of six dense configs (the 0.6B's dims, a padded hidden 896/1024, q|gate doubling, split gate/up, a derived head_dim, a _b8 family) where the generator's set must equal the engine's exactly -- plus five families that must emit no section. The parity test earned its place immediately. It caught two real disagreements in my first port: O's K is the UNDOUBLED q width (q4nx_derive_designs keeps q_rows = NH*HD for O and doubles only QKV's N), and the _b8 suffix the forward applies to every artifact name is not part of the derivation. It also forced the name suffix and attn_output_gate into the dump instrument, because a dump that cannot represent those would 'agree' with a wrong generator. Also moved A2a's check in the forward to just before the context-creation loop: the pb16i family appends its split designs and the b8 family rewrites every name between the derivation and there, so that is the first point at which designs_[] is what a target would actually open. Co-authored-by: bong-water-water-bong <bong-water-water-bong@users.noreply.github.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@users.noreply.github.com>
…er rules that now gate it (#1454) The page described A2 as entirely ahead of it. Two of its items are merged (#1330/#1333 for the plan naming the design set, #1380 for the target opening the M=128 ELF set), and two of the owner's later rules are in the tree as well, so a reader of the split document was being told to wait for things that exist and not told about the ones that now block a plan. * A2 now says, item by item, what is done and what is still in the forward (the M=16 decode family and the lm-head context, which need only the configure(Setup) state that is already there), and that adopt_geometry/make_ctx stay on the surface until those move; * a new section 4b records the two rules the owner added: every op a plan may name ships a host reference with known answers (op_registry + ctest, with the gdn_step/attn_step gap and WHY it is a gap -- their signatures carry none of the layer's parameters or state), and completeness refused at load by name (Plan::check_artifacts, tools/npu_plan.py --check, same rule in both), including the boundary it deliberately leaves to the container. Docs only; no code change. Co-authored-by: bong-water-water-bong <bong-water-water-bong@users.noreply.github.com>
…w stops saying the derivation is all there (#1529) * docs/npu-plan: the schema table learns about designs[], and the P4 row stops saying the derivation is all there is Three statements on this page were true when written and are not now, all of them about fields the loader reads: * the schema table had no row for designs[] -- the per-role kernel set the plan may name (#1330, #1333), with its both-directions agreement check, its bare-file-name artifact, the generator writing it only where it can be complete, and Plan::check_artifacts() refusing a missing artifact at load (and npu_plan.py --check asking the same question at import time); * the layers[] row said the forward 'derives designs from the container', which is now only the no-section case; * the 'Not yet' P4 entry stopped at A2b part 1 (#1380). A2b is complete: the M=128 ELF set in open_prefill (#1380) and the lm-head context in open_head (#1458, #1478), so q4nx_forward.cpp creates no I8Ctx at all. What is genuinely still open is the residency step -- load_block is a named no-op, the cache fills lazily inside gemv -- and that is an optimisation whose evidence is a tokens/s row, not a structural move. Docs only; no code change. * docs/npu-plan: the M=16 decode family is still the forward's, so A2 has two items left, not one Review at cfa7829 is right and this is the correction: the paragraph said the setup side is the target's and that q4nx_forward.cpp creates no I8Ctx at all. The prefill ELF set (#1380) and the lm-head context (#1458, #1478) did move - the forward calls open_head() at :1357 and then asks head_ctx() for it - but the M=16 decode family is still built here: the target allocates the memory (XdnaTarget::make_ctx16) while the forward decides which decode contexts exist and configures each one field by field (MD, KD, ND, blocked_b, pb16i, t2s, planes) at q4nx_forward.cpp:1306-1330, and docs/npu-p4-target-split.md section 3 says exactly that ('still in the forward, and therefore still owed: the M=16 decode family'). So A2 has two items left: A2c moves that decode-family setup behind the target, and load_block stays a named no-op whose explicit block step is an optimisation with a tokens/s row as its evidence, not a structural move. The paragraph now says that, cites the lines, and no longer claims the forward creates no I8Ctx. Docs only; copyright --check rc 0. --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@users.noreply.github.com> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
What
A2a2 of #803, stacked on the plan-designs section in #1330. A2a taught
plan.jsonto carry a per-role kernel set; a section is only useful if something writes it, and only safe if it names exactly what the engine derives. Both halves here, and neither is assumable.The generator
tools/npu_plan.pygainsdesigns_from_config(), which ports the four dense-family rules fromq4nx_derive_designs()onto the samepad_k/pad_ngrids and the same artifact names:QKVpad_k(H)pad_n(q_rows·(gate?2:1) + 2·NKV·HD)Opad_k(q_rows)— undoubledpad_n(H)GU(orG+U)pad_k(H)pad_n(2·IM)(orpad_n(IM)each)Dpad_k(IM)pad_n(H)and the plan carries the section only when the tool can state the whole set. Families it does not model yet — MLA, a GDN layer mix, Gemma4's second attention type / per-layer embeddings / double-wide MLP — emit no section: the engine's check is both-ways, so a partial set would be refused at load, and absent means the engine keeps deriving. Emitting nothing is the honest answer there, not a smaller section.
The parity instrument
tests/npu_derive_designs_dumpprints the engine's ownq4nx_derive_designs()asROLE M K N elflines, andtests/npu_plan_designs_parity_test.py(ctestnpu_plan_designs_parity) requires the two sets to be identical across a matrix of six dense configs — the 0.6B's own dims, a padded hidden (896 stored at K 1024, the case engine#845 exists for), q|gate doubling, split gate/up, a derivedhead_dim, and a_b8family — plus five families that must emit no section.It earned its place before it was even committed
The parity test caught two real disagreements in my first port:
q4nx_derive_designskeepsq_rows = NH·HDforOand doubles onlyQKV's N underattn_output_gate; I had doubled both. My hand-checked 0.6B case passed, and theattn_output_gatecase caught it._b8suffix is not part of the derivation — the forward rewrites every artifact name afterwards — so the dump instrument had to learn bothattn_output_gateand the tag, or it would have "agreed" with a wrong generator.It also moved A2a's check in the forward to just before the context-creation loop: the pb16i family appends its split designs and the b8 family rewrites every name between the derivation and there, so that is the first point at which
designs_[]is what a target would actually open. (At the old position, a b8 model's plan would have been compared against unsuffixed names — a false refusal waiting to happen.)Measured (one
npu-holdticket,amdxdna0 → 0)The generated plan, end to end through the forward, on the 0.6B container:
Rows
npu_plan_designs_parity(new)npu_plan,npu_design_gridRelease,ONEBIT_NPU=ON,ONEBIT_JINJA=ON,ONEBIT_NPU_DEBUG_DUMPS=OFF,thermal-run.shnpu_forward_loader_testviaadd_dependencies, becauseci.ymlnames its build targets explicitly and this lane has noworkflowscope (the same mechanismhost_gemm_pad_testuses)Stacked on #1330 (base
1bit/npu-p4-a2-plan-designs): merge #1330 → this one. A2b (the target opening the set) and A2c (load_block()owning residency) are still to come.Refs #803 (A2a2 of three). Part of #799.