Skip to content

5080deploy: opt-in on-demand experts for short prefills (FREETOKEN_MOE_SMALL_PREFILL_TOKENS, off) - #68

Open
gdevenyi wants to merge 3 commits into
5080/27-ple-fill-before-launchfrom
5080/28-small-prefill-on-demand
Open

gdevenyi wants to merge 3 commits into
5080/27-ple-fill-before-launchfrom
5080/28-small-prefill-on-demand

Conversation

@gdevenyi

Copy link
Copy Markdown
Owner

Stacked on #67. This is an opt-in path that is off by default and left off in production. It is published with its measurements so the result isn't lost.

What: FREETOKEN_MOE_SMALL_PREFILL_TOKENS=N. A prefill of at most N tokens makes only its routed experts resident through the LRU cache, instead of streaming every expert of every layer.

  • The motivation: nsys shows a cached-prefix agent turn streams ~68 GB of experts, which puts a ~1.25 s floor on time-to-first-token (TTFT) however few tokens are new.
  • The path runs the GEMM on the cache's bank views, as decode does. A layer whose unique experts exceed half the cache falls back to the whole-layer path.

Commits:

  • 51a6321: the path and its dispatch tests.
  • d47f8eb: hand lru_ensure the unique ids, not tokens × top-k. The kernel block is next_pow2(ids) × next_pow2(slots); a 156-token turn overflowed Triton's 1M-element limit and took the engine down.
  • 283cb2e: ensure in 32-id (decode-width) chunks. A 512 × 2048 block was still under the limit but failed to launch with CUDA OOM on the serving GPU.

Each fix has a GPU test that fails before and passes after. The real-size test (200 tokens × top-10, 512 experts, 1100 slots) asserts the ensure width.

A/B on an RTX 5080, Qwen3.8-Flash-Next-NVFP4, 1650 slots, a ~28-40K cached conversation:

new tokens per turn off N=1024 N=256
37-200 (short question) 1.22-1.25 s 0.59-0.86 s 0.66-0.86 s
720 1.30 s 2.65 s (not on the path)
992 ~1.45 s 3.03 s (not on the path)
1.1K-3K 1.44-2.47 s same same
  • Decode rate after a turn is unchanged (39-45 tok/s).
  • Quality (15 greedy items plus needles): 14/15 off, 13/15 on. arith2 flips from 1047576 to 1047572, the same fragile item a bf16->FP8 change flips. On-demand prefill goes through the decode GEMM and its rounding, so its outputs are not identical to the whole-layer path.

Verdict: a ~0.5 s TTFT win on turns under ~200 new tokens. Past a few hundred tokens it is 2× slower: nearly all experts are needed, and chunked ensure + copy loses to the overlapped bulk stream. With the numerics change on top, production stays at 0 (off). A useful N would sit around 128-256 and would still need a quality sign-off.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bu6LgoxLR4wETqb7RPR2vt

…_MOE_SMALL_PREFILL_TOKENS)

A prefill streams every expert of every layer (the next layer's routing is unknown when its
prefetch must start). For a short extension over a cached prefix -- an agent turn -- that is
~68 GB on Qwen3.8-Flash-Next and ~1.3 s before the first token, whatever the token count.
Prefills of at most N tokens now take the decode path's on-demand LRU load (route, fetch the
missing routed experts, slot GEMM); N = 0 (default) keeps the whole-layer path everywhere.

Assisted-by: Claude Opus 5.5
…unked under Triton's limit

The kernel's block is next_pow2(ids) x next_pow2(slots): a 156-token turn's 1560 routed ids
against 1650 slots is a 2048 x 2048 block, past Triton's 1M-element limit, and the compile
error took the engine down. The on-demand path now dedups the ids (at most num_experts),
ensures them in chunks that fit, copies each chunk's misses before the next ensure, and maps
the tokens' ids to slots; a layer needing more than half the cache takes the whole-layer path.
The GPU tests now assert that the on-demand path is really taken.

Assisted-by: Claude Opus 5.5
Chunking the unique ids only under Triton's element limit still handed the LRU kernel a
512 x 2048 block; on the serving GPU (~1 GiB free) its launch failed with CUDA out of memory
on a 156-token turn and took the engine down. Each ensure now takes at most 32 ids, the
width a decode step already gives it. The GPU test asserts the width.

Assisted-by: Claude Opus 5.5
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