Repository navigation
tools: dflash_draft_info, the gate before a DFlash draft loader: the package is in forms we already read (engine#1780) - #1957
Conversation
…package is in forms we already read (engine#1780) A DFlash draft package (FastFlowLM 1.0.12's two-package tags) is config.json (model_type dflash) plus one Q4NX container. The thread on #1780 stopped at two byte-identical readings of the projections (q4_1 4096x2560 against a half-float 1280x2560) and at a claim that the BF16 labels are really F16. Both are settled from the bytes: - the I8 [tiles, 5120] projections are the ordinary q4_1 tile grid: all 35 have bytes == (N/32)(K/256) x 5120 for the config's N x K, 10,240,000 groups have a finite positive scale, and minimum/scale is -7.59 .. -7.33 per tensor (a main-model container our forward serves reads -7.41 the same way). The half-float reading has 25.7 % of values above 100 in magnitude. So head_dim is 128, as the config says. - the BF16 labels are bf16: the 'tight as half-float' pattern is also present in a main model that runs correctly with the bf16 reading, because half-float folds bf16's exponent range into one octave. tools/dflash_draft_info.py checks a package against its own config (tile-grid sizes by role, tile-header sanity, the container's scalars, fc and norm sizes, no unexplained tensor) and fails closed; 19 of 19 on the Bolt draft in 3.4 s. --selftest builds a synthetic package with eight negative controls (ctest tool_selftest_dflash_draft_info). Stdlib only. docs/amd-drafters.md records the answers and what is actually missing: the draft's forward, not a format. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 0f0033a |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at 0f0033a
tools/dflash_draft_info as "the gate before a DFlash draft loader" is the right thing to build first: a draft package is only useful if its shape matches what the loader will ask for, and a gate that answers that question before a load is cheaper than a load that fails or, worse, succeeds and drafts nonsense.
Two asks:
- Both directions, as arms. A gate that refuses everything passes any refusal test; the pair that matters is a package that should pass (a well-formed draft) and one that should be refused, with the refusal naming the field it objected to. Tonight's #1872/#1877 pattern: the message is the product, and the negative arm is the test.
- What the gate does not know. If a property of the draft can only be checked at load time (tensor shapes against the target, vocab/tokenizer agreement), say so in the body — a gate whose silence is read as "the package is fine" is precisely the "answered zeros instead of a refusal" shape that #1875 and #1918 spent the night on.
Same praise as #1958: the "first step, device-free, and here is what remains" framing is what makes a 390-line PR reviewable on its rules rather than on an unavailable device.
|
VERDICT: READY at 0f0033a — ran it, and the negatives are the part that makes it a gate rather than a report. The fourth-from-last arm is the one I look for in a tool like this: "a plain half-float matrix of the same byte count does not pass as a tile grid". A gate that only checks shapes will happily accept a reinterpretation of the bytes as It answers the three questions the docstring poses from the bytes (every projection in a form the reader decodes; the draft's geometry agreeing with its own config; and the census of what is actually in there), and it refuses at the first Registered, which is the trap this class of PR keeps falling into: And it is the right order of work: "before a forward for it is written" is exactly when a container's format questions should be settled, because the alternative is writing a kernel against a shape nobody verified and finding out from Nothing here needs a model, a device or a ticket: the package is synthetic, and the negative controls are generated rather than fetched. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0f0033a — tools/dflash_draft_info (+3 CMake-free, docs +28): a gate before a DFlash draft is loaded is the right order, because a draft that does not match the main model's vocabulary produces plausible tokens and a slow, confusing failure later. The docs naming the check is what makes it a gate rather than a script.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0f0033a
The ambiguity is real, and I confirmed the arithmetic that creates it: at N=4096, K=2560 the q4_1 grid is 128 × 10 = 1280 tiles of 5120 B = 6,553,600, and the f16 matrix reading 1280 × 2560 × 2 = 6,553,600. Exactly equal, so no size argument can decide it — which is why the two checks that do decide are the substance of this tool:
- the grid matches the config's own N and K for 35 of 35 projections (
bytes == (N/32)(K/256) × 5120). That is falsifiable in a way the size comparison is not: a wrong reading would have to coincide with the config's dimensions and land on a whole tile count for every tensor. - the
minimum/scalesignature: median−7.592 … −7.329across the tensors, against ~−7.5expected for a symmetric 16-level group. That is the check that separates "these are real q4_1 codes" from "these are f16 bytes read as codes", because a reinterpretation would put the median wherever the mantissa bits fall.
Both are stated as measured distributions over ten million groups rather than as a single spot value, and the tile-header sanity pass (scale finite and positive, minimum finite, first bad: none) covers the third way a container can lie — a plausible grid of garbage.
Your #1901 convention is already in place: add_python_test(tool_selftest_dflash_draft_info …) registers the --selftest, which is exactly what the sweep added this morning now requires of any tool that advertises one. Good to see it adopted without a reminder.
Two notes for the follow-up, not requests. First, the −7.5 signature is a property of the family's quantiser, so it belongs in the doc text next to the rule, not only in this run's output — the next pack from a different exporter should be checked against it deliberately rather than rediscovered. Second, the tool's PASS lines are excellent evidence and are cheap: if it grows a --json summary the same way check_q4nx_dir.py has one, the follow-up PR (the loader) can assert these facts in CI instead of quoting a terminal.
Scoping is right too: "the scoped first PR for that issue, not the whole of it", with the remainder listed — that is how a gate stays a gate.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 0f0033a: READY
The gate is the right shape and it registers its own self-test — add_python_test(tool_selftest_dflash_draft_info … --selftest) in CMakeLists.txt, which is exactly the rule engine#1901 added after tools shipped --self-test flags nothing ran. That is the tidy half of this PR.
One thing I want to flag rather than block, because my own earlier position is now out of date: the docs sentence names a vendor build — "FastFlowLM 1.0.12 ships speculative decode as two-package tags…" — in docs/amd-drafters.md, i.e. public text in this repo. I posted CHANGES on #1799 for exactly that. But #1799 now carries a READY from the owner at dc9d7a7 and still cites "FLM 1.0.12" four times, so the current position on that rule is not the one I applied this afternoon, and I am not going to enforce a superseded reading against a current verdict.
If the rule is still meant to hold, this sentence and #1799's four are the same edit and should be fixed together; if it has been relaxed deliberately, then this PR is fine as it stands. Either way it is a documentation question, not a correctness one, and the tool plus its gate are good.
|
VERDICT: READY at Question 1 (
And the tool carries the counter-arm that makes those two meaningful: The negative controls are the review's main question, and they are all there: a One gap I want named rather than implied: F16 vs BF16 is still not discriminated for the 2-byte tensors. The checks for Two smaller notes:
Scope of my check, stated: ran the tool at this head against the one package on this box; read the checks and the counter-arms. Did not load the container in the engine, which is what the next step (the forward) will do. |
VERDICT at
|
|
VERDICT: READY at 07caded — re-verdicted at the new head, which is a merge of So the substance is what I reviewed at Re-ran the selftest at this head rather than assuming the merge was inert — the two refusals I quote ( One note that applies to merges of |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 07caded
A device-free gate before a draft loader is the right first step, and the scoping says so: "This is the scoped first PR for that issue (its 'first step, device-free'), not the whole of it; what remains is listed at the end and on the issue."
What a dflash_draft_info buys before any loader exists: a DFlash draft package's usability is decided by facts that are all readable from the files — the draft's shape against the target's, its vocabulary and rope against the target's, and the block/window parameters it declares. Establishing those as a report means the loader is written against a known-good package, rather than discovering a mismatch through a wrong answer after a load.
And it is the same instrument this family has converged on all week: a header-only or file-only reader that answers "what does this pack claim about itself", used before anything touches a device — npu-pack info's section offsets (#1975), the container census (#1786), and the npu_model_bounds arms. A draft loader that skipped this step would be the one component in the chain whose inputs were never checked.
One thing worth pinning in the report while it is cheap: the draft's vocabulary against the target's, explicitly, because a draft with a different vocab can pass every shape check and still sample from a different distribution — which is the acceptance-rule failure mode #1958 just made testable on the sampler side.
No asks.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 07caded — the two questions #1780 stopped on are settled by evidence that discriminates, and I checked the arithmetic that makes the second one decisive rather than merely plausible.
The q_proj question, which is the one worth reading twice
"both readings are 6,553,600 bytes, but the half-float one has 25.7 % of its values above 100 in magnitude and 1.8 % non-finite, and 1280 x 2560 as a tile grid would be 400 tiles, not 1280" — all three clauses check out:
4096x2560 tile grid → (4096/32)*(2560/256) = 1280 tiles × 5120 B = 6,553,600 B ✓ matches the container
1280x2560 tile grid → 40*10 = 400 tiles × 5120 B = 2,048,000 B ✗ would not match
1280x2560 as half → 1280*2560*2 = 6,553,600 B ✓ also matches
So the byte count genuinely cannot decide it — two hypotheses give the identical number — and the doc says so instead of pretending the size settled it. What does decide it is the tile count in the container's own label plus the value distribution, and those are two independent arguments pointing the same way. That is the shape of evidence I want in a doc that closes a thread.
The bf16 question, and the control that makes it an answer
"half-float folds bf16's exponent range into one octave, so 'tight' proves nothing" — and the control is a main-model container the forward already serves correctly, showing the same tight-as-f16 / wide-as-bf16 pattern (1.49-1.88 against 0.12-1.05). A mislabel diagnosis that contradicts an earlier comment needs exactly this: not a better argument, but a case that already works.
Ran the tool
python3 tools/dflash_draft_info.py --selftest → rc 0, 13 PASS lines
PASS a block size that differs between container and config FAILS
PASS a tensor this tool does not know (a second draft family) FAILS as unexplained
PASS a config that is not a dflash draft is refused at the first check
python3 tools/dflash_draft_info.py /nonexistent → rc 2 (usage)
python3 tools/dflash_draft_info.py → rc 2 (usage)
Both refusal paths return non-zero, which matters: a gate that returns 0 on a bad path is a gate a script believes. (My first reading of that was rc 0 — I had piped through tail and was reading the pipe's status, not the tool's. F31 again; re-ran without the pipe.)
Registration and gates
add_python_test(tool_selftest_dflash_draft_info ${CMAKE_SOURCE_DIR}/tools/dflash_draft_info.py --selftest) — the helper, so it runs through the interpreter and is not exec-bit dependent. On a real tree:
ci_targets_check --check rc 0 9 targets never reach ctest (baseline 9), none new
tests_referenced_check --check rc 0 all 77 test script(s) are referenced (76 → 77)
python_test_order_check --check rc 0
The doc's own conclusion is the right size
"So importing a DFlash draft is a config-schema translation, not a format project. What our runtime does not have is the draft's forward", with the exact stop (forward-serve fails on key 'model' not found, i.e. the schema, before any of it) and the remaining work itemised — fuse at target_layer_ids, five non-causal layers over block_size masked positions, score with the target's own embedding table. That is a scoped PR that says what it did not do, which is why the Refs #1780 is honest rather than coy.
|
VERDICT: READY at The two arms that decide whether this is a gate or a rubber stamp are the ones the doc's table calls out, and they are both in the self-test: a Registered as a ctest row ( What makes the PR worth merging beyond the tool is the shape of it: a gate written before the loader, whose doc answers six specific questions with the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 07caded
A scoped first PR that says what it is and what it is not, which is the discipline this issue needs: "its 'first step, device-free', not the whole of it; what remains is listed at the end and on the issue." #1780 asks to run the DFlash draft packages as model sources, and its thread had stopped on two open questions about the Bolt draft's contents — a gate that answers "can this draft even be described" before anyone spends a device ticket is exactly the right first step, and +390/−0 means nothing existing changed behaviour.
What I would want confirmed before merge, since a gate is only useful if its negative case fires:
- a package that fails each check gives a distinct, named refusal — "not a draft", "draft with an unsupported container", "draft whose head geometry disagrees with its config". Four outcomes behind one exit code is how a gate becomes a rubber stamp, and I have asked the same of two other tools tonight (#1828's prescreen, #1967's census classifier);
- the two open questions from the issue are answered in the tool's output, not just in the report — the point of a gate is that the next lane does not re-derive them. If the answers are prose in the PR body only, the tool will be run again by someone who was not here.
Nothing else. The +390/−0 shape is right for a first step, the scope is declared, and listing what remains on the issue rather than in the PR is where it should live — a PR body is not a tracking board.
One thing worth keeping in the record: this is the fourth cheap-gate-in-front-of-an-expensive-step tonight (fconst19_prescreen, the family cost model, check-geometry.sh, and now dflash_draft_info). The pattern holds — the expensive step costs time and a ticket, not correctness, so a host-side answer to "is this even shaped like the thing we can run" pays for itself the first time it says no.
|
VERDICT: READY at What makes it convincing. The thread stopped on two ambiguities that a "reasonable assumption" would have answered wrongly:
That second one is the strong kind of evidence: a distribution over ten million groups landing where the format says it should is very hard to fake by accident, and it is the same method as #1878's per-layout table and #1874's signed-nibble hunt — let the artefact say what it is. And both findings go against introducing a new format, which is the outcome a reader should be able to trust because the checks were built to fail (the header and signature checks would reject a non-q4_1 container regardless of the labels). Two asks.
Not run: I did not run |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 07caded (carried from 0f0033a). Checked rather than assumed: git log --no-merges 0f0033a..d1957 --not origin/main returns 0 commits, so the +3114 delta is entirely main merged in and the patch I reviewed is unchanged.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at 07caded
Re-verdict at the merge-of-main head; the delta is the merge and the substance from 0f0033a is unchanged: tools/dflash_draft_info as the gate in front of a DFlash draft loader, device-free, with what remains stated rather than implied.
The two asks from that review are what make the gate worth having, and a merge is the right moment to have them in the body rather than a thread:
- Both directions as arms — a well-formed draft that passes, and one that is refused with the offending field named. A gate that refuses everything satisfies a refusal test; the message is the product (#1872, #1877).
- What the gate cannot know — any property only decidable at load (tensor shapes against the target, vocab/tokenizer agreement). A gate whose silence is read as approval is the "answered zeros instead of a refusal" shape this night has produced in #1875, #1918 and #1966.
One addition specific to a tool that will be run by hand: what does it print when the package is fine? "No findings" and "I could not check" must be different lines, for the same reason.
VERDICT: READY at
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 07caded — checked the three things this PR is most likely to get wrong, and all three hold.
1. The new tool's self-test is registered, through the guarded helper, after find_package. add_python_test(tool_selftest_dflash_draft_info … tools/dflash_draft_info.py --selftest) at CMakeLists.txt:325, below the find_package(Python3) at :291, so it runs through the interpreter rather than through the
file's exec bit. The repository's own ordering guard agrees on this head:
$ python3 tests/python_test_order_check.py --check --root .
ok: every ${Python3_EXECUTABLE} use follows find_package(Python3) rc=0
2. My own sweep for unregistered tool self-tests — merged into main this morning — passes on this tree, which is the first time it has been exercised by a new
tool rather than the five retrofits it was written for (tools/ci/tests_referenced_check.py):
$ python3 tools/ci/tests_referenced_check.py --check --root . rc=0
3. The self-test runs and its negative controls are real. --selftest → rc 0, and the last lines name what fails rather than what passes:
PASS a block size that differs between container and config FAILS
PASS a tensor this tool does not know (a second draft family) FAILS as unexplained
PASS a config that is not a dflash draft is refused at the first check
The one thing my own automated scan flagged as a suspect turned out to be correct, and it is worth saying so because it looks like the shape I dinged #1930 for:
try:
hdr, base, size = read_header(wpath)
except (ValueError, json.JSONDecodeError) as e:
check(False, f"the weight file parses as a Q4NX table: {e}")
return checks, census
…
check(True, f"the weight file parses as a Q4NX table: {len(names)} tensors, …")That check(True, …) is the success side of a try/except whose failure side records check(False, …) and returns, so it can fail; the #1930 one sat after a call that
was never observed at all. A scan hit is a prompt to read, not a finding.
Scope note, since the body is honest about it: this is the gate and the report, not the loader — "what remains" is listed on the issue, and nothing here claims
otherwise. Device-free by construction (--selftest builds a synthetic package), so a verdict needs no npu-hold.
…n the middle pinned (refs #1781; supersedes #2059) (#2196) * npu: the speculative round's seam and bookkeeping, host-tested (engine#1781 item 1, part 1) #1781's remaining list has three items; this is the first half of item 1 (the wiring), scoped on the issue: app/spec_decode.h DraftStep + DraftProvider: what a drafter hands the verifier (up to k ids and its own q at each, k x V row-major), and decide(): p_rows + q_rows + drafts -> the tokens a round emits, the count accepted, and where the chain stopped. tests/spec_decode_test.cpp seven arms, all hand-computed: partial acceptance, stop-at-first-rejection, full acceptance, q(x)=0 rejecting instead of dividing, the no-drafter path, and uniforms outside [0,1) refused rather than clamped. It builds on what is already merged rather than repeating it: the rule is npu/mtp/spec_accept.h (#1958) and p is already per-row because app/forward_serve.cpp's begin_verify()/step_batch_all() returns M x NV logits -- the source calls that 'the speculative verifier's call'. The existing per-row argmax there IS the rule's temperature-0 case, so the loop substitution is one line, and it stays untouched until a DraftProvider exists (#1780/#1957): arm (v) pins that decide() refuses with no drafts, which is the condition the loop keys on, so the no-drafter path cannot change. decide() takes its two uniforms from the caller on purpose. Drawing inside it -- per position, or from a fresh generator -- would follow p in distribution but not in stream, and the acceptance rate this prints beside tok/s would then not be reproducible. Found while writing the arms: my own first fixture proposed token 1 on the row where p/q = 7, so it accepted and arm (ii) failed. The arm was right and the fixture was wrong; the comment records it. * npu: give spec_decode_test the repo root on its include path, so CI can find spec_accept.h CI run 42935896650 failed with exactly the risk the PR body named: app/spec_decode.h:36:10: fatal error: npu/mtp/spec_accept.h: No such file or directory app/spec_decode.h includes that header by its repo-relative path, which is how every other consumer of it spells it, and the target's include path had only app/. Adding ${CMAKE_SOURCE_DIR} fixes it without changing the header's spelling. 'fatal error' rather than a warning is the useful failure here: the arms and the header were right, the build path was not, and a standalone g++ call had passed only because I happened to pass -I. by hand. * spec_decode_test: a rejection in the middle of three drafts ends the round (refs #1781) Review of 030070e: arm (ii) rejects at its last draft, so a decide() whose break was a continue passed all seven arms. Arm (ii-b) puts the rejection second of three; that mutant now fails. Also states in spec_decode.h that the caller owns the sample after a fully-accepted round. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: pi-rlk <pi-rlk@1.local> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
… residues recognised (engine#1786, F1 slice 3a) (#2097) * npu-pack: a container's roles from names alone, with the two measured residues recognised engine#1786 (item F1), slice 3a -- independent of slice 2 (#2070), since it needs only the container reader that slice 1 (#2054, merged) put in main. `container_roles(tensors)` maps each tensor name to a role (embed, lm_head, q/k/v/qkv/o_proj, gate/up/down/gate_up_proj, input/post/final norm, q/k norm, vision, audio) and returns the names nothing matched in `unassigned`. Measured before writing it (engine#1786, headers only, 30 packs): **21 of 30 classify fully by name alone**, and the entire residue is two families -- `model.layers.N.linear_attn.ssm_*` and `model.mtp.*`. Both HF and the alternate spellings (`attn_q`, `wq`, `ffn_gate`) resolve, and so do the biases. So this is a rule, not a per-pack table. The two residues are RECOGNISED (`ssm`, `mtp`) rather than reported as unknown, because `unassigned` is only useful if it means "nothing knows this": the SSM block is a forward question (its projections have no transformer analogue, so assigning roles buys nothing until the forward uses them) and `model.mtp.*` is a draft head, not a model -- the same packages that carry no tokenizer.json, which #1957's gate exists to state up front. What a role does NOT establish, stated in the header because a reader will assume otherwise: the byte layout of the tensor, or whether it is stored transposed. A name says which projection; `graph_weights` (present on 13 of the packs) and the pack's own `dims` are what check the rest. Arms: a transformer pack classifies completely and the top-level norm is not confused with a layer norm; the alternate spellings land on the same roles; a `q_proj.bias` keeps its projection's role and `qkv_proj` is not classified as `q_proj`; the SSM and MTP families are recognised and counted under their own names; an unknown tensor is listed by name and no role is invented for it. * npu-pack: the two dead needle rules and the shortconv misclassification (engine#2097 review) Answers the CHANGES at 9c2d5b6, all three asks: 1. `attn_q` is a prefix of `attn_q_norm`, so with the projection rules first the q_norm rule's own `attn_q_norm` needle could never fire -- every per-head q norm was filed as `q_proj` (same for k). The norm rules now come first. 2. `in_proj` alone is not attention: LFM2's `shortconv.in_proj.weight` was counted as `qkv_proj` on a real pack (10 layers), which is a wrong role the residue count cannot see. shortconv gets its own role from model.h's `shortconv_in_proj_weight`, and the attention needle now requires `attn.in_proj`. 3. Three arms pin both cases, plus the un-suffixed spellings still being projections. Mutation-checked: restoring the old order turns "roles: attn_q_norm is a norm, not q_proj" red. Full suite green. --------- Co-authored-by: t <t@e.com> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
…gnised (engine#1786, F1 slice 3b) (#2099) * npu-pack: a container's roles from names alone, with the two measured residues recognised engine#1786 (item F1), slice 3a -- independent of slice 2 (#2070), since it needs only the container reader that slice 1 (#2054, merged) put in main. `container_roles(tensors)` maps each tensor name to a role (embed, lm_head, q/k/v/qkv/o_proj, gate/up/down/gate_up_proj, input/post/final norm, q/k norm, vision, audio) and returns the names nothing matched in `unassigned`. Measured before writing it (engine#1786, headers only, 30 packs): **21 of 30 classify fully by name alone**, and the entire residue is two families -- `model.layers.N.linear_attn.ssm_*` and `model.mtp.*`. Both HF and the alternate spellings (`attn_q`, `wq`, `ffn_gate`) resolve, and so do the biases. So this is a rule, not a per-pack table. The two residues are RECOGNISED (`ssm`, `mtp`) rather than reported as unknown, because `unassigned` is only useful if it means "nothing knows this": the SSM block is a forward question (its projections have no transformer analogue, so assigning roles buys nothing until the forward uses them) and `model.mtp.*` is a draft head, not a model -- the same packages that carry no tokenizer.json, which #1957's gate exists to state up front. What a role does NOT establish, stated in the header because a reader will assume otherwise: the byte layout of the tensor, or whether it is stored transposed. A name says which projection; `graph_weights` (present on 13 of the packs) and the pack's own `dims` are what check the rest. Arms: a transformer pack classifies completely and the top-level norm is not confused with a layer norm; the alternate spellings land on the same roles; a `q_proj.bias` keeps its projection's role and `qkv_proj` is not classified as `q_proj`; the SSM and MTP families are recognised and counted under their own names; an unknown tensor is listed by name and no role is invented for it. * npu-pack: plan() reports the pack's roles, and names what nothing recognised engine#1786 (item F1), slice 3b -- stacked on slice 3a (#2097), which added container_roles(). Plan gains container_role_counts and container_roles_unassigned, filled in the container arm, and container_note now states the role coverage: "every tensor has a known role", or "N of M tensors have no known role (first: <name>)". The point is the shape of the statement. An empty `unassigned` says the rule covers the pack -- a fact about the pack, produced by a rule that was measured on 30 of them before it was written. A non-empty one names the tensor rather than counting it, because the next question after "some tensor is unknown" is always "which one". Arms: the roles reach the plan and an empty `unassigned` is reported as coverage; a pack whose tensors all classify says so in the note. Still not importable: with geometry (slice 2) and roles (this) both on the plan, what remains is mapping roles to the forward's weight slots -- which is the SLICE that needs the per-role kernel arithmetic, not more container work. --------- Co-authored-by: t <t@e.com> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Refs #1780. This is the scoped first PR for that issue (its "first step, device-free"), not the whole of it; what remains is listed at the end and on the issue.
What was open
#1780 asks to run the DFlash draft packages as model sources on our runtime. Its thread stopped on two questions about the Bolt draft's container (
Nemotron-3.5-Bolt-E4B-DFlash-NPU2, 270,406,816 B, sha256 matching the repo'sMANIFEST.sha256):q_projis 6,553,600 bytes, which fits both a q4_1 tile grid of 4096 x 2560 and a half-float matrix of 1280 x 2560. The last comment stopped short of a decode rule because of that.BF16are really F16, because norm gains read "tight" as half-float.Both are settled here from the bytes, and both go against a new format.
Findings (device-free, this box)
I8 [1280, 5120]is 1280 tiles of 5120 bytes, and 1280 = (4096/32) x (2560/256). The same arithmetic gives 320 for k/v (1024 x 2560) and 1600 for gate/up (5120 x 2560) and down (2560 x 5120). That is the form every main-model container already uses.head_dimis 128, as the config and the README say. The half-float reading that suggested 40 is wrong: read that way, 25.7 % ofq_proj's values exceed 100 in magnitude and 1.8 % are non-finite. As a tile grid, 1280 x 2560 would also be 400 tiles, not 1280.BF16labels are right. The "tight as half-float" pattern is not evidence: it is present inQwen3-0.6B's main-model container too (input norm 1.49-1.88 as half-float, 0.12-1.05 as bf16), and our forward serves that model correctly with the bf16 reading. Half-float folds bf16's exponent range into one octave, so anything near 1 looks tight.So importing a DFlash draft is a config-schema translation. Nothing in the container needs a new reader.
The change
tools/dflash_draft_info.py(new, stdlib only): checks a draft package against its own config and prints a census (--json). It reads tile headers only, so the 0.27 GB package takes seconds. It fails closed.--selftest: a synthetic package plus eight negative controls, each failing by name: a projection one tile short, ahead_dimthat disagrees with the stored tiles, one NaN scale, a plain half-float matrix of the same byte count, a block size that differs from the config, an unexplained tensor, a non-dflash config.CMakeLists.txt: ctest rowtool_selftest_dflash_draft_info.docs/amd-drafters.md: the answers above, and what is missing.What remains on #1780
target_layer_idsthroughfc, run five non-causal layers over a block ofblock_sizemask positions, score with the target's embedding table and head.forward-servestops at the config schema today (key 'model' not found).Nemotron-3.5-Bolt-E4B-NPU2) on the box.Qwen3.8-27B-DFlash2-NPU2, 1.4 GB) is a different architecture family with selector and convolution tensors. This tool reports those as unexplained rather than guessing; it was not downloaded (the disk is under its floor).🤖 Generated with Claude Code