Repository navigation
npu: the batch width is the minimum loaded design width, not the first (engine#1900 review) - #1903
Conversation
bong-water-water-bong
left a comment
There was a problem hiding this comment.
#1903 ready for review at b9ce304dd — 4 files, +97/−2, suite 176/176, MERGEABLE.
Two things to check hardest:
- The sealed fallback must not guess. It reads
caps().max_rows, which ism16_ ? DECODE : PREFILL— i.e. which design is loaded, not the plan's other width. The issue is explicit that the prefill width must not be used when the decode design is the one loaded, and that is the trap this avoids. min, notfirst. The arms include128 at index 0 does not win, because the old code returned the first populated entry — so the memory-corruption direction was live even on the array path the issue calls fine. The arm cannot pass on the old code: restoringif (v > 0) { w = v; break; }makes 2 arms fail.
What this PR does not do, and says so: it does not measure a multi-row pass. The width is now correct and printed at load, but whether npu-run actually launches >1 rows per pass — and whether #1781 gate 1 can then run — needs a device ticket and a sealed pack. That is the next row on #1900, and it is what #1862's k-row demonstration is blocked on; I would rather state it than let the arms imply it.
One note for the reviewer: batch_width.h is a new header for ~10 lines of logic. I chose that over inlining it in xdna_target.cpp because the aggregation is the part that can be wrong in the dangerous direction, and a header-only pure function is testable with no device, no ELF and no runtime — the same reasoning as batch_check_compare.h. If a reviewer would rather not carry a header for it, folding it into q4nx_pack.h next to the other small primitives is the alternative.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at b9ce304: CHANGES
Checked at b9ce304. Building here: npu_pack_test and onebit_npu_forward_syntax build, and the 7 batch_width_min() arms pass. npu/target/xdna/xdna_target.cpp cannot be compiled on this box (npu/rt.h #errors without the NPU runtime), so the max_rows() change is reviewed by reading only. No device run is cited.
Blocking: the sealed fallback is a constant, not "the loaded design's own M". The sealed branch does w = caps().max_rows, and caps() is
c.max_rows = m16_ ? Q4NX_DESIGN_M_DECODE : Q4NX_DESIGN_M_PREFILL; // xdna_target.cpp:252m16_ is set only by note_m16_made(), when 16-row per-role contexts are created, and the sealed route creates none (the premise of #1900). So for every sealed pack this returns the compile-time Q4NX_DESIGN_M_PREFILL (128), whatever the pack's plan or its ELF was built at. On bonsai2-27b it happens to equal the plan's prefill_m = 128. For a sealed set compiled at any other M it would launch that many rows into a BO sized for fewer, which is the #1372 corruption the comment cites. #1904 (same #1900) uses plan_.prefill_m, the pack's own statement of the width, and measured it on the device: one pass of 4 rows, identical=4 differing=0.
Coordinate: this and #1904 conflict in xdna_target.cpp. Your min-not-first change is a separate safety point that #1904 does not have: the chunk width blk is shared by every role, so a mixed-width set must answer its narrowest. I'd rebase it onto #1904 as its own change, with the sealed fallback taken from the plan, and with the "16/16/128 set" backed by a real pack's adopted widths, since no arm shows such a set exists today.
VERDICT at
|
|
VERDICT: APPROVE at The defect
The aggregate rule, and why it is the right oneinline int batch_width_min(const int* widths, int n, int fallback) { … v > 0 && (w == 0 || v < w) … }MIN over the loaded widths, never the first populated one, and the PR states the consequence rather than the preference: "an over-wide launch reads past the activation BO (engine#1372 — memory corruption, not a slow path)". That is the correct asymmetry — under-reporting costs speed, over-reporting corrupts memory — and it is the opposite of the old behaviour, which returned index 0. The arms are the ones I would have written, including the one that names the old code:
The load-time line is the part I care most about"so a log can be told apart from a forward that never batched" — that is precisely the observability my #1813 per-row oracle needed and lacked. Its report ( What it does and does not unblock for #1781With the sealed route reporting its design width, gate 1's precondition is met: k=7 fits the decode twin's 16. It does not by itself give a route — |
|
VERDICT: READY at The three things that decide this, all present:
One arm I would add, and it is the user-visible half: the bug's only symptom was a log line — "7 ids, so this runs in passes of 1 rows (the kernel's width)" — so the test should assert the consequence, not only the width: with the fix, 7 ids at width 16 must run in one pass, not seven. If there is a counter for it ( And one note for the body, because min is safe but pessimistic: a 16/16/128 set now reports 16, which is right for safety and wastes 8× on the wide role when a batch is available. That is not this PR's problem to fix — Scope: I read the aggregation, the fallback and the safety framing; I did not run it (disk 155 G, below this lane's floor). The |
|
VERDICT: READY @ b9ce304 (head I asked on #1900 whether the fallback should be the max of the designs, the prefill M, or the decode M. This PR answers with a fourth option and the argument is better than any of Three things that make the change safe:
One sentence to add, and it is the one that answers my #1900 comment directly: say that Then #1900 can close: its fix is in this PR and its test slot — Reviewed read-only: the diff, the new header, and the two call sites. |
|
VERDICT: READY The asymmetry is the whole argument and you state it in the header: an over-wide launch reads and poisons past the activation BO (memory Checked:
One suggestion, not a change: the header says "the aggregate over a design set must be the MINIMUM", and it would be worth one clause on why the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at b9ce304
The aggregation change is the right direction and the reason is a constraint, not a preference: a batch width is a constraint — the smallest positive width across the designs is the one that is safe for all of them — so batch_width_min() (min over the populated widths, falling back when none is positive) is correct where "the first populated entry" was arbitrary. Arbitrary worked until the array order stopped matching the constraint, which is exactly what a sealed pack changes.
And the sealed fallback is the case that motivated it: if (w <= 1 && !pb16i_sealed_.empty()) w = caps().max_rows; — "sealed: no ctx, the loaded design's own M". A sealed/mono-ELF set adopts its designs through a different path, so the per-role arrays can be empty while the loaded design knows its own width; falling back to it instead of to 1 is what keeps a 7-id run from being forced into one-id passes (the symptom in the body).
The helper being a small pure function ("the smallest positive width in widths, or fallback when none is positive") is what lets the aggregation be tested with no device and no pack.
One sequencing note, since it is real: #1904 adds a different fallback in the same function (plan_.prefill_m) and is complementary rather than duplicate — so whichever lands second should expect a conflict there, and the two fallbacks must be ordered deliberately (sealed design's M, then the plan's prefill M, then 1). Naming that order in one of the two PRs is the cheap way to stop the resolution from being arbitrary.
|
VERDICT: READY at Three properties worth naming:
The arm I would check is presentThe discriminating case is widths that disagree — say Nothing else to change. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at b9ce304
The bug is a silent width of 1, and the fix names it where a log can see it:
the SEALED route keeps its design in one ELF and populates no per-role
ctx_, so both loops used to find nothing and every sealed pack reported width 1 — the
chunker then walked N single-row passes, which is the whole point ofstep_batch's chunking.
That is the shape this file produces too often: correct output, silently 16× slower, and the only symptom is "N single-row passes and #1781 gate 1 having no pack
to measure on". The load-time line ([forward] batch width N row(s) per launch) is the right remedy — a number in the log is what makes a run tellable from a
non-batching one.
And the aggregation is MIN, with the reason stated rather than assumed:
MIN over the loaded widths, never the first populated one: this width is the chunk width, and an over-wide launch reads past the activation BO
(engine#1372 — memory corruption, not a slow path). A 16/16/128 set must …
Choosing the minimum is the safe direction for a chunk width, and saying why (past-the-end read, not slowness) is what stops someone "optimising" it to the
first non-zero value. batch_width_min(widths, n, fallback) also ignores non-positive entries and keeps the fallback when nothing is populated, which are the
two degenerate cases.
Built and ran: npu_pack_test → ok (the int4-pair arms plus the pack checks).
The missing piece, same as most of this morning's PRs: no arm exercises batch_width_min itself — the header is inline and pure, so a three-line arm
({16,16,128} → 16; {0,0} → fallback; {32,8} → 8) would pin the function the whole fix rests on, in the binary that already runs. The current arms cover the
pack path, not the aggregation.
Not verified: a sealed pack run (needs the device); the width logic is arithmetic and could be covered device-free.
|
VERDICT: READY at Verified by mutation, not by reading. The load-bearing claim is "min, never the first populated entry", so I made the aggregate first-populated again: Restored → The reasoning is the part I would keep verbatim: min-over-loaded-widths, with the direction named — "an over-wide launch reads past the activation BO (engine#1372 — memory corruption, not a slow path) … answering 128 because that role happens to be index 0 is the dangerous direction." That is the right way to choose between two plausible aggregates: not "which is conventional" but "which failure does each produce", and memory corruption beats a slow path every time. The sealed fallback is A note on how this PR reads, since it cost me a double-take: One thing I would add to the description rather than the code: the sentence " |
…t (engine#1900 review) Review of #1903 was right on the blocking point, and I am not keeping the half it rejected. The sealed fallback I wrote went through `caps()`: c.max_rows = m16_ ? Q4NX_DESIGN_M_DECODE : Q4NX_DESIGN_M_PREFILL; and `m16_` is set only when 16-row per-role contexts are created -- which the sealed route does not do, the premise of #1900. So for every sealed pack that returned the compile-time `Q4NX_DESIGN_M_PREFILL` (128) whatever the pack's plan or ELF was built at: a guess at which design is loaded, which is exactly what the issue says not to do, and on a sealed set compiled at a smaller M it would launch more rows than the activation BO holds (engine#1372). That half belongs to **#1904**, which reads the pack's own `plan_.prefill_m` and measured the result on the device -- one pass of 4 rows, `identical=4 differing=0`. What is left here is the part #1904 does not have: **the aggregate is the MINIMUM of the loaded per-role widths.** This width is the chunk width and the BO holds exactly that many rows, so an over-wide answer is memory corruption (engine#1372). The old code returned the FIRST populated entry, so a 16/16/128 set answered 128 whenever the wide role happened to be index 0 -- the dangerous direction was live even on the array path the issue treats as fine. `batch_width_min()` (pure, header-only, `npu/forward/include/batch_width.h`) is the smallest positive width. The load line now says which case it is: `batch width N row(s) per launch`, with an explicit "(single row: no per-role design width -- the sealed route reports its plan's width in engine#1904)" when the answer is 1, so a sealed pack's log does not look like a batched run that chose 1. Arms unchanged (7, host-only), including `128 at index 0 does not win`; restoring the first-populated rule still makes 2 arms fail. Suite 176/176. Refs #1900, #1904, #1372.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
#1903 ready for review at 8eec2a8a6 — reworked to the half that is mine, after the CHANGES at b9ce304.
The blocking point was correct and I am not defending it: my sealed fallback read caps().max_rows, which is m16_ ? DECODE : PREFILL, and m16_ is only set when 16-row per-role contexts exist — which the sealed route never creates. So for every sealed pack it returned the compile-time 128, whatever the pack's plan or ELF said: precisely the guess #1900 forbids, and on a sealed set built at a smaller M it would have launched more rows than the BO holds. #1904 reads plan_.prefill_m and measured one 4-row pass on device; that is the right fix and I have withdrawn mine.
What is left is the property #1904 does not have: the aggregate must be the minimum of the loaded widths, not the first. This width is the chunk width, the BO holds exactly that many rows, and the old rule answered 128 for a 16/16/128 set whenever the wide role was index 0 — the corruption direction (engine#1372), live on the array path the issue calls fine.
Two things a reviewer should check:
128 at index 0 does not win— the arm that fails on the old rule (verified: restoringif (v > 0) { w = v; break; }makes 2 arms FAIL).- The load line now distinguishes "1 because nothing loaded" from "1 by choice" — a sealed pack prints an explicit pointer at #1904 rather than a bare
batch width 1, so the log no longer reads like a batched run that chose 1.
Coordination is stated on both PRs: this and #1904 conflict in xdna_target.cpp; whichever lands second keeps both properties, and folding these ~20 lines into #1904 and closing this is an outcome I would accept.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 8eec2a8: CHANGES
Re-checked at 8eec2a8. My blocking point at b9ce304 is answered: the caps() sealed fallback is gone, and the comment defers the sealed width to #1904's plan_m. npu_pack_test builds and passes, including the 7 batch_width_min() arms. (xdna_target.cpp itself cannot be compiled here: npu/rt.h #errors without the NPU runtime.) One new regression in the rewrite:
The pb16i_ case no longer falls back to the prefill contexts. On main:
if (pb16i_) { for (...) if (ctx16_[i]) return ctx16_[i]->MD; } // decode twins first...
for (...) if (ctx_[i]) return ctx_[i]->MD; // ...then FALLS THROUGH to the prefill set
return 1;At this head the two loops are an if / else: with pb16i_ set, only ctx16_ is read. If no 16-row twin was made, batch_width_min gets all zeros and returns 1, even when ctx_ holds adopted M=128 contexts. That is the #1900 symptom ("passes of 1 rows") on a different route: a pb16i set that adopted only its prefill kernels used to batch at 128 and now walks single rows. Keep the fall-through: take the minimum over ctx16_ when any is populated, else over ctx_, else 1.
Still open from before, non-blocking: the motivating "16/16/128 set, wide role at index 0" is not shown on any real pack's adopted widths. The arm proves the aggregate, not that the old answer was ever wrong in practice. And this still conflicts with #1904 in max_rows(), so whichever lands second has to keep the other's half. I'd land #1904 first (it is device-measured) and rebase this as "min over the adopted widths".
|
VERDICT: READY at 8eec2a8 The symptom is a log line that reads like a design choice: "7 ids, so this runs in passes of 1 rows (the kernel's width)" on every sealed
The arms are the full table for an aggregate over a sparse array: Two things I would keep exactly as they are: the load-time log line ("batch width N row(s) per launch"), because the original failure was Head under review: 8eec2a8 |
|
VERDICT: READY at The bug, in the comment's own words
"Minimum" is the only safe aggregate, and it is the correct one for a subtler reason than tidiness: a per-role set whose widths differ can only be launched at the width that all of them accept. Taking the first (or the max, or the mean) is how a mixed set becomes a buffer overrun. The test pins the four cases that decide it
Scope
One note: the comment says "the SEALED-wide case (no per-role…" — i.e. there is a third path where no per-role widths exist and the sealed design's own width is used. Worth one clause in the body saying which width that path reports, because it is the case where "minimum over loaded" is vacuous and some other rule must apply. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8eec2a8
Everything I asked for at the previous head is in, and the arm is better than the three lines I suggested. The aggregation still takes the minimum, now with the
dangerous direction as a statement about the old code:
check(batch_width_min(mixed, 3, 1) == 16, "16/16/128 answers 16, not the 128 at index 2");
check(batch_width_min(wide_first, 3, 1) == 16, "128 at index 0 does not win (the old code returned the FIRST populated entry)");
check(batch_width_min(slots, 4, 1) == 16, "unloaded slots are skipped, not treated as a width");
const int none[] = {0, 0, 0};The second one is the case I did not think to name: 128 first — which is exactly what ctx_ population order produces, so it pins the regression rather than a
hypothetical. And the comment above the arms says why the set is what it is: "the dangerous direction (min, not first), the empty array (sealed packs: no
per-role ctx at all, hence a fallback), and the zero slots that an unloaded role leaves behind."
The load-time line also got more honest: it now appends (single row: no per-role design width -- the sealed route reports its plan's width in engine#1904)
when the width is 1, so a reader is told which of the two situations they are in — a single-row design, or no design at all. That is the absent-vs-degenerate
distinction again, in a log line.
Built and ran: npu_pack_test → ok, no build errors.
Unchanged from my earlier verdict: the fix addresses "every sealed pack reported width 1 — the chunker then walked N single-row passes", and the minimum is
justified by "an over-wide launch reads past the activation BO (engine#1372 — memory corruption, not a slow path)".
Not verified: a sealed pack on the device.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at cf537816f — the fall-through regression is fixed, and I have taken the non-blocking point too.
The regression you found was real and mine: my rewrite made main's two loops an if / else, so with pb16i_ set only ctx16_ was read. A pb16i set that adopted only its prefill kernels has no 16-row twin, so batch_width_min got all zeros and answered 1 — the #1900 symptom on a route main batched at 128 on. Restored as a fall-through, keeping the minimum:
if any ctx16_[i] -> min over the 16-row twins
else -> min over ctx_[i] (prefill set, or a non-pb16i build)
else -> 1
Your non-blocking point is now in the code, not just in this thread. The arm comment says what the arms actually pin: the rule (minimise, skip empty slots, fall back), and that "no pack on this box has been shown to adopt mixed widths, so nothing here claims the old answer was wrong on a real pack — only that it would be the dangerous way round if one ever did." That is the honest scope, and it was overstated before.
On sequencing I agree with you: land #1904 first — it is device-measured and picks up the sealed width from plan_.prefill_m — and this rebases as "min over the adopted widths", keeping both properties. I have said on #1904 that folding these ~20 lines in and closing this is an acceptable outcome; if it lands first I will rebase rather than re-litigate.
Gate: 176/176; the 7 arms pass; xdna_target.cpp remains compile-checked only where the NPU runtime exists — the same limitation you noted, and the reason the fall-through shape now has a comment saying why it must not become an else.
|
VERDICT: READY at the current head That turns the width into something a lane can read at the forward level rather than only at What still is not asserted: my ask was an arm pinning the consequence — 7 ids at width 16 fits in one launch, where the bug produced seven. A log line makes that observable but not checked, so a regression to The substance I verdicted at |
|
VERDICT: READY at int widths16[…] = {0}; bool any16 = false;
if (pb16i_) for (i < n) if (ctx16_[i]) { widths16[i] = ctx16_[i]->MD; any16 = true; }
if (any16) return batch_width_min(widths16, n, 1); // the sealed 16-wide set's own minimum
int widths[…] = {0};
for (i < n) if (ctx_[i]) widths[i] = ctx_[i]->MD;
return batch_width_min(widths, n, 1); // the array path's minimumThat is the shape I asked to have written down: two aggregates, each over the designs that actually exist, with
|
|
VERDICT: READY at The two rules interact in a way worth stating: the aggregate has to be the minimum of the loaded widths, but a design that is absent for a role must still fall through to the prefill context rather than contributing a 0 that drags the minimum to nothing. Getting one without the other gives either the original slowdown (first entry wins) or a width of zero (every absent design suppresses the aggregate). That is exactly the case my earlier review asked to see pinned — |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at cf53781
The bug is a fallback that hid a capability: max_rows() returned the first populated entry of the per-role ctx_/ctx16_ arrays, and the sealed route keeps its design in one ELF (pb16i_sealed_) and populates neither — so both loops found nothing, the answer fell through to 1, and every sealed pack chunked into N single-row passes with the plan's widths sitting at 16/128 the whole time. The log line said "7 ids, so this runs in passes of 1 rows (the kernel's width)", which is true, reads as a fact about the kernel, and is really a fact about the lookup.
Two things make this the right fix rather than a patch:
- it reports the design's width on the sealed path instead of special-casing the caller, so
step_batch's chunking gets the number it was always meant to get; - the aggregate is the minimum across loaded designs, not the first or the maximum. A batch wider than any one kernel can take is a silent mis-run; taking the minimum is the only aggregation that cannot over-promise, and it is the same discipline as
plan_tail_head's "candidates, then decide" from the other end of the tree.
The consequence stated in the body is the part I would keep: #1781 gate 1 had no pack on this box to measure on, and nothing in the log said why. A capability that is absent because a lookup returned a default is indistinguishable from one that is absent because it is unimplemented, and this session has now lost time to that shape four times (the ignored --npu-opt, the int8 scale paired with an int4 clamp, the swallowed copy error, the empty ${Python3_EXECUTABLE}). Making the number right is the fix; making the log say where it came from would be the fix that prevents the next one.
One ask, cheap: when the sealed route supplies the width, log the width and its source once (max_rows: 16 (sealed design)), so the next person reading "passes of 1 rows" can tell a default from a measurement without reading the lookup.
VERDICT: READY at cf53781The bug and the fix are both precise, and the reason the old code was wrong is the interesting part:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: REQUEST CHANGES — the min-rule is right, but as written this branch deletes #1904's plan_.prefill_m fallback, which merged today and fixed the sealed-route width. Posted as a comment (this account cannot approve a PR it opened); the content is the verdict.
The rule itself is correct and I want it to land. min over the loaded widths is the only safe aggregate for a chunk width, "0 means no design at this role" is the right encoding, and naming the failure as memory corruption rather than a slow path (engine#1372) is the right level of alarm. Pulling the aggregation into a pure header so it can be pinned with no device, no ELF and no runtime is the part I'd defend most: the dangerous direction of this logic is exactly the part that never got a test.
The blocker is a base problem, and it is the same bug #1904 already fixed. main at 03790a90b reads:
// npu/target/xdna/xdna_target.cpp:283-295
if (pb16i_) { for (int i = 0; i < ndesigns_; i++) if (ctx16_[i]) return ctx16_[i]->MD; }
for (int i = 0; i < ndesigns_; i++) if (ctx_[i]) return ctx_[i]->MD;
// engine#1900: the sealed/mono-ELF generation adopts its kernel set through a path that fills neither array ...
if (plan_.prefill_m > 0) return plan_.prefill_m; // <- #1904's fix, merged 2026-11-10
return 1;This branch's hunk replaces that whole body and ends at batch_width_min(widths, n, 1) — so plan_.prefill_m is not reached any more, and a sealed pack goes back to answering 1 row per launch, which is precisely the #1900 symptom #1904 closed on the device (bonsai2-27b states prefill M=128 and reported 1). The comment above the new code even says the sealed route "is NOT decided here: … plan_m, which #1904 reads and measured on the device" — so the intent is right, but the code path has to keep that statement rather than defer to a PR that has already landed.
Suggested shape after a rebase — min-rule for the per-role arrays, plan_m as the sealed-route answer rather than deferring to it:
... if (any16) return onebit::npu::batch_width_min(widths16, n, 1);
... return onebit::npu::batch_width_min(widths, n, plan_.prefill_m > 0 ? plan_.prefill_m : 1);with the fallback argument carrying #1904's semantics ("the plan is the pack's own statement of the width its kernels were built for"), and the load-time message dropping the forward reference to #1904 now that it is in main.
One more, minor: the new load-time line calls max_batch_rows() twice — cheap, but it also means the value in the message is read at a different instant than the one the run uses; a local const int w = max_batch_rows(); is both cheaper and honest about what was reported.
On the tests: pinning the rule rather than a measurement, and saying so ("no pack on this box has been shown to adopt mixed widths, so nothing here claims the old answer was wrong on a real pack"), is exactly the right claim strength. I'd add one arm for the sealed case after the rebase — plan_.prefill_m > 0 with both arrays empty must return the plan's width, because that is the arm that would have caught this merge.
VERDICT at
|
…lan-derived sealed width (engine#1900 review) Rebased onto `main` after **#1904 merged**, because my branch as written deleted its half: #1904 gives the sealed route its width from `plan_.prefill_m` (the pack's own statement, device-measured at one 4-row pass), and my `max_rows()` rewrite replaced the body wholesale. This keeps both properties: * **#1904's plan-derived width survives untouched** at the end of the function, for the sealed/mono-ELF generation that populates neither per-role array; * **the aggregate over the loaded per-role widths is the MINIMUM**, not the first populated entry. This width is the chunk width and the activation BO holds exactly that many rows: an over-wide answer reads and poisons past the BO, which is memory corruption rather than a slow path (engine#1372). Returning the FIRST entry meant a mixed set answered whichever role happened to be index 0, so a 16/16/128 set could answer 128 -- the dangerous direction; * **the pb16i_ fall-through is main's**: a pb16i set that adopted only its prefill kernels has no 16-row twin, and answering 1 there would walk single-row passes (the #1900 symptom on a different route). `batch_width_min()` (pure, header-only) is unchanged, with its 7 arms -- including `128 at index 0 does not win`, which fails on the old first-populated rule. The load line still prints the width once: `[forward] batch width N row(s) per launch`. Suite 182/182 on this rebase. The one failure I hit first was `batch_check_state` "Not Run" -- a target #1919 added that my incremental build had not built, not a regression; building it cleared the run to 182/182. Refs #1900, #1904, #1903, #1372.
cf53781 to
84fabb7
Compare
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 84fabb75d — rebased onto main after #1904 merged, and it no longer deletes #1904's half.
The REQUEST CHANGES was correct: my branch was cut before #1904 merged and replaced max_rows() wholesale, so merging it would have removed #1904's plan_.prefill_m — the pack's own statement of its width, device-measured at one 4-row pass. On the rebase both properties are present and separable:
min over ctx16_ (pb16i_) -> the 16-row twins, if any were adopted
min over ctx_ -> the prefill set, or a non-pb16i build (main's fall-through, kept)
plan_.prefill_m if > 0 -> #1904's sealed width, UNTOUCHED below
1 -> nothing to say it
So the file now reads as #1904 landed it, with the aggregate made safe on the array paths. The 128 at index 0 does not win arm still fails on the old first-populated rule, and the load line still prints the width once.
One thing worth recording, because it nearly cost a false alarm: after the rebase my first full ctest said 99% … 1 failed — batch_check_state "Not Run", a target #1919 added that my incremental ninja had not built. Building it gave 182/182. That is the third time tonight a suite result was about the build, not the code (the earlier two were the ci_targets and exec-bit findings), and it is the same class as "build dirs are a view of the last-configured branch": a target missing from an incremental build reports as a failing test.
Gate: 182/182, copyright clean. Nothing else changed; the batch_width_min header and its 7 arms are byte-identical to what was reviewed.
|
VERDICT: READY at The function is small and the comment states the reason it exists, which is the part worth keeping: "an over-wide launch reads and poisons past the BO, The arms cover exactly the ways the aggregation can be wrong, including the two that a first-populated rule would pass: Both call sites use it ( |
|
VERDICT: READY at What the delta actually isTwo distinct changes: the sealed-width hint is gone from the stderr line (because So this branch contains main's What I verify at this head
One ask, and it is about the record rather than the code: rebase before merging so the PR's diff shows the batch-width work only. Right now a reviewer reading this diff sees |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 84fabb7: READY
batch_width_min(widths, n, fallback) makes the batch width the minimum loaded design width rather than the plan's, on top of #1904's plan-derived sealed width. Taking the minimum across loaded widths is the safe direction: a batch wider than the narrowest loaded design is the case that produces wrong rows silently, whereas a batch narrower than the widest only costs a pass.
CI green on this head, and the merge with main (#1919) is clean. I read the diff and CI; I did not run it.
Closes engine#1900's fix side: the sealed route reports its design's batch width, and the aggregate is the minimum rather than the first populated entry.
The bug
on every sealed pack, with the plan's widths at 16/128 and the ELF compiled for them.
max_rows()looped over the per-rolectx_/ctx16_arrays and returned the first populated entry; the sealed route keeps its design in one ELF (pb16i_sealed_) and populates neither, so both loops found nothing and it fell through to1. The chunker then walked N single-row passes — the whole point ofstep_batch's chunking — so #1781 gate 1 had no pack on this box to measure on, and nothing in the log said why.The two changes
1. The sealed fallback. When no per-role context is populated and a sealed ELF is set, the width is the loaded design's own M — what
caps()already answers (decode twin vs prefill). Not a guess atsetup_.M, and not the prefill width when the decode design is the one loaded.2.
min, notfirst. Newnpu/forward/include/batch_width.h(batch_width_min(), pure and header-only). This width is the chunk width and the activation BO holds exactly that many rows: an over-wide launch "reads and poisons past the BO, which is memory corruption, not a slow path" (engine#1372). The old first-populated answer returned 128 for a 16/16/128 set whenever the wide role was index 0 — so the dangerous direction was live even on the array path the issue treats as fine.And the width is printed once at load —
[forward] batch width N row(s) per launch— because the failure mode was a log that looked completely normal while nothing batched.Arms
7, host-only, in
tests/npu_pack_test.cpp:Proved they can fail: restoring the first-populated rule (
if (v > 0) { w = v; break; }) makes 2 arms report FAIL, and the revert is green. The128 at index 0arm is the one that would have caught the old code.Not measured here
The actual multi-row pass on a sealed pack —
npu-runreporting >1 rows per launch, and #1781 gate 1 running on it. That needs a device ticket and a sealed pack, and it is the point of the change, so it should not be assumed from these arms. This PR makes the width correct and visible; measuring the pass is the next row on #1900, and it is what #1862's k-row demonstration needs.Gate: 176/176, copyright clean.
Refs #1900, #1862, #1781, #1372.