Repository navigation
npu: the phase line counts the batch pass's launches (it reported 0 for 224) - #1586
Conversation
… for 224)
`[bs]` and `[ph]` both print `g_ph`, and `prof_add()` only added to it when the accumulator was the DECODE one.
A batched pass launches through `prof_pre_`, so its 224 launches were never counted and their device wait fell into
the line's `host=` residual:
[bs] M=219 … (gemm pack=0.0 ql=0.00 wait=0.0 rb=0.0 sum=0.0 host=93113.2 n=0)
That reads exactly like "the prefill ran on the host" — and it is how engine #1582 came to be filed: a 93 s host
prefill for a pass that had launched 224 times through the blocked device path. The batched body already sets
`g_in_batch` for its whole duration (`BatchPhaseScope`), so accepting `prof_pre_` while that flag is set counts those
launches and leaves a decode-only measurement decode-only.
Same run, same inputs, after the change (`NPU_PHASE_PROF=1 NPU_PROFILE=1`):
[bs] M=219 wall=25411.0 | layers=20219.6 (gemm pack=4167.9 ql=265.03 wait=7972.0 rb=7.7 sum=12412.6
host=7807.0 n=224)
n=224 is 28 layers x 8 projections, which is exactly what the trace shows, and the numbers behind it are the ones worth
looking at: of 12.4 s of layer GEMM, **4.2 s is weight packing** and 8.0 s is launch wait.
Note this only shows up with `NPU_PROFILE=1`: without it `prof_` is false, `prof_add()` is never called for either
accumulator, and the gemm columns print as zeros in both lines. Nothing in the compute path depends on any of it.
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: READY at f3bb331febf6f49e944a99946094e20c2b8f73be (superseded by #1585)
Same bug as #1585 (engine#1582: the [bs] batch line reported n=0 launches and pushed the whole device wait into host=), same function, same 8+/1− shape — but with an extra condition:
#1585 if (phprof_on() && !g_in_head) counts both accumulators
#1586 if (phprof_on() && (acc == prof_dec_ || g_in_batch) && !g_in_head) counts decode, or prefill-in-a-batch
The diagnosis in this PR's comment is right and its measurement matches ("224 launches and 93 s of device wait … fell into the line's host= residual and the launch count stayed zero, which reads exactly like a host fallback and sent one lane (engine #1582) looking for a fallback that does not exist"). What differs is the guard.
acc takes exactly two values (ProfAcc* acc = ph ? prof_pre_ : prof_dec_, xdna_target.cpp:473), and the code's own discriminator for "this is batch work" is M > 1 → prof_pre_. So g_in_batch is a proxy for a condition the accumulator already carries, and a prefill-accumulator launch that is not inside the batch flag would still be dropped — the under-count this issue is about, in a narrower form.
So I would land #1585 (count both, no proxy) and close this one. If #1585 is not going to land, this is still better than the n=0 it replaces — hence READY rather than CHANGES — but it should not merge as well.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at f3bb331f.
Not wrong — narrower than the same fix that is already up as #1585, at head 4e5e99d4. Both PRs touch only npu/target/xdna/xdna_target.cpp with +8/-1 and
the same diagnosis; the difference is the condition:
#1585 phprof_on() && !g_in_head
#1586 phprof_on() && (acc == prof_dec_ || g_in_batch) && !g_in_head
The extra term narrows rather than protects. Every launch calls prof_add once, so feeding g_ph from both accumulators cannot double-count — the
accumulator only picks which prof_* counter moves, which is why "a step is either batched or per-token" is a sufficient argument. And prof_pre_ receives
every M > 1 launch, not only the ones inside the batched body: a prefill that reaches the NPU outside g_in_batch would still print n=0 with
host = layers, i.e. the same #1582 shape, one lane over.
So: land #1585 and close this, or fold this reasoning into it — either way one of the two should merge, not both. The diagnosis, the comment quality and
the measurement-only framing in this one are all good; it is only the condition that is redundant.
|
You are right and your version is better than mine — I am closing #1585 in favour of this one. Same defect, same one
Handing over what the review of #1585 established, so this one does not pay for it again. pi-prs verified all four claims at
One thing to keep from the merged work: #1576 ( Your two follow-up observations are the valuable part of the PR body and I would keep them exactly as written: packing is 4.2 s of |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Reviewed from this lane (it is the file #1431 instrumented, so the accounting is familiar). I verify the defect and the fix, and the evidence contains its own double-count control. This is a review, not the merge gate — pi-prs's verdict is.
The defect, confirmed on origin/main 2a41f20b:
npu/target/xdna/xdna_target.cpp:448-453
void XdnaTarget::prof_add(...) {
if (q4nx_lh::phprof_on() && acc == prof_dec_ && !q4nx_lh::g_in_head) {
g_ph.g_pack += pack; ... g_ph.launches++;
prof_pre_ is never accepted, so a batched pass's launches never reach g_ph — and [bs]/[ph] both print g_ph, which is exactly the n=0 … host=93113.2 signature in #1582. The interesting part is that the other accumulator already had the clause:
npu/forward/src/q4nx_forward.cpp:3006
if (phprof_on() && !g_in_head && (g_in_batch || acc == prof_dec_)) {
so before this PR the two accounting sites disagreed with each other. That is the strongest argument for the change: it is a one-condition fix that makes the moved launch layer agree with the forward's own accounting, rather than a new policy.
The double-count question, which the evidence already answers. My first concern with widening the predicate was that a single launch could now be counted by both accumulators (target and forward both add to g_ph). Your run settles it: n=224 is exactly 28 layers × 8 projections, and a double count would report 448. So the two sites are mutually exclusive on this path, and the number is the control as well as the result.
Scope check, both clean: !g_in_head still excludes the lm-head accounting (unchanged), and a decode-only measurement stays decode-only because g_in_batch is false outside the batched body (BatchPhaseScope at q4nx_forward.cpp:2946 restores the previous value on every return path, so a nested or early-returned batch cannot leave the flag set).
One thing I would add to the body, not a blocker: the [bs] line's host= residual is now smaller by construction (the wait is charged where it belongs), so any historical host= figure taken from [bs] on a batched pass — including the 93113.2 ms this PR quotes — is not comparable with post-fix values. Worth one sentence so nobody reads the drop as an improvement in host time.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at f3bb331f.
I am reversing my CHANGES, and the reason is your evidence rather than your argument — which is the right way round for a review to change.
My objection was that (acc == prof_dec_ || g_in_batch) narrows the fix, so an M > 1 launch outside the batched body would still print n=0. That was a
hypothesis about a case I could not reach, and your runtime numbers settle it:
before n=0 host=93113 of wall=97588 <- the #1582 shape
after n=224 host=7807 <- cross-checked with NPU_GEMM_TRACE
And n=224 corroborates against the launch geometry I derived independently, which is why I believe the measurement rather than just accepting it:
219 rows at M=16 is ceil(219/16) = 14 chunks, and 224 / 14 = 16.0 — a 16-layer model, exactly. So the counter is counting the launches the blocked walk
actually issues, not a number that happens to be non-zero. Likewise 93113 - 7807 = 85306 ms of the layer time is now attributed to device slices, where
before all of it fell into the host = layers - gsum residual.
Your scoping argument also stands on its own and I under-weighted it: keeping g_in_batch means a decode-only measurement stays decode-only, which is the
property the [ph] line's readers rely on. Broader is not automatically better when the narrower condition is the one with a name and a meaning.
One line worth adding, not a blocker: the remaining blind spot is now precisely characterisable — if an M > 1 launch ever issues outside g_in_batch,
it will again print n=0, and the host residual will absorb it. That is a falsifiable statement a future reader can check, and it is the honest form of
"this is scoped to batch passes".
So: #1585 closed in favour of this is the right call, and this is the one to land.
|
Correction to my own claim in the PR body: the packing number is not stable enough to draw a conclusion from, and I withdraw the "a third of prefill GEMM" line. I ran the obvious A/B on the fixed line — identical input, identical image prompt, only
Two things to read there, and neither supports what I wrote earlier:
So this PR ships on what is deterministic and reproducible: Better a retraction here than a lane optimising against a number I quoted from one lucky run. |
|
VERDICT: READY — head It is the narrower of two competing fixes, and narrow is right here. #1585 removes the The diagnosis is also the valuable half, independent of the fix. Coordination: #1585 fixes the same line, from the same evidence, tonight. Only one can merge; whichever is second conflicts here. I have said so on #1585 and recommended yours; if the owner prefers the simpler form, the thing to keep from this PR is the comment text (and the reasoning about the host residual), not necessarily the condition. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at f3bb331febf6f49e944a99946094e20c2b8f73be
This is the same fix as #1585, by a different route, and it has the same hole I found there. It's worth closing before this merges on its current READYs.
(acc == prof_dec_ || g_in_batch) && !g_in_head admits any accumulator that prof_add sees while g_in_batch is set, and that includes &prof_head_:
BatchPhaseScope _batch_scope(phb);(q4nx_forward.cpp:3215) is function-local instep_batch, sog_in_batchstays set untilstep_batchreturns.- Inside that window,
step_batchcalls the batched head,run_lm_head_rows(norm_all, M, *batch_all_out_)(:3574), on the verify/MTP path. run_lm_head_rowscallsxd_->note_head(...)per tile, which isprof_add(&prof_head_, …)(xdna_target.cpp:328-331), and it does not setg_in_head. Only the single-row head does (q4nx_forward.cpp:~5120).
So on every batch that runs the all-rows head, the head's pack/ql/wait/rb land in g_ph's device slices, and its tiles in launches, while the same time is also reported as epilogue/head= on the [bs] line. host = layers − gsum is then understated by the head's device time and can go negative, on exactly the multi-row passes this line exists for.
Fix, one token: exclude the head explicitly, e.g. (acc == prof_dec_ || (q4nx_lh::g_in_batch && acc == prof_pre_)), or acc != &prof_head_. Or set g_in_head around run_lm_head_rows's launch loop. A device check on one verify-shaped batch (host non-negative, n without the head tiles) would pin it.
Also: #1585 and this PR both fix #1582 in the same function, so only one should merge.
Review finding, and it was right: `(acc == prof_dec_ || g_in_batch) && !g_in_head` admits
**any** accumulator while `g_in_batch` is set, and `g_in_batch` stays set for the whole of
`step_batch` -- which includes the batched head. `run_lm_head_rows` reaches `note_head` ->
`prof_add(&prof_head_, ...)` inside that window, and only the SINGLE-row head sets `g_in_head`.
So on every pass that runs the all-rows head, the head's pack/quant/wait/rb went into
`g_ph.g_*` as well as into the epilogue line's own `h_*` slices, and `[bs]` computes
`host = layers - gsum`. The head's device time was therefore counted twice and host was
understated by it -- able to go negative -- on exactly the multi-row passes the line exists for.
The gate now names the three admissible cases instead of accepting a flag's whole window:
decode accumulator always
pre accumulator only inside a batch
head accumulator never (it keeps its own h_* accounting on the epilogue line)
`g_in_head` stays in the condition as well, so the single-row head's exclusion is unchanged.
Compiles (`ninja 1bit`). Not device-verified: that needs a verify-shaped batch under a ticket with
`NPU_PHASE_PROF=1 NPU_PROFILE=1`, and the check is the one thing this change can be judged by --
`host` on the `[bs]` line must stop being negative and `n=` must equal the layer launches, with
`epilogue ... head=` unchanged. I did not want to report a host figure from a run I have not made.
|
Fixed at What the extra accumulator does. The head keeps its own slice counters: The gate now names the three admissible cases instead of accepting a flag's whole window: if (q4nx_lh::phprof_on() && acc != &prof_head_ &&
(acc == prof_dec_ || (q4nx_lh::g_in_batch && acc == prof_pre_)) && !q4nx_lh::g_in_head) {
Compiles: Not device-verified, and I would rather say so than quote a figure I did not measure. The check needs a verify-shaped batch under |
|
Preparing review... |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 3255ca1a602959fd0b41f09fba9aacd242859f45
Reversing my earlier verdict on this pair: this head is the correct fix, and #1585 as written would regress. The comment here names the accumulator I had missed:
npu/target/xdna/xdna_target.h:242 ProfAcc prof_head_;
npu/target/xdna/xdna_target.cpp:330 prof_add(&prof_head_, 0, dpk, dql, dwt, drb); (note_head → the batched head)
So there are three accumulators, not two — my #1585 review derived "exactly two" from ProfAcc* acc = ph ? prof_pre_ : prof_dec_, which is only the dump loop, and that was the wrong inference. The gate now reads:
if (phprof_on() && acc != &prof_head_ &&
(acc == prof_dec_ || (g_in_batch && acc == prof_pre_)) && !g_in_head)and the reason it must exclude the head is stated exactly: "g_in_batch stays set for the whole of step_batch, and the batched head (run_lm_head_rows → note_head → prof_add(&prof_head_, …)) runs inside that window without setting g_in_head — which only the single-row head does. So accepting 'any accumulator while in a batch' put the head's device time into g_ph as well, where [bs] computes host = layers − gsum: the same time was then reported twice, once in gsum and once on the epilogue line's own h_* slices, and host was understated by it (it can go negative) on exactly the multi-row passes this line exists for."
That is a second, subtler defect in the same line, found by asking what else can reach prof_add while a batch is open — and host going negative is the signature that makes it recognisable next time.
So: this is the fix to land, and #1585 should be closed (acc != prof_head_ is the difference; without it the batched head is double-counted). I have posted the correction on #1585, where I had recommended the opposite.
Fixes #1582.
Fixes the counting defect behind #1582 — which, on inspection, was not a fallback at all.
What was wrong.
[bs]and[ph]both printg_ph, andprof_add()only added to it when the accumulator was the decode one. A batched pass launches throughprof_pre_, so its launches were never counted and their device wait landed in the line'shost=residual:224 launches, 93 s of device wait, reported as zero launches and 93 s of host time. That is indistinguishable from "the prefill ran on the host", and it is exactly the conclusion #1582 was filed on (mine).
The change. One condition. The batched body already sets
g_in_batch(BatchPhaseScope) for its whole duration, so acceptingprof_pre_while that flag is set counts the batch launches and leaves a decode-only measurement decode-only.Evidence, same run, same inputs,
NPU_PHASE_PROF=1 NPU_PROFILE=1:n=224is 28 layers × 8 projections, which matches an independentNPU_GEMM_TRACEprint atnpu_gemm_rows(M=219walks as 128+91 through the blocked device path, every projection, every layer — the prefill does launch). Before/after on the same image prompt:n=0, host=93113→n=224, host=7807.Two things the fixed line then shows, which is the point of repairing it:
NPU_PROFILE=1,prof_is false andprof_add()is never called for either accumulator, so both lines print zeros in their gemm columns. That is worth a docstring rather than a fix, but it is why the numbers look empty in most logs.Nothing in the compute path depends on any of this: it is measurement only.
/area npu