Repository navigation
npu: the batch width falls back to the plan's prefill M, so a sealed pack makes a multi-row pass at all (fixes #1900) - #1904
Conversation
…pack makes a multi-row pass at all (fixes #1900) `XdnaTarget::max_rows()` scanned the per-role `ctx_[]` / `ctx16_[]` arrays and returned the first populated `MD`, else `1`. The sealed/mono-ELF generation (`SEALED-*.elf`, `full_i8_*`) adopts its set through a path that fills **neither** array, so it fell through to `1` -- and that value is the **chunk width** in the forward (`const int blk = max_batch_rows(); if (M > blk) …`), so every batched pass on those packs was walked one row per launch whatever the plan said: ``` models/npu/bonsai2-27b-NPU-b8 plan prefill M=128 -> "this runs in passes of 1 rows" models/npu/bonsai2-27b-t2s2 plan prefill M=128 -> 1 row ~/scratch-packs/imp1519b plan prefill M=128 -> 1 row ``` The fallback is the plan's own `prefill_m` when no per-role context was adopted: the plan is the pack's statement of the width its kernels were built for, and this target already refuses a plan whose M disagrees with adopted kernels. Nothing that adopts a context changes -- the new branch is only reachable where the function returned `1` before -- and it still returns `1` when the plan states nothing, because guessing a width launches into a BO sized for fewer rows (engine#1372's memory-corruption path). ## Measured on the device, under an npu-hold ticket `models/npu/bonsai2-27b-NPU-b8`, sealed `kernels/` (14 ELFs), `--ids "1 2 3 4"`: | before | after | |---|---| | `passes of 1 rows`, 4 separate launches | **one pass of 4 rows** | | `[batchcheck] M=1 start=1 … MISMATCH` | **`[batchcheck] rows M=4 identical=4 differing=0`** | | 544 s, ticket timed out | **313 s, exit 0** | `identical=4 differing=0` is the row-identity datum **#1781 gate 1** asks for -- rows 2..k bit-identical to k sequential steps, in one launch -- and it had no pack to run on before this. The `M=1 start=1 MISMATCH` in the "before" column is the same bug seen from the oracle's side: with the width at 1 the ids are chunked, and for a chunk at `start > 0` the oracle `reset()`s the cache before its sequential replay, so it compares "one token at position 1 from scratch" against "position 1 continuing the previous chunk". The oracle is only meaningful for a single pass from 0, which is what the fix allows. Fixes #1900. Refs #1862, #1894, #1781, #1372.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at c653e94: READY
Checked at c653e94 (npu/target/xdna/xdna_target.cpp +8, Fixes #1900). xdna_target.cpp cannot be compiled on this box (npu/rt.h #errors without the NPU runtime), so this is a read plus the device measurement in the description. The change is one line, reachable only where max_rows() used to return 1 (no per-role context adopted). It takes the width from plan_.prefill_m, the pack's own statement, and keeps 1 when the plan states nothing. That's the right source. #1903's alternative, caps().max_rows, is the constant Q4NX_DESIGN_M_PREFILL on the sealed route, because m16_ is only set when per-role 16-row contexts are made. The npu-hold run on bonsai2-27b-NPU-b8 (one pass of 4 rows, [batchcheck] rows M=4 identical=4 differing=0, 544 s → 313 s) is the #1781 gate-1 datum the issue is about. Filing the oracle's start > 0 reset as its own issue is the right call.
Two notes, not blocking:
- The device run used 4 ids, below both 16 and 128, so it shows batching starts. It does not show that a pass is bounded at
prefill_m. A run with more ids than the design's M (say 160) would show the second block. - #1903 fixes the same #1900 and conflicts in this file. Its other half,
minrather than the first populated width (the chunk width is shared by every role), is worth landing separately on top of this.
|
VERDICT: READY at The mechanism is worth restating because it is a two-sided bug. The fix is the careful kind. Falling back to And the evidence is exactly what #1781 gate 1 wants: One ask: the new branch is described as "only reachable when no per-role context was adopted" — i.e. the two paths are mutually exclusive. Is that exclusivity pinned? If it is, say so (an arm Not run: I did not run the sealed pack or |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 7a0e0d5-equivalent head — complementary to #1903, not a duplicate
The fallback itself is right: if (plan_.prefill_m > 0) return plan_.prefill_m; — when the per-role arrays carry nothing, the plan's prefill M is the only figure the pack itself states, and it is a better answer than 1 (which forces every run into single-row passes). The defect statement matches: max_rows() scanned ctx_[]/ctx16_[] and returned the first populated MD, else 1, and a sealed/mono-ELF set adopts its designs through a path that leaves those arrays empty.
Two things worth saying because they will otherwise be discovered in review of the merge:
- this is complementary to #1903, not a duplicate of it — that PR changes the aggregation (minimum over the populated widths instead of the first) and adds a sealed-design fallback (
caps().max_rows); this adds the plan-prefill fallback. Both are needed, and they touch the same lines, so whichever lands second will conflict. - the precedence between the three sources should be stated once: the sealed design's own M, then the plan's prefill M, then
1. Without that order written down, the conflict resolution decides it by accident — and the difference between "the sealed design's M" and "the plan's prefill M" is exactly the case each of these two PRs was written for.
…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.
|
Offering you the one safety property my #1903 had that this PR does not — and withdrawing the rest of mine in your favour. My sealed fallback took the width from What is still worth taking from mine, in ~20 lines: the aggregate over the loaded per-role widths must be the minimum, not the first populated one, because this width is the chunk width and the BO holds exactly that many rows (engine#1372). The old code returned the first entry, so a 16/16/128 set answered 128 whenever the wide role was index 0. Your sealed path bypasses that array entirely, so it does not have the bug — but the array path still does, for any pack that populates mixed widths.
|
…, and names the positions (fixes #1905) (#1909) `NPU_BATCH_CHECK` ran a sequential replay over `start_pos .. start_pos+M-1` and then called `reset()`, so the batched pass that followed started from an EMPTY cache while the replay had started from the real prefix. For any chunk with `start_pos > 0` the oracle therefore compared "one token at position 1 from scratch" against "position 1 continuing the previous chunk" and reported MISMATCH — measured on `bonsai2-27b-NPU-b8`, width 1, four ids: [batchcheck] M=1 start=0 seq_argmax=198 batch_argmax=198 MATCH [batchcheck] M=1 start=1 seq_argmax=220 batch_argmax=653 MISMATCH The first row matching and every later row not is the signature of a state mismatch in the debug path, not of a row-dependence bug in the kernels -- and it sends the reader to the kernels. Latent until now because the packs on this box make a single pass (#1904); it goes live as soon as an M=128 prefill chunks a prompt, where every chunk after the first would "diverge". **The fix uses what the forward already has:** `snapshot_state(start_pos, snap)` before the replay and `restore_state(snap, start_pos)` after it -- the pair the prefix cache uses, whose plane-copy half (`q4nx_state_planes.h`) is device-free and unit-tested. `reset()` stays for the `start_pos == 0` case, where there is nothing to restore and it is exactly right. If the snapshot cannot be taken the oracle REFUSES with a message rather than comparing two different states again. **And the line names the positions compared:** `positions=%d..%d` instead of only `start=%d`, so the range is in the output rather than inferred from `start` and `M`. Co-authored-by: t <t@e.com>
… review, second pass) Second review was right again: my rewrite turned main's two loops into an `if / else`, so with `pb16i_` set only the `ctx16_` array 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 ("passes of 1 rows") on a different route, where main batched at 128. Main's shape is a fall-through, and this restores it while keeping the minimum: if any ctx16_[i] -> min over the 16-row twins (a pb16i set that made them) else -> min over ctx_[i] (the prefill set, or a non-pb16i build) else -> 1 The arm comment is also corrected on the reviewer's non-blocking point: these arms pin the RULE (minimise, skip empty slots, fall back), and 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. The sealed route's width is #1904's, read from the plan and measured on the device. Suite 176/176. Refs #1900, #1904, #1372.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at c653e94
This is the target-side half of #1903, and together they are the whole of #1900. The line lands after both adoption loops (ctx16_[], then ctx_[]), so it
is reachable only when no per-role context was adopted — "so nothing that has one changes" — which is exactly the sealed/mono-ELF case:
284 if (pb16i_) { for (…) if (ctx16_[i]) return ctx16_[i]->MD; }
285 for (…) if (ctx_[i]) return ctx_[i]->MD;
293 if (plan_.prefill_m > 0) return plan_.prefill_m;
The metric the symptom is stated in is the right one: this is not "the design width" in the abstract, it is "the CHUNK WIDTH (q4nx_forward's
blk = max_batch_rows())" — so a pack that batching must chunk at 1 row "batched ONE ROW per launch however wide their design is — models/npu/bonsai2-27b-NPU-b8
states prefill M=128 and reported 1." A named pack with a named stated width is what makes the bug concrete.
And the fallback does not weaken the existing guard: a plan whose M disagrees with the adopted kernels is still refused above (:206), so this only fills
the case where nothing was adopted at all. Still 1 when the plan states nothing, and the reason is the memory-corruption one: "guessing a width launches into
a BO sized for fewer rows, which is the memory-corruption path engine#1372 records." Refusing to guess is the right default.
Builds clean at this head (npu_pack_test, no errors).
Not verified: the bonsai2-27b pack run on the device; the fallback is arithmetic on the plan, which npu_pack_test's existing width arms now cover on the
forward side.
Ordering note for the merge lane: #1903 (forward side, MERGEABLE) and this (target side, MERGEABLE) are two halves of #1900 and neither conflicts with the
other; landing both is what turns the sealed width from 1 into the plan's M end to end.
…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.
…lan-derived sealed width (engine#1900 review) (#1903) 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. Co-authored-by: t <t@e.com>
XdnaTarget::max_rows()scanned the per-rolectx_[]/ctx16_[]arrays and returned the first populatedMD, else1. The sealed/mono-ELF generation (SEALED-*.elf,full_i8_*) adopts its set through a path that fills neither array, so it fell through to1— and that value is the chunk width in the forward (const int blk = max_batch_rows(); if (M > blk) …), so every batched pass on those packs was walked one row per launch whatever the plan said:The fallback is the plan's own
prefill_mwhen no per-role context was adopted: the plan is the pack's statement of the width its kernels were built for, and this target already refuses a plan whose M disagrees with adopted kernels. Nothing that adopts a context changes — the new branch is reachable only where the function returned1before — and it still returns1when the plan states nothing, because guessing a width launches into a BO sized for fewer rows (engine#1372's memory-corruption path).Measured on the device, under an npu-hold ticket
models/npu/bonsai2-27b-NPU-b8, sealedkernels/(14 ELFs),--ids "1 2 3 4",NPU_BATCH_CHECK=1:passes of 1 rows— 4 separate launches[batchcheck] M=1 start=1 … MISMATCH[batchcheck] rows M=4 identical=4 differing=0identical=4 differing=0is the row-identity datum #1781 gate 1 asks for — rows 2..k bit-identical to k sequential steps, in one launch — and before this there was no pack on the box that could produce it.The
M=1 start=1 MISMATCHin the "before" column is the same bug seen from the oracle's side, and it is worth knowing: with the width at 1 the ids are chunked, and for a chunk atstart > 0the oraclereset()s the cache before replaying sequentially, so it compares "one token at position 1 from scratch" against "position 1 continuing the previous chunk". The oracle is only meaningful for a single pass from 0 — which is exactly what the fix makes possible. I will file that as its own issue rather than change the debug path here.Fixes #1900. Refs #1862, #1894, #1781, #1372.