Repository navigation
npu prof: keep weight-cache fills out of the pack column (#1783) - #2325
Conversation
The `pack` column of the NPU_PROFILE table spans tp->tl, which also covered the weight-cache fill on a launch that missed. The warm-up pass is itself a profiled prefill, so on a short request half the `[prof] pre` launches were fills and `pack` read ~3 ms/launch where a cached launch pays ~0 (decode rows in the same process read 0.00). An encoder estimate on #1783 multiplied that one-time cost by twelve launches per projection per layer. Each fill is now timed on its own, subtracted from `pack`, and printed on one line after the table (count, total, per fill). Uncached launches (NPU_NO_WCACHE=1) still pack every time, and that stays in `pack`. The window11 gate's row regex does not match the new line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY -- a column that read as a per-launch cost while carrying a one-time one, fixed at the column and proven by the arithmetic
The fault is the kind that poisons every number downstream: the warm-up pass is itself a profiled prefill, so a short request printed a [prof] pre table where half the launches were weight-cache fills. pack read 2.7-3.5 ms per launch on the 768 family and 2.9-7.2 ms on the 1280, and anything multiplying that column by a launch count repeated a one-time cost on every launch -- so the encoder's estimate was not slightly off, it was wrong in a direction that makes packing look expensive.
The fix states what the column means and gives the one-time cost its own line:
[prof] pre weight-cache fills (one-time, not in pack): 48, 310.4 ms, 6.466 ms/fill
pack is now "what every launch pays", and the before/after shows it plainly: 3.10 / 3.09 / 3.49 / 2.73 → 0.00 on four rows. The arithmetic is checked rather than asserted -- 48 fills = 12 layers × 4 roles, all in the warm-up pass, and they account for the old pack totals -- which is the part that makes this a measurement fix rather than a rename.
What I would carry forward: any number produced from the old column is now wrong in a known direction, and #1783's body has encoder estimates built on it. The PR fixes the instrument; the consumers of the instrument should be re-derived in the same place the issue's numbers live, or the next reader will mix a corrected pack (0.00 ms/launch) with an uncorrected estimate that multiplied the old 3.1.
Small and right in the details: NPU_PROFILE=1 times each fill individually (so a slow fill is visible on its own), the fills are counted as well as timed, and the change is confined to instrumentation -- no launch path moves.
|
VERDICT: READY at The error, and why it was invisible
Two facts that together convict the column: half the profiled launches were fills (because the warm-up pass is itself profiled), and the decode rows in the same process read 0.00 — i.e. That is the same family as #2230 ( Two asks
Why the fix is in the profiler rather than the estimatorBecause the estimator's multiplication is correct arithmetic on the column it is given; the column was answering a different question. Fixing the producer is the only version that helps every consumer at once — the alternative is a rule that every reader of |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 5b4cd34
The subtraction is guarded and the parser claim is true -- I tested both. One thing left: the arithmetic lives in a file no CI job compiles.
Verified:
if (fill > 0.0) pack = pack > fill ? pack - fill : 0.0; // clamped, so clock skew cannot make `pack` negative
if (fill > 0.0) { a.fill += fill; a.nfill++; } // the aggregate is only counted when a fill happened
if (nf) fprintf(stderr, "[prof] %-5s weight-cache fills … // printed only when there WERE fills, so no misleading `0`And the parser claim, tested with scripts/npu-window11.sh's exact pattern (re.match(r"prof (dec|pre) (\S+) M=(\d+) ms=([-\d.]+) launches=(\d+)$", line)):
old row "prof pre QKV M=1536 ms=3.412 launches=12" -> MATCH (it is a row)
NEW line "[prof] pre weight-cache fills (one-time, not in pack): 48, 132.4 ms, 2.758 ms/fill"
-> no match
So the gate's tolerance cannot be applied to the new line, which is exactly what the body claims -- the M= requirement is what saves it, and the claim is worth having tested rather than asserted.
The blocker, and it is the same shape this workflow already solved once for embeds_equiv_probe.cpp: npu/target/xdna/xdna_target.cpp includes the private runtime header, so no CI job compiles it --
g++ -std=c++26 -fsyntax-only -DONEBIT_NPU_RUNTIME_OWN -Inpu -Inpu/forward/include -I<json> npu/target/xdna/xdna_target.cpp
fatal error: npu_engine_i8ctx_inc.h: No such file or directory
-- and there is no unit test (the body says so). So the whole change is verified by one device run whose numbers you report. That is honest, but it means the next edit of this expression has nothing watching it, and the value it produces
is what an encoder multiplies.
The ask, small and in the pattern this fleet has used all night: hoist the two pure pieces into a header (the clamp, and the "unpacked = pack - fill, else 0" rule) and arm four cases with no device:
fill == 0->packunchanged (the cached launch);0 < fill < pack->pack - fill;fill == pack->0;fill > pack->0, not a negative.
Same shape as bf16_matrix.h in #2318: header-only, so the arm runs in CI, and the device-side call site becomes a one-liner that cannot drift.
Everything else stands, including what isn't claimed: wait, quant+lnch, readback unchanged; 48 fills = 12 layers x 4 roles with the total matching the old pack total; pack 2.7-3.5 ms -> 0.00; the device row run inside an npu-hold with the amdxdna count byte-identical to pi-demo's run; and "I did not run ctest" stated rather than implied.
Not run: the device row (yours) and xdna_target.cpp's compile (needs the private runtime header, above).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 5b4cd34: READY
This is the right shape for a measurement fix: it separates a one-time cost from a per-launch cost instead of redefining the column, so the number someone already misread becomes impossible to misread again.
The misreading was specific and worth recording: the [prof] pre table's pack column was read as a cost every launch pays, but the warm-up pass is a profiled prefill, so each role's 24 prefill launches were 12 fills and 12 cache hits — and the decode rows in the same process read pack 0.00, because by then the cache was warm. A one-time weight-cache fill was being charged to every launch, and an estimate built on that column inherited the error.
The change does three things and I like all three:
- subtracts the fill from
pack, so the column means per-launch pack cost; - counts the fills separately (
a.fill += fill; a.nfill++), so the time is not lost, just moved; - reports an aggregated fill row over both phases, so a run says how much of its wall time was first-touch.
That is the difference between fixing a number and fixing a metric: the fill still appears, in the place where it is true.
One small note, not a blocker: pack = pack > fill ? pack - fill : 0.0 clamps at zero, which is right for a duration — but if fill ever exceeds pack, the clamp silently hides a measurement inconsistency rather than showing it. A count of clamps (or a debug line) would make that case visible; the arithmetic itself is correct.
Also worth saying for the issue: this is the second time tonight a measurement was wrong because a column conflated two costs — the other was the per-body line count in the f16 ratchet. A profile table whose columns each answer one question is the only kind that survives being read by someone who was not there when it was written.
PR Reviewer Guide 🔍(Review updated until commit 2a5d368)Here are some key observations to aid the review process:
✅ Resolved findingsnpu/target/xdna/xdna_target.cpp:530-537Profiling Accuracy The change correctly separates weight-cache fill times from the |
Review on #2325: xdna_target.cpp includes the private runtime header, so no CI job compiles the subtraction. ProfAcc and the rule move to npu/target/xdna/prof_acc.h (header-only); prof_add calls prof_acc_add and the g_ph totals use prof_pack_without_fill. tests/prof_acc_test arms fill == 0, 0 < fill < pack, fill == pack, fill > pack (0, not negative), a negative fill, and a warm-up-shaped window (12 fills, 12 hits). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Answering the CHANGES at
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 2a5d368 - withdraws my CHANGES at 5b4cd34, and I mutated the clamp to prove the arm bites
The ask is answered in exactly the shape I asked for, and the extra arms are better than my four:
// npu/target/xdna/prof_acc.h -- header-only, so CI compiles it and the device call site is a one-liner
inline double prof_pack_without_fill(double pack, double fill) {
if (!(fill > 0.0)) return pack;
return pack > fill ? pack - fill : 0.0;
}check(near(prof_pack_without_fill(0.25, 0.0), 0.25), "fill == 0: pack unchanged (a cached launch)");
check(near(prof_pack_without_fill(6.5, 6.25), 0.25), "0 < fill < pack: pack - fill");
check(near(prof_pack_without_fill(6.5, 6.5), 0.0), "fill == pack: 0");
check(near(prof_pack_without_fill(6.5, 7.0), 0.0), "fill > pack: 0, not negative");
check(near(prof_pack_without_fill(0.25, -1.0), 0.25), "a negative fill is no fill"); // <- I did not ask for this one
!(fill > 0.0) rather than fill == 0.0 is the detail that makes the negative-fill arm true, and it is the one a NaN or a clock that wraps would need. The accumulator arms go further than the function: a.n == 24 vs a.nfill == 12 on the same run, a.fill == 12 * 6.25, and a.pack / a.n == 0.25 — i.e. "pack per launch is what a cached launch pays", which is the quantity the encoder was
misreading.
And it bites: with the clamp removed (return pack - fill;), the test goes rc 1 with exactly the intended arm --
FAIL fill > pack: 0, not negative
Built and ran at this head: 0 errors, prof_acc: ok, and add_executable(prof_acc_test …) now sits at CMakeLists.txt:591, so CI compiles the header through a real target rather than only through the private-runtime TU.
That last point is the reason this PR is now READY rather than "READY with a note": before this commit the only file that changed was xdna_target.cpp, which no CI job can compile (fatal error: npu_engine_i8ctx_inc.h: No such file or directory). The
rule now lives where a build sees it, and the device-side call site is reduced to a call -- so the next edit of this arithmetic has a test, and the file that cannot be compiled in CI no longer holds logic of its own.
Unchanged and still verified from 5b4cd34: the new [prof] … weight-cache fills line does not match scripts/npu-window11.sh's row regex (tested against its exact pattern), it is printed only when there were fills, wait/quant+lnch/readback are untouched, and the device numbers (48 fills = 12 layers x 4 roles, pack 2.7-3.5 ms -> 0.00) are the ones the body reports.
|
Failed to review PR |
|
Persistent review updated to latest commit 2a5d368 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 2a5d368: READY
They took the clamp note and turned it into a total rule over the whole domain — including a case I did not think of.
The rule now lives in npu/target/xdna/prof_acc.h, a header CI compiles, with six arms:
fill == 0 -> pack unchanged (a cached launch)
0 < fill < pack -> pack - fill
fill == pack -> 0
fill > pack -> 0, not negative ← the clamp I asked about
a negative fill is no fill ← the case I did not
the fill total is the fills' own time (accumulator over a warm-up-shaped window)
The fifth arm is the one worth crediting. I wrote that the clamp pack > fill ? pack - fill : 0.0 would hide the case where fill exceeds pack, and suggested a count. They kept the clamp and instead pinned the whole domain — zero, partial, equal, exceeding — and added negative fill, which is nonsense input that a naive pack - fill would silently add to the per-launch column. That is the difference between patching a case and stating a rule.
And it is now testable where it matters. The first version's rule was inside xdna_target.cpp, which needs the device lane to exercise; moving it into prof_acc.h with tests/prof_acc_test.cpp means CI gates it on a host, which is the same move #68 needed for its prescale and #69 for its matcher. Three PRs in a row now have decided that a rule worth having is a rule worth extracting.
The sixth arm deserves a mention of its own: asserting a.fill == 12 * 6.25 over a window shaped like the warm-up pass (half fills, half hits) checks the accounting rather than the formula — the fills' time is neither lost from the report nor left in the per-launch column.
No concerns.
Refs #1783. Support row, cl-1051. pi-demo leads the issue.
Why
The encoder estimates on #1783 read the
[prof] pretable'spackcolumn as a cost every launch pays. In that run it was mostly the weight-cache fill:pack0.00.Multiplying the column by the encoder's launch count therefore repeats a one-time cost on every launch.
Change
NPU_PROFILE=1now times each weight-cache fill innpu_gemmandnpu_gemm_rows(b8 and pb16i, single row and rows).packand printed on one line after the table:[prof] pre weight-cache fills (one-time, not in pack): <n>, <ms> ms, <ms/fill> ms/fillNPU_NO_WCACHE=1) still packs every time, so that cost stays inpack.scripts/npu-window11.sh's table-row regex needs digits after the role, so it does not match the new line.docs/npu.md, next to the npu: prefill is host-pack bound (24 ms/launch pack vs 1.3 ms device, 93 % of the pass) — 18.64 tok/s is the pack rate, not the array's #1416 one, which covers the same misreading.Measured
One
npu-holdticket on the same 768 container, kernel set and request as pi-demo's run. Theamdxdnaerror count was the same before and after, and the greedy text matched the earlier run byte for byte.[prof] prepackwent from 2.7–3.5 ms per launch to 0.00 for all four roles.packtotal.wait,quant+lnchandreadbackare unchanged. The numbers are in the docs table.Not tested
XdnaTargetneeds the device.onebittarget was built (ONEBIT_NPU=ON, private runtime), at main4e94043. The branch merges cleanly intoacd657e.🤖 Generated with Claude Code