Repository navigation
mtp: the draft loop's cost is 45 % verify batch / 28 % step / 18 % head, measured per phase instead of inferred - #1042
Conversation
…ad, measured per phase instead of inferred The loop's cost model was assembled by subtraction across tickets: "two passes per iteration" (one plain step plus one M=verify batch) came from counting calls, and everything after that was arithmetic. Counting calls is not the same as pricing them, and the two candidate changes this lane has been weighing are decided by exactly that difference -- removing the plain step, or widening the batch -- so the four terms now get measured in the same run. `Stats` gains ms_step / ms_batch / ms_draft / ms_commit (steady_clock::now() around each phase, always accumulated, printed only under MTP_PHASE_SPLIT=1) and the live report prints one split line per prompt plus one for the run. Measurement only: nothing here is read back into a decision the loop makes, and the emitted text, acceptance and the drafter's stream are unchanged. Measured on bonsai2-27b-pb16i (sealed M=16 pb16i ELFs, one npu-hold claim, amdxdna errors 0), prompt 0, 64 tokens, k=4, tau=0, 20 iterations, load 53-57: step 11477.6 ms (28.1 %) + batch 18334.9 ms (44.9 %) + head 7466.3 ms (18.3 %) + commit 3554.7 ms (8.7 %) = 40833.4 ms of draft phase = 2042 ms/iteration, chain 3.20 tokens/pass Two things follow, and both are decisions rather than observations. First, the plain step is 28 % of the draft phase, so folding it into the verify batch cannot buy the 25 % it was priced at: a one-pass loop has to draft from the previous committed row, whose first draft is a self-regressive head step -- depth-2 quality (65 % here) instead of depth-1 (90 %) -- and 2042 - 574 (step) + 87 (extra row) + 81 (extra head step) over a 2.55 chain is 642 ms/token against today's 638. Second, the batch is the single largest term (45 %): verifying five rows costs 917 ms/iteration while the same array launches M-independent rows, so the per-row host work is where a draft-route win would have to come from -- not from the loop's shape. No acceptance claim is made or moved by this commit: p0 still reports 44/80, chain 3.20 tokens/pass, per-depth 90/65/35/30, TEXT IDENTICAL, 0 stream mismatches.
PR Reviewer Guide 🔍(Review updated until commit 365400b)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: READY at 14d982051
Measurement-only, and I checked that it is: the four accumulators are written and printed, and nothing reads them back —
grep for ms_step|ms_batch|ms_draft|ms_commit in npu/mtp/draft.h, excluding the declarations, phase_ms() and the += sites
-> nothing
and the header parses and runs standalone, so the change is not entangled with the tool that prints it:
g++ -std=c++17 -fsyntax-only -I. (a TU that includes npu/mtp/draft.h) -> OK
onebit::npu::mtp::Stats{}.phase_ms() -> 0.000
The placement of the five lap() calls matches the phases the body claims — two ms_step (the loop pays two single-token passes per iteration), one ms_draft (the head's own chain), one ms_batch (verify batches, array + host), one ms_commit (snapshot restore + replay) — and tp is seeded once before the loop, so no phase double-counts its predecessor's tail.
The reason given for measuring all four in one window is the right one and worth keeping in the commit message: "two passes per iteration" only prices a loop change if the passes carry the cost, and subtracting one phase across two runs is exactly how a number ends up attributed to the wrong thing.
Two notes:
lap()is unconditionally accumulated with the justification "steady_clock::now()is ~20 ns against a ~0.5 s pass". That is true of a real pass, butMTP_PHASE_SPLITalso runs on the 16-token acceptance arms where a step can be ~10 ms — still negligible (4 clamps ≈ 80 ns), but if this ever lands in a hot path (per-token, not per-pass) it should be gated likeNPU_PHASE_PROFis.- The claim "bit-identical to the same run without the flag" is the one thing I could not check without a device; it follows from nothing being read back, which is what I verified, so I am satisfied — but if you have the two acceptance logs, quoting the text/acceptance rows side by side is cheaper for the next reviewer than trusting the argument.
|
Persistent review updated to latest commit 365400b |
…table stops freezing on its first call (#1056) A batched verify pass is 45 % of the MTP draft loop (#1042) and 43 % of that phase was unattributed, because the phase instrument only existed for the per-token path: prof_add() folds the pack / quant+launch / wait / readback split into g_ph only for M == 1 launches (acc == prof_dec_), while a verify batch's launches are M > 1 and land in prof_pre_. - g_in_batch (set for the batched body of step_batch() by a scope that clears it on every return path) makes those launches feed the same g_ph. - step_batch() now prints one [bs] line per pass with [ph]'s partition: wall | embed | layers (gemm pack / ql / wait / rb / sum / host, launches) | epilogue (head + its own split) | tail. tail is what the partition misses. - The ranked [lh] table and its UNATTRIBUTED row move out of step() into lh_dump_layer_host(what, unit) and run for the batch too, so both residuals are read the same way; step()'s own output is unchanged. Fixed on the way, and it was already visible in a log: the TOP-LEVEL PARTITION table's row array was `static`, so it initialised once and every later line reprinted the first call's numbers - ticket-batchprof-p0-before-20261104-142823.log shows load_layer 8587.41 ms on every token while the live ACCOUNTED row moves 654 -> 690 ms, with rows reading 1300 % of "gemm-free host". It is now a plain local. prof_dump() returns early unless NPU_PROFILE is set: the launch timers are shared now, and the per-role table is not what a phase run asks for. Measurement only: nothing is read back, and with the environment unset no code path changes. Host-verified: ninja mtp_draft_test mtp_acceptance rc 0 (the pre-existing -Wstringop-overflow at q4nx_forward.cpp:2037 only), ctest -R mtp_draft 1/1, tools/copyright.py --check clean. The device confirmation is NOT done: this box's NPU array went unusable at 22:28 ("aie2_create_context: NPU array is unusable until the driver is reloaded") and the ticket that was to take the numbers stopped at ELF init with device error 102, 10 s in, before any compute - see TMTPLOOP-STATE.md. Co-authored-by: pi-mtploop <pi-mtploop@1bit.local>
What this does
Statsgains four phase timers —ms_step,ms_batch,ms_draft,ms_commit— accumulated withsteady_clock::now()around each phase of the draft loop, and the live report prints one split line per prompt plus one for the run underMTP_PHASE_SPLIT=1.Measurement only: nothing here feeds back into a decision the loop makes. Emitted text, acceptance, per-depth tables and the drafter's stream are bit-identical to the same binary without the flag (p0:
44/80, chain3.20,90/65/35/30,TEXT IDENTICAL,0 mismatches).Why it is needed
This lane's cost model for the draft route was assembled by subtraction across tickets: "two passes per iteration" came from counting calls, and the price of each term was inferred from arms measured at different load. Two candidate changes have been weighed on that model — remove the plain step, or widen the batch — and both are decided by the difference, so the four terms are now measured in one window.
Measured (bonsai2-27b-pb16i, sealed M=16 pb16i ELFs, one
npu-holdclaim,amdxdnaerrors 0)Prompt 0, 64 tokens,
k=4,tau=0, 20 iterations, box load 53-57, plain decode 0.479 s/token:commit_verify+ drafter advance)The two decisions this settles
1. Folding the plain step into the verify batch cannot buy the 25 % it was priced at. A one-pass loop must draft from the previous committed row (
npu/mtp/draft.h:246is what produces the base hidden that:261hands toMtpDrafter::draft(), andnpu/mtp/mtp_head.cpp:367chains from it). That first draft is then a self-regressive head step — depth-2 quality, 65 % here, instead of depth-1's 90 %. On measured terms:A dead heat, so the change is not carried in this PR. One earlier arm corroborates the direction:
k=6bought chain 3.25 for about 0.84 units of cost more thank=4(depth 5 hit 20 %, depth 6 hit 0 %).2. The batch, not the loop's shape, is where a draft-route win has to come from. Verifying five rows costs 917 ms/iteration while a pb16i launch runs its rows M-independently, so that 45 % is per-row host work. For the serve switch that means: the draft route's remaining lever is
step_batch_all's per-row cost and the base step, not another loop rearrangement.Cost of the flag
steady_clock::now()twice per iteration, ~40 ns against a 2 s iteration; the accumulators are four doubles inStats. It prints nothing unlessMTP_PHASE_SPLITis set, so CI and the toy test are unaffected (ctest -R mtp_draftgreen,tools/copyright.py --checkclean).