Skip to content

5080deploy: expert prefetch into low-priority slots with single-writer slot maps (FREETOKEN_MOE_PREFETCH), +22% decode - #74

Open
gdevenyi wants to merge 25 commits into
5080/base-6599710from
5080/32-expert-prefetch
Open

gdevenyi wants to merge 25 commits into
5080/base-6599710from
5080/32-expert-prefetch

Conversation

@gdevenyi

Copy link
Copy Markdown
Owner

Stacked on #71/#72/#73 (branch base 5080deploy @ 6599710). Expert prefetch for the offloaded-MoE decode, behind FREETOKEN_MOE_PREFETCH=off|measure|on (default off), plus two debug detectors that were needed to make it lossless.

What. While layer L-1 computes, predict layer L's routed experts and copy them over the otherwise-idle PCIe link. A decode step leaves the link idle about half the time, ~225 µs per layer in nsys.

  • Predictor stream: runs layer L's router on layer L-1's router input (training-free lookahead), with no cache reads or writes. It is forked at the start of layer L-1's MoE block.
  • Compute stream: right after ensure(L-1), prefetch_ensure(L) installs the first budget non-resident candidates into low-priority slots: held with usage 0, keyed between empty slots and every real resident, so a wrong guess is the next victim. It writes its own copy plan.
  • Highest-priority copy stream: copies that plan with the slim copy kernel (5080deploy: opt-in slim expert copy kernel for copies beside compute (FREETOKEN_MOE_SLIM_COPY, off) #73) after the demand copy(L-1).
  • Joins: the compute stream waits for the prefetch copy before copy_missing(L). A demand hit on a prefetched slot clears its low-priority mark.
  • Settings:
    • FREETOKEN_MOE_PREFETCH_BUDGET: default 3 before GDN layers and 4 before attention layers; 4 is the measured best.
    • FREETOKEN_MOE_PREFETCH_K: candidates, default 16.
    • measure: counts useful predictions without copying.

Single writer. Every write to the slot maps (slot_for_id, id_of_slot, usage, eviction mirrors) happens on the compute stream in program order. Only reads and byte copies cross streams. An earlier version ran prefetch_ensure on the predictor stream behind an event join. On the real model it corrupted greedy output in rare runs:

  • 1/60 cold prompts at budget 4
  • up to 6/60 under the verifier's timing

A CPU model of both kernels proved the recorded bad state is reachable only if the two map writers overlap. nsys showed no violation in 400k samples, so the join failed only in some rare execution (first decode after a prefill). Rather than depend on that edge, the maps now have one writer.

Measured. RTX 5080, Qwen3.8-Flash-Next-NVFP4, 1650 slots, rule eviction + copy overlap (#71/#72), bs=1, interleaved ABAB, decode tok/s at 32K / 128K:

Arm 32K 128K vs off demand misses/token
off 43.36 42.67 — 206
on, default budget 51.32 50.45 +18.3% 123
on, budget 4 53.02 52.12 +22.2% 111

Measure mode on the real model: useful predictions 1.73–1.84 per layer at precision ~0.55, covering ~42% of demand misses.

Losslessness evidence on the real model (greedy, 256-token outputs, cold nonce-prefixed prompts, compared prompt by prompt with a prefetch-off reference that is identical across three server restarts):

  • default budget: 0/60 mismatches
  • budget 4: 0/60
  • budget 4 + FREETOKEN_MOE_PREFETCH_VERIFY=meta: 0/120, 0 verifier records. This is the config that amplified the old bug to 2–6/60.
  • budget 4 + FREETOKEN_MOE_SLOT_AUDIT=64: 0/120; 444 full-cache audits byte-checked every held slot, 0 bad.
  • Greedy fingerprints are identical to production on 3 fixed prompts.

Debug tools (default off, zero cost when off):

  • FREETOKEN_MOE_PREFETCH_VERIFY=meta|1 checks every routed slot's maps (meta), plus its bytes before and after each expert GEMM (1).
  • FREETOKEN_MOE_SLOT_AUDIT=N records the last writers of every slot and byte-checks the whole cache every N decode steps.
  • Both have fault-injection tests (missing join, corrupt bytes, stale map, write during GEMM, skipped copy).

Tests: 75+ new tests, including:

  • bit-identity of on vs off over 200 steps (eager and graph, bs 1 and 2, rule and lru, overlap on and off, bf16 and nvfp4)
  • delay-kernel races on every stream, garbage predictors and prefills in between
  • low-priority band arithmetic
  • the single-writer structural test
  • eviction of a slot whose prefetch copy is in flight, with a negative control

Full not-slow suite: only the 4 known environment failures.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bu6LgoxLR4wETqb7RPR2vt

…KEN_MOE_PREFETCH

Stage A of cross-layer expert prefetch: in GPU decode, layer L-1's router input times layer L's
router predicts layer L's experts on a dedicated stream forked after ensure(L-1); the first
BUDGET non-resident candidates of the top-K are joined and counted against layer L's routing
after its ensure. No copies and no cache writes; off by default and launch-identical when off.

Assisted-by: Claude Opus 5.5
…d its measure mode

Assisted-by: Claude Opus 5.5
…gh layer l's router

The counter test scores the spied logits, so it cannot see a wrong router or input; this one
feeds distinct per-layer inputs and recomputes each prediction's logits on the host.

Assisted-by: Claude Opus 5.5
…FREETOKEN_MOE_PREFETCH=on

The predictor stream installs layer L's kept candidates with a prefetch mode of the scored
ensure kernel (no clock bump, usage == step stays pinned, usage 0 marks the slot low
priority) and a third dedicated stream slim-copies them after copy(L-1). The compute stream
joins the install before ensure(L) and the copy before copy(L); ensure(L) counts useful
hits, calls, misses and late copies. LRU runs the vendored kernel while prefetch is on.

Assisted-by: Claude Opus 5.5
…der delayed streams

A delayed install that lands after ensure(L) changes no output, so the race test now also
compares the delayed run's slot maps and counts with an undelayed run's.

Assisted-by: Claude Opus 5.5
…a cache frees its banks

The prefetcher's back-reference made a cycle, so a dropped cache kept its slot banks in VRAM
until the next gc pass.

Assisted-by: Claude Opus 5.5
The slim copy is an SM kernel. At default priority a copy that starts even 50 us late
waits for SMs behind the next compute grid while the compute stream waits on it: +13.7%
per step vs off at bs 1 in bench_moe_prefetch --on, against -16.3% at high priority.
No measurable cost when nothing is late.

Assisted-by: Claude Opus 5.5
…the prefetch bench

Assisted-by: Claude Opus 5.5
…d graph interleaving

Adds a stall before each decode GEMM (copy(L+1) lands before GEMM(L) reads; flood candidates
reach the pinned slots even under lru), prefills between decodes on every prefill path
(materialize, overlap, overlap + hit-D2D, small ensure), bs 1/2 graphs in one pool replayed
between eager steps, a per-layer delay fuzz over all four streams, the per-bank copy fallback,
and the kernel against the reference at 48 x 512 experts, 1480 slots and 8 rows.

Assisted-by: Claude Opus 5.5
prefetch_ensure(L) scored slots as if layer L had just run, so L's own residents looked a
whole pass away and were the first residents it evicted, right before L routed them. On
the steady-state trace bench this cost +0.18 (bs 1) and +0.42 (bs 2) extra demand misses
per layer; scored from layer L-1 the excess is gone and on-mode steps are 1.6-2.5 pts faster.

Assisted-by: Claude Opus 5.5
--steady replays a recorded routing trace with no cache restore, a noisy copy of each layer's
traced logits as the lookahead and per-layer-kind fillers, and interleaves the arms step by
step (each on its own copy of the cache state) so drift hits them alike. It reports pollution
and the late fraction before GDN and QSA layers next to ms/step.

Assisted-by: Claude Opus 5.5
…and relax the eager late bound

Every offload cache under test makes dedicated streams and torch keeps a cuBLAS workspace per
stream for the session (~16 MiB a prefetch stack): the prefetch suites grew one pytest process
past 8 GiB and hit OOM beside another GPU user; now the peak is ~1 GiB. The eager delayed-copy
case asserted late >= 0.9 of copies, which a loaded host misses (3 of 10 loops); replay keeps
the exact late == copied check.

Assisted-by: Claude Opus 5.5
… the gpu

A process-wide FREETOKEN_MOE_PREFETCH made every hybrid or CPU-decoded cache raise at build
(two hybrid-fetch and two overlap-flag tests failed under the flag). The flag now logs and
leaves those caches off, as FREETOKEN_MOE_COPY_OVERLAP does; an explicit prefetch_mode still raises.

Assisted-by: Claude Opus 5.5
…behind FREETOKEN_MOE_PREFETCH_VERIFY

Debug detector for the prefetch-on nondeterminism. For every GPU decode layer call, on the compute
stream and without any event or wait: the routed slots map to the router's experts (before and after
the GEMM) and, with =1, every routed slot's bank rows equal the host rows before and after the GEMM
(a 53 MiB scratch filled by the fused copy). Per-layer device counts and the first 64 offending
records (with the slot owner, usage and which copy plan held the slot) are logged every 256 decode
steps and at shutdown. =meta checks only the maps (~1% decode cost); =1 costs 2.4-3.3x.

Assisted-by: Claude Opus 5.5
…een decode steps

Assisted-by: Claude Opus 5.5
…d FREETOKEN_MOE_SLOT_AUDIT

Assisted-by: Claude Opus 5.5
… sizes and eager steps

Assisted-by: Claude Opus 5.5
…write has one writer

The predictor stream ran prefetch_ensure(L), a slot map writer, guarded from the demand
ensure(L) only by a cross-stream event; a production verify record shows the two overlapped.
The predictor now forks at the start of layer L-1's MoE block and only computes the merged
lookahead candidates (it reads no cache state); right after ensure(L-1) the compute stream
waits for them and runs prefetch_ensure(L), which applies residency and the budget itself, so
the installs equal the old ones and do not depend on stream timing. join_ensure is gone.

Assisted-by: Claude Opus 5.5
… right after a prefill

Assisted-by: Claude Opus 5.5
…-writer test

At d6a0a99 the select assertion came first and raised AttributeError on
_none_resident, so the writer-stream checks never ran against the old engine.
Now it fails there on prefetch_ensure off the compute stream (16/16).

Assisted-by: Claude Opus 5.5
…its negative control

prefetch_ensure(L) runs on the compute stream before join_copy(L-1), so it can
take over a slot copy(L-1) is still writing; count that on the device under late
copies and check no bit changes. Ordering copy(L) only after its own install, on a
second stream, corrupts the slot.

Assisted-by: Claude Opus 5.5
@gdevenyi

Copy link
Copy Markdown
Owner Author

Deployed on 5080deploy (8ba20f6) with FREETOKEN_MOE_PREFETCH=on FREETOKEN_MOE_PREFETCH_BUDGET=4, on top of rule eviction and copy overlap. Validated on the production launcher:

  • Greedy output: the fingerprints on 3 fixed prompts are identical to the pre-prefetch production output.
  • Quality: quality.py scores 14/15 (code2 is the known model miss); needle 32K and 128K both pass.
  • 255K prompt with 3 concurrent side calls: decode runs at 50.8 tok/s. It was 43.0 with rule + overlap and 38.4 with the original LRU. Side calls take about 1.7 s.
  • Peak VRAM: 15,716 MiB against about 15,882 usable, i.e. +148 MiB over rule + overlap. That covers the predictor stream's cuBLAS workspace, the plan buffers and the extra graph branches, and leaves about 166 MiB of headroom.
  • 15-minute agentic soak with side calls: PASS. It ran 57 main turns (50 without prefetch), 41 side calls and 593 tool calls, with a worst token gap of 14.3 s (a long-turn prefill).

https://claude.ai/code/session_01Bu6LgoxLR4wETqb7RPR2vt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant