perf: Stage 2 — per-role GPU timing instrument, and the decode profile it produced - #4
Draft
ulises-c wants to merge 9 commits into
Draft
perf: Stage 2 — per-role GPU timing instrument, and the decode profile it produced#4ulises-c wants to merge 9 commits into
ulises-c wants to merge 9 commits into
Conversation
Stage 2A instrumentation. `TURBO_FIELDFARE_KERNEL_STATS=1` — and only that exact value — makes RealForwardRunner accumulate completed GPU spans by role: embed, attention_router, shared_expert, the optional moe_phase1_hit split, every routed_moe layer, and whichever of fused_head/logits_head ran. Aggregation is by role rather than per sample, so state is bounded by role count instead of token count, and it clears on reset() and on prompt-cache continuation so one generation never inherits another's spans. Routed, shared, and phase-one buffers are recorded in the common drain path after completion and error validation, so every layer is counted and no timestamp is read before its buffer completed. With the flag off the only added work is a branch at each completion point. This breaks open the `unaccounted (GPU waits)` line that TURBO_FIELDFARE_PHASES=1 already prints. Observation only: no scheduling, pipeline, or default changes.
Under TURBO_FIELDFARE_KERNEL_STATS=1 the CLI prints one deterministic role-sorted line per role plus a total after generation. It goes to stderr only, so generated stdout stays byte-identical to an uninstrumented run (verified against a Stage 1 baseline hash on the installed Gemma model). Per-token values divide by the captured fused/logits-head count, not by newTokens: the first generated token is seeded by prefill and has no decode command buffer to time, so newTokens overstates the denominator by one. It falls back to newTokens when no head role was captured. GPU spans are reported separately from the CPU phase footer — overlapping command buffers make their sum unsuitable as additive wall time. Also records the Stage 2A contract and the denominator rule in the plan.
Runs the new instrument on all three frozen real-generation-v1 prompts and writes down what it actually said, rather than what NVMAI's numbers predicted. Profile: expert I/O await is ~40% of decode and flat across prompt lengths (1222/1198/1192 ms), so it is a per-token fixed cost of pulling routed experts. The remaining ~55% `unaccounted (GPU waits)` is the part that grows with context, and the per-role report says why: attention_router goes 527 -> 684 -> 903 ms across the three prompts while every other role stays flat. Summed GPU spans sit well under decode wall time, so the gap is stall. First A/B: --expert-cache-slots 32 vs the default 16 is +1.93 tok/s (+4.3%), winning all four adjacent pairs with disjoint distributions. The gain is bought in expert I/O await (-19.7%) — the exact block the profile named — with about half refunded to `unaccounted` as decode stalls on the GPU instead, which is why it is +4.3% and not the ~7% the I/O delta suggests. The first attempt at this A/B is recorded too: arms run back to back, every run faster than the last regardless of arm, page-cache warming aliased onto run order. Redone interleaved with warmups discarded. rdadvise cut I/O await but returned all of it, and was only measured in the confounded pass, so it is logged as "no effect observed" rather than a settled negative. No defaults change: 16 slots stays the default, 32 stays opt-in.
Sync the decode-perf branch onto the latest fork-main (GUI model picker, railguard-ignore, 256K context-ladder docs). Clean auto-merge; only Run.swift needed content merging and it resolved automatically.
Scripts/check_app_version.rb fails when fallbackShortVersion falls more than one release behind the newest published drumih/turbo-fieldfare release. fork-main still pinned 0.6.0 (two behind 0.7.1); bump the constant so a clone build reports a current version and CI passes. Note: the fork will eventually carry a distinct version scheme from upstream; this is a stopgap to keep the check green.
Adds the Stage 2C measurement scripts (baseline profile, interleaved slot A/B with warmups discarded, peak-RSS check) and the raw jsonl they produced on an M5 Max 36 GB. Same matched-control treatment as the Gemma Stage 2B A/B: two warmups discarded, arms interleaved A/B, byte-identical stdout asserted per run.
+26.0% tok/s from --expert-cache-slots 32 on Qwen (vs +4.3% on Gemma), disjoint distributions, byte-identical output. Unlike Gemma there is no unaccounted refund: await -26.8% and unaccounted -17.6% both drop. Cost is +1.07 GiB peak RSS (+78%), so it stays opt-in. Closes the coverage caveat that Qwen was not installed.
Re-runs off vs adaptive after the Gemma pass was invalidated by run-order confounding. Two warmups discarded, four interleaved pairs, byte-identical stdout. Adaptive cuts expert I/O await 12.8% but raises unaccounted stall 18.1%, losing 10.4% throughput in every pair with disjoint distributions.
Upgrades the earlier order-confounded 'no effect observed' to a clean, matched-control result: adaptive is -10.4% tok/s vs off. The hints trade I/O wait for more GPU stall, so off stays correctly default.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stage 2 of the Qwen 3.6 perf work. Two things land here: an opt-in per-role GPU
timing instrument, and the first real measurements taken with it on this host.
Stage 2 was originally planned as "cherry-pick NVMAI's individually-measured
perf commits." That premise did not survive contact — their headline
optimization (
4beb74f, parallel expert pread fills) is already in our treeat
PreadExpertStreamer.executeExpertCachePlan, and the profiler-bug fix927c94erepairs an instrument we never had. Their percentages are measuredagainst a serial baseline we no longer have, so they are not transferable. The
rule adopted, and applied here: measure first, never carry over a reported
percentage as an expected result.
What's in it
a73c25bf2a8e6cda7d816docs/STAGE2_PLAN.mdTURBO_FIELDFARE_KERNEL_STATS=1— and only that exact value — enablescollection. Roles:
embed,attention_router,shared_expert, the optionalmoe_phase1_hitsplit, everyrouted_moelayer, and whichever offused_head/logits_headran.Design points worth a reviewer's attention:
per command buffer, so storage is bounded by role count rather than token
count or runner lifetime. Cleared on
reset()and on prompt-cachecontinuation, so one generation never inherits another's spans.
buffers are recorded in the common drain path after completion and error
validation, so every layer is counted and no timestamp is read early.
completion point. No scheduling, pipeline, or default changes anywhere.
The profile
M5 Max, 36 GB, macOS 26.6.2, Swift 6.3.3, release build, Gemma 4
scratch/gemma4.gturbo. All three frozenreal-generation-v1prompts,--max-new 128 --max-context 4096 --temperature 0.2 --top-k 64 --top-p 0.95 --seed 20260721.Two separable blocks, and the per-role split is what separates them:
(1222/1198/1192 ms) — a per-token fixed cost of pulling routed experts, not a
context effect.
unaccounted (GPU waits)is the other ~55 % and it is the part that growswith context (1692 → 2059 ms).
attention_routeralone accounts for it,going 527 → 903 ms while every other role stays flat.
Summed GPU spans (1382 ms) sit well below decode wall time (3016 ms) even though
the roles cover the whole forward pass — command buffers overlap, so the gap is
stall, not unmeasured compute.
embedis 0.36 ms over 128 tokens; nothing towin there.
First A/B: expert-cache slots
Interleaved A/B/A/B, four pairs, two warmup runs discarded, short-explanation:
+1.93 tok/s (+4.3 %), winning all four adjacent pairs with disjoint
distributions (min B 46.38 > max A 45.93). The mechanism matches the profile
rather than merely correlating with it: the gain is bought entirely in expert
I/O await (−19.7 %), the exact block the profile named, with roughly half
refunded to
unaccountedas decode stalls on the GPU instead — which is why theheadline is +4.3 % and not the ~7 % the I/O delta alone would suggest.
A confound I hit, and how it's controlled
The first attempt ran the arms back to back and produced a clean monotonic win
for more slots. It was an artifact. Every run beat the previous one
regardless of arm — page-cache warming aliased perfectly onto run order. The
numbers above are the redo with arms interleaved so order cannot alias the
lever. The discarded pass is written up in the plan doc too, since the failure
mode is the more reusable finding.
--rdadvise adaptive/boundedcut I/O await similarly but returned all of it tounaccounted, netting no throughput change. It was only measured in theconfounded pass, so it is logged as "no effect observed", not a settled
negative.
Test plan
Scripts/test.sh— 1328 tests in 203 suites passed (161s)the Stage 1 baseline hash on the installed model
all 10 A/B runs and both flag states
Blocking questions
per AGENTS.md this is a measurement, not a default change. Making the case
needs the other two prompts, a memory-headroom check at 32 slots, and the
same interleaved treatment. Worth doing now, or defer until Qwen is
installed?
attention_routerin scope for Stage 2C? It is the entirecontext-dependent cost and currently the larger block, but it is also the
part where NVMAI's picks are least likely to transfer.
rdadviseget a clean interleaved A/B, or is "no effect observed"enough to drop it?
Caveat on coverage
The installed model is Gemma 4; Qwen 3.6 is not downloaded. Expert-I/O,
slot-cache, and rdadvise work is family-agnostic and measurable on Gemma today,
but any Qwen-specific decode claim needs the ~19.6 GB install first.