Repository navigation
perf(cuda-ep): make native eager decode beat ORT eager (DeepSeek +37.8%, crosses ORT +3.5%) - #1383
Conversation
…r decode beat ORT Native eager decode was ~25% behind ORT eager (DeepSeek-V2-Lite short-ctx: 65.17 vs ORT 86.78 tok/s) purely due to per-token host-dispatch overhead, not GPU compute (CUDA-graph capture already hid it). Each eager op ended with a redundant trailing stream drain (kernel->kernel ordering is guaranteed by the single in-order EP stream; dtoh/dtod self-synchronize), and rotary/gather/ structural/GatherElements issued eager-only 24-byte validation D2H readbacks that block the pipeline. Add an env-gated (ONNX_GENAI_DEFER_EAGER_SYNC, default OFF) eager-fast path: - runtime: public synchronize() becomes a no-op when deferred; new private force_synchronize() keeps the real drain for dtoh/dtod host reads. - kernels: skip eager-only index/position validation D2H when deferred (these are numerically inert; the captured path uses the device error latch). DeepSeek eager 65.17 -> 89.80 tok/s (+37.8%), crossing ORT eager by +3.5%. GLM-4-9B dense eager 142.42 -> 145.31 (+2.0%). Graph=1 stays valid with the flag on (fallbacks=0). DeepSeek 24-tok golden lock passes BYTE-IDENTICAL with defer ON. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…atch) Flip defer_eager_sync to default ON so native eager decode is fast out of the box (65->91.6 tok/s on DeepSeek-V2-Lite, beating ORT eager 86.78) rather than only behind an opt-in flag. This makes eager consistent with the already-shipped captured (graph=1) production path, which likewise relies on the device error latch instead of per-op validation D2H. ONNX_GENAI_DEFER_EAGER_SYNC=0 (or false/off/no) remains an escape hatch that restores the old always-sync path. Re-validated with the flag UNSET (default ON): DeepSeek 24-tok golden lock BYTE-IDENTICAL; graph=1 stays valid (fallbacks=0); qwen2.5-0.5b dense logprobs byte-identical vs the old path; eager 91.64 tok/s. Escape hatch (=0) restores 66.06 tok/s old path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2166207 to
48cf0ab
Compare
Records the #1383 native-eager-beats-ORT-eager default-ON win (DeepSeek 91.64 vs ORT 86.78, +5.6%; GLM +2%) and the #1379 Bug-1 regression guards in now.md. Also flags the pre-#1383 eager-vs-eager table as stale for the eager column. Docs-only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`cargo fmt --all -- --check` also fails on main (f72700a): Diff in crates/onnx-runtime-ep-cuda/src/runtime.rs:455 The `defer_eager_sync` initialiser added in #1383 (4e2a8b2) is one char over rustfmt's budget. Attributed with `git blame -L 450,458`. Folded into this PR rather than shipped separately because fmt and the cross-compile guard are two steps of the *same* required `Rust quality` job: a PR fixing only one of them still leaves that lane red, so neither could reach a green run on its own. This is the mechanical `cargo fmt --all` output, one file, one hunk, no behaviour change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…de (+35% eager tok/s) (#1427) ## Summary Raises the native **eager** decode "launch-bound floor" by removing the dominant per-token blocking scalar D2H. On clean dense GQA exports native eager sat at ~300 tok/s vs ORT eager 475; this lever lifts it to ~403 tok/s short-ctx (**+35.1%**) / ~367 deep-ctx (**+43.6%**), byte-identical. ## Root cause The floor is **not** the GQA kernel readback (that is already skipped in steady-state eager by the kernel's `warmed_signature` self-warming). The real cost is the **executor's dynamic-shape resolver**: `resolve_node_outputs` → `dynamic_output_shapes` reads GroupQueryAttention inputs 5 (`seqlens_k`) and 6 (`total_sequence_length`) back to host **per layer** (~56 blocking 4-byte D2H/token) to size the present-KV outputs. Each forces a host stall on the prior kernel. ## Fix (executor-only, 2 files) - **`dynamic_shapes.rs`** (GQA branch): `present_sequence` falls back to `past_key[2]` when `total_sequence_length` is absent. Native binds present-KV aliased in-place to past-KV at fixed physical capacity (`kernel_input_uses_physical_capacity`), so `present == max(past_key[2], total) == past_key[2]` always — host-known from the input shape. - **`dispatch.rs`** (`resolve_node_outputs`): skip materializing GQA integer shape-inputs **only when** the present-KV output (`outputs[1]`) is externally capacity-bound (`external.outputs`). A growing `past ⧺ current` GQA keeps the readback → fail-closed, correctness + generality preserved. `seqlens_k` is never used in GQA shape inference (pure waste). ## Generality Keys ONLY on structural shape conditions (GQA op + capacity-bound present output). **No model-family / head-size / head-count branch.** Lifts qwen / llama / glm / phi / deepseek eager decode uniformly (RULES.md §2). ## Escape hatch `ONNX_GENAI_GQA_SHAPE_ONDEVICE` (default-on; disable via `0`/`false`/`off`/`no`), mirroring #1383's `ONNX_GENAI_DEFER_EAGER_SYNC`. ## Measurements Export `deepseek-r1-distill-qwen-1.5b-int4-cuda` (1536h/28L GQA), H200 pinned idle, `ONNX_GENAI_CUDA_GRAPH=0` eager, greedy, medians-of-5, `profile_native --steady`: | ctx | ON (new default) | OFF (`=0`, old path) | delta | shape D2H/step | |-----|-----------------:|---------------------:|------:|---------------:| | short | **402.85 tok/s** | 298.12 | **+35.1%** | 1392→104 (13×) | | deep ~2600 | **367.31 tok/s** | 255.83 | **+43.6%** | — | vs ORT eager 475 (matched io-binding onnxruntime-genai harness, same export): native **0.85×** short — **did NOT cross 475** (honest gap −15% short / −23% deep). Residual is per-launch host dispatch: `cuLaunchKernel` unchanged at ~219/token — closing it needs kernel fusion / launch-batching, a separate larger lever. ## Correctness — golden locks (byte-identical to pre-change, default-ON, `--test-threads=1`) - DeepSeek-V2-Lite 24-tok lock (run ALONE, dodge #804): **PASS** (50.6s) - qwen3-0.6b Foundry golden: **PASS** (14.6s) - GLM-4-9B golden (block-128 GQA): **PASS** (22.2s) - Escape hatch `ONNX_GENAI_GQA_SHAPE_ONDEVICE=0` — qwen3 lock still **PASS** (old readback path restored) ## Notes⚠️ Do **not** self-merge — coordinator validation requested. Recommend coordinator independently re-run the DeepSeek golden lock before admin-merge. Read-only otherwise: no kernel edits, no unrelated files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Recovers coordinator `now.md` campaign-brain entries destroyed when an agent ran `git reset --hard origin/main` in the main checkout (uncommitted working-tree edits lost). Records the merged arc since #1383: **#1435** grid-fill narrow-N GEMV, **#1438** MAX_HEAD_DIM 256→512 (general), **#1442** gemma4-e2b dual head-size (256+512) e2e golden lock, **#1444** qwen3.5-2b-text context-scaling moat lock (3.03× deep), **#1445** capability-driven RMSNorm-fold gate + banked GLM structural verdict. Also updates the moat table (qwen3.5-2b recharacterized as a context-SCALING graph-block moat, not a fixed 1.65×) and the GLM verdict (structurally ORT-ahead at depth; stop forcing GLM levers). Doc-only. Co-authored-by: Copilot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Flagging a side effect of this change that surfaced while investigating an unrelated crash. Information only — the eager-decode win here is not in question, and I have not touched anything in this PR.
Those call sites never asserted the requirement explicitly; they inherited it from the general-purpose primitive. So this reads to me as a shared-primitive contract change reaching a distant caller, not a defect in the optimisation itself. Evidence (RTX 4060 Laptop 8 GB, CUDA 13.1,
The fault also moves between calls across runs — including Worth knowing beyond the crash: with stable-slot weights (#716) the VA is retained and physical granules are remapped under it, so a late kernel reading that VA after a remap would read the successor weight's bytes rather than faulting — wrong numbers, no crash. That path is inferred from the mapping mechanism, not directly observed as a wrong-number event, but it means weight-lending measurements taken with deferral on may be unreliable. (I audited our own benchmark docs against the build boundary; none of ours are affected — they predate Full chain, including the exoneration of #1325 which was the first suspect: #1439. A candidate fix is proposed there — draining unconditionally at the five pre-unmap sites via a dedicated |
|
Correction to my earlier comment — I overstated the severity and the follow-up measurement refutes it. I wrote that a late kernel reading a retained VA after a remap "would read the successor weight's bytes rather than faulting — wrong numbers, no crash", labelled as inferred from the #716 mapping mechanism. That inference is wrong, and a deliberate attempt to reproduce it failed. Measured (RTX 4060,
Why the silent path does not exist here, from the code rather than from the run: stable slots are keyed by weight So the accurate severity is: a use-after-unmap that crashes loudly, with a byte-identical workaround — not "results on this path may be silently wrong". That is materially less alarming than what I said, and the correction is on #1439 as well. The bound on this conclusion, stated honestly: two shipped configs were exercised (size-blind and byte-aware); every default-off diagnostic knob was not swept, so it holds for reachable shipped behaviour rather than universally. Nothing else changes — #1455 remains available if you want it, |
🔴 Benchmark Regression DetectedComparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).
Visual flags: Host infoWhat this cannot catch
|
… path was falsified The doc-comment stated as fact that under stable-slot reuse (#716) a late read "silently returns the successor weight's bytes rather than faulting". That is the claim our 11/11 experiment refuted, and it was corrected on #1439/#1383. Leaving it in a comment would outlive the discussion and be read as settled fact. Restate what was actually established: - eliding the pre-unmap drain is a use-after-unmap, observed as CUDA_ERROR_ILLEGAL_ADDRESS (11/11 runs on the offload repro); - the silent successor-weight variant was hypothesised and FALSIFIED — stable slots are per-key and admission refuses rather than remaps on collision, so a stale read faults or returns the same weight's byte-identical bytes, never a different weight (11/11 crashed, 0 diverged); - reference #1439 for the full chain. Comment-only change; no behaviour change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Records Wallace's dense eager-vs-ORT-eager generality pass. Defer-sync lever generalizes; pure-eager mixed on dense; production auto-capture path wins everywhere. Docs-only. Co-authored-by: Copilot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… crash/silent corruption (#1439) (#1455) ## Summary Fixes the reproducible `CUDA_ERROR_ILLEGAL_ADDRESS` crash in the VMM weight-lending page-in path (#1439), and the silent weight-corruption it can cause. **Root cause.** Three shipped-path barriers in `weight_paging.rs` — the VMM release/evict free (`release_allocation`), admission eviction, and `admit()` eviction — drain the compute stream via `CudaRuntime::synchronize()` immediately before `cuMemUnmap`ing an evicted weight granule, so no in-flight decode kernel is still reading it. **#1383 (`4e2a8b2e`) changed that primitive's contract**: `synchronize()` became a **no-op** whenever `defer_eager_sync` is set, which is **on by default**. The pre-unmap drains are therefore silently elided, and eviction unmaps a granule while a decode kernel is still reading it — a **use-after-unmap**. Because the fault is an *asynchronous* out-of-bounds access, it surfaces later, at the first synchronising call — which is why the crash appears at varying, pointer-free sites such as `cuEventCreate(start)`. Worse, under stable-slot VA remapping (#716) the late read can land inside the *successor* weight's freshly-mapped physical granule and **silently return the wrong bytes instead of faulting**. **Why the fix is here and not in #1383.** #1383's optimisation is correct — eliding a *trailing per-op* eager sync is safe on a single in-order stream. What was wrong is that five call sites relied on `synchronize()` as an *unconditional pre-unmap barrier*, an assumption they never asserted. This PR makes that requirement explicit and local: it does **not** touch `defer_eager_sync` or anything in #1383. ## Change - Add `CudaRuntime::drain_for_unmap()` — an unconditional compute-stream drain that ignores the eager-sync deferral, documented as a correctness barrier. - Replace every explicit `self.runtime.synchronize()` barrier in `weight_paging.rs` with `drain_for_unmap()` (3 shipped-path pre-free sites + 3 default-off diagnostics: #945 pin-refill, #888 sync-before-fill, #945 pin-checksum). The 4 `copy_stream().synchronize()` calls are left untouched — they go direct and were never deferred. `+24 / -6` across 2 files. ## Verification (RTX 4060 8 GB, `qwen2.5-14b-onnx`, `ONNX_GENAI_VRAM_LIMIT=6GiB`, weight offload, `defer_eager_sync` **default-on**, same source base) Both directions, built from this exact source with only the fix stashed/unstashed between them: | Binary | Result | |---|---| | **unfixed control** | `CUDA_ERROR_ILLEGAL_ADDRESS` at `cuEventCreate(start)`, EXIT 1, ~90 s | | **fixed (this PR)** | 12 tokens, **byte-identical** to known-good `[96347, 3375, 724, 11, 358, 2776, 14589, 311, 6723, 429, 498, 3003]`, EXIT 0 | ## Cost The added drains fire **only when the page cache evicts/frees**. A model that fits in its VRAM budget evicts nothing and adds **zero** drains, and #1383's per-op eager win is untouched by construction (no decode-path per-op sync was changed). In the deliberately pathological offload-stress config above (4615 evictions, 6010 page-ins, 59.8 GB H2D over 12 tokens), total barrier time was `admit_sync 98.8 ms + vram_free_sync 7941 ms ≈ 8.0 s` over a **1085 s** run — **~0.7%**, dwarfed by the paging it protects. (`vram_free_sync_ms` is the pre-`cuMemUnmap` drain bracket, separate from the bookkeeping-dominated `vram_free_ms`; see #1439 / the vram-free attribution note.) ## Merge intent **Do not merge on my authority** — #1383 is not my change. This is a ready, verified, reversible fix posted so whoever owns #1383 can merge in one click or decide on an alternative. Until it lands, the workaround on #1439 is `ONNX_GENAI_DEFER_EAGER_SYNC=0` (byte-identical to known-good). Fixes #1439. --------- Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
Sibling to §40, which I added earlier today. Where §40 covers **thresholds** that were correct in the regime they were calibrated in, this covers a **shared primitive changing contract** while distant callers break — because they depended on the old behaviour without ever asserting it. Two instances landed within a day of each other and produced five downstream failures between them. **#1383 made `CudaRuntime::synchronize()` a no-op** under `defer_eager_sync`. That is correct for its purpose: eliding a *trailing per-op* eager sync is safe, since the single in-order EP stream already preserves kernel-to-kernel ordering. But two other sets of callers used the same function for something else: - **Six pre-`cuMemUnmap` barriers in `weight_paging.rs`** needed a real drain. Without it, a granule is unmapped while an in-flight decode kernel still references its VA — use-after-unmap, `CUDA_ERROR_ILLEGAL_ADDRESS` on 11/11 runs of the weight-offload repro (#1439). Fixed in #1455 by giving those sites a dedicated `drain_for_unmap()` that *states* the requirement rather than inheriting it. - **Two `graph::tests` capture tests** assert `synchronize()` **errors** inside an active capture. With the no-op they get `Ok`, so they are red on default `main` and pass with `ONNX_GENAI_DEFER_EAGER_SYNC=0` — stale assertions against a changed contract, not a code defect. **Marlin M>1 default-on** changed what the M>1 decode path *is*, staling the `m == 1` capture-safe invariant (#1405) and two bit-exactness assertions that pass only with `ONNX_GENAI_MARLIN_M_GT_1=0`. **Neither change was wrong.** Both were measured, deliberate, and beneficial where their author was working. The cost came from **implicit dependence** — `weight_paging.rs` never said "I need a real drain here", it called the function that happened to provide one. And the failures surface in a different crate from the change, which is why every one of these was found while investigating something else: a crash in a paging path does not look like a decode optimisation. **Practice suggested**: name the guarantee at the call site when you depend on a specific behaviour of a general-purpose primitive (#1455's `drain_for_unmap()` is the shape); grep call sites across crates when changing one; and mark assertions that encode a *contract* rather than a *result*, so a stale expectation stays distinguishable from a real regression. That last distinction matters — updating a stale assertion is correct, weakening a live one to get green is how a real bug ships. All claims verified against `main` before writing: six `drain_for_unmap` call sites in `weight_paging.rs`, the two `synchronize().is_err()` assertions at `graph.rs:566` and `:843`, and `marlin_m_gt_1_enabled()` defaulting on. Docs-only. Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
…tch/teardown drains (#1788) ## Summary Working as Sebastian (Performance Engineer). Follow-up to #1777 for issue #82: investigates and resolves `QMoEKernel::execute()`'s unconditional eager-path `runtime.synchronize()` call. **Headline correction to the record**: `CudaRuntime::synchronize()` has been a deferred no-op by default since #1383 (merged before #1777 was authored), gated by `defer_eager_sync`. This means the trailing sync in `execute()` was **already inert in production** before this PR. An A/B test (remove the call vs. keep it) shows **zero measurable performance difference** — confirming this was never a live blocking point in the default configuration. #1777's causal claim that this call caused `host_us≈median_us` convergence for grouped M>1 shapes is falsified by this A/B test. **The real invariants** that call accidentally protected (only in configurations where the debug flag forces synchronous behavior, or in any future default change) are now handled explicitly with the runtime's existing unconditional-barrier primitive (`drain_for_unmap`, already used by `elementwise.rs`'s `BroadcastMetadataCache`) rather than a second synchronization authority: - `ScratchPool::ensure()`: drains before freeing a growing slot's old pointer (order fixed so a drain failure can't leak the fresh allocation — mirrors the `elementwise.rs` precedent). - `Drop for QMoEKernel`: best-effort drain before freeing scratch at teardown. `self.runtime.synchronize()` itself is **kept, not deleted**, at the end of `execute()`: it's a no-op by default, but keeping it preserves the `ONNX_GENAI_DEFER_EAGER_SYNC=0` debug escape hatch for QMoE, consistent with every other kernel in this EP (e.g. `MatMulNBitsKernel::run`). **A second, related bug** was found and fixed in the #1777 benchmark harness itself (`qmoe_gpu.rs`): its own inter-rep/post-warmup `runtime.synchronize()` calls were *also* no-ops, letting consecutive reps' async-enqueued kernels pile up in the CUDA launch queue until `cuLaunchKernel` itself blocked on a full queue. This queue-saturation effect — not a QMoE sync defect — is the actual mechanism behind #1777's `host_us≈median_us` observation for grouped shapes. Fixed by using `drain_for_unmap` (a real barrier) between reps/after warm-up. Stale doc comments describing the original (falsified) theory are corrected. ## Measurements (idle A100, `CUDA_VISIBLE_DEVICES=1`, merged #1777 probe, 25 reps) | shape | M | median_us (GPU) before | median_us after | host_us before | host_us after | |---|---|---|---|---|---| | deepseek-v2-lite | 2 | 674.6 | 674.9/674.6 | ~672 | 60–66 | | deepseek-v2-lite | 8 | 3308–3318 | unchanged | ~3300 | 60–66 | | glm-5.2 | 8 | 7150–7157 | unchanged | ~7150 | 60–67 | 3 independent "after" runs are consistent within noise; `median_us` (true GPU event time) is unchanged across all runs — confirming this is a pure measurement-methodology fix to the harness, not a compute-time change. No tok/s claims are made. ## Tests New regression tests in `qmoe_gpu.rs` (in the same commit): - `qmoe_scratch_pool_regrows_and_shrinks_across_calls_matches_cpu`: one long-lived kernel instance cycled through growing/shrinking row counts (`[1,8,8,2,8,4,16,1,16,2]`), with consecutive calls enqueued back-to-back (no intervening drain) so a missing `drain_for_unmap` in the growth path has a genuine chance to corrupt data; every step checked against the CPU oracle. - `qmoe_execute_does_not_block_host_for_device_completion`: asserts host-side `execute()` return time stays a small fraction of measured GPU completion time; fails if an unconditional blocking sync is reintroduced. - `qmoe_drop_after_inflight_launch_leaves_runtime_usable`: drops a kernel immediately after an async call with no readback, then verifies the runtime/stream remain healthy for independent subsequent work (both a fresh QMoE kernel and a plain htod/dtoh round trip). **Verified on idle A100 (device 1)**: `cargo test -p onnx-runtime-ep-cuda --release --features gpu-tests --test qmoe_gpu` → **37/37 passed** (34 existing + 3 new). `cargo clippy -p onnx-runtime-ep-cuda --release --features gpu-tests --test qmoe_gpu -- -D warnings` and `cargo fmt --check` are clean. ## Review An independent review pass (code-review agent) found 3 substantive issues in an earlier revision of this diff, all fixed and confirmed resolved by a follow-up review pass before this commit: 1. Two of the three new regression tests forced a full stream drain (via `dtoh`) immediately after every call, making them structurally unable to expose the bugs they claimed to guard against — fixed by splitting "enqueue" from "read back" (`PendingExecution`/`enqueue_with_existing_kernel`) so consecutive calls genuinely overlap. 2. A potential device-memory leak on the `drain_for_unmap()` error path in `ScratchPool::ensure()` (alloc happened before the fallible drain) — fixed by reordering to drain-before-alloc, mirroring `elementwise.rs`'s existing precedent. 3. A doc comment inaccurately claimed parity with `MatMulNBitsKernel` while actually diverging from it (having fully removed the debug-escape-hatch-respecting sync call) — fixed by reinstating `self.runtime.synchronize()` and correcting the comment. ## Scope No paging/residency, per-expert-identity, or exporter changes are included in this PR (explicitly out of scope per issue #82's follow-up plan). ## Next gate for #82 With this merged, the remaining #82 dependency chain is: (1) this PR — QMoE `execute()` async-safety + benchmark methodology fix [this PR], (2) the batch-M≥2 capture-eligibility fix (still open — QMoE grouped paths remain capture-ineligible; the "repeat-launch + CUDA-event" pattern used by the #1777 probe is the correct proxy until captured replay is supported for M>1), then (3) wiring `BlockQuantizedMoE` paging+prefetch (explicitly deferred, not touched here). This PR does not change capture eligibility; it only fixes the async-safety and measurement-methodology issues blocking trustworthy baselines for that next step. Per explicit user directive, this PR proceeds on local A100 validation (34+3 targeted tests, 3 independent benchmark runs, clippy/fmt, independent review) without waiting on remote CI, which remains supplementary asynchronous evidence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Native eager decode was ~25% behind ORT eager on DeepSeek-V2-Lite short-ctx (66 vs ORT 86.78 tok/s) — a pure per-token host-dispatch deficit, not GPU compute (CUDA-graph capture already hid it: graph=1 ~143 tok/s). This PR closes and reverses that gap, ON by default.
Root cause
Two numerically-inert host serializers per op on the eager (
!capturing) branch:synchronize(). It buys nothing: the single in-order EP stream guarantees kernel to kernel ordering, anddtoh/dtodself-synchronize before their synchronous copy.Per-token cost (nsys): DeepSeek MoE ~1187 launches, 416 cuStreamSynchronize, 147 D2H/token; ~54% of eager wall is recoverable host overhead.
Fix (default ON;
ONNX_GENAI_DEFER_EAGER_SYNC=0escape hatch)runtime.rs:defer_eager_syncdefaults true unless the env is explicitly falsey (0/false/off/no). Publicsynchronize()no-ops when deferred; new privateforce_synchronize()keeps the real drain fordtoh/dtodhost reads (read correctness preserved).rotary_embedding.rs/gather.rs/structural.rs/indexing.rs: skip eager validation D2H when deferred.Default-ON makes eager consistent with the already-shipped captured path; the env var is a one-flag rollback for debugging.
Results (GPU3, greedy, medians-of-5, pinned idle GPU, flag UNSET = default ON)
=0)Validation (flag UNSET, default ON)
d2e4bc2e...).=0restores old path: yes (66.06 tok/s).Recommendation
Ship default-ON (this PR). Remaining headroom (DeepSeek eager 91.6 vs graph 143) is the ~1187 launches/token to kernel fusion, a separately-scoped follow-up.
Findings:
.squad/decisions/inbox/wallace-eager-launch-overhead.mdCo-authored-by: Copilot 223556219+Copilot@users.noreply.github.com