Repository navigation
feat(moe): load a short prefill chunk's experts on demand through the slot cache - #676
YevheniiKotyrlo wants to merge 6 commits into
Conversation
…ache, bit-identical to the stream
OffloadMoeCache raises when --moe-prefill-demand-tokens is set for cpu or hybrid decode, whose experts are not read from the slot cache a demand-loaded chunk fills; the test drives both targets.
|
I tested this on Linux: RTX 5080 16 GB (sm_120), torch 2.14.1, Triton 3.8, sglang-kernel 0.4.9, 1.
So any chunk between ~64 and 512 tokens fails, which is the range the description recommends. Handing the kernel the chunk's unique ids (at most 2.
The illegal access poisons the CUDA context, so the first short prefill takes the server down. The bf16 and the NVFP4 GEMMs both go through it. The Triton align in Results with both fixed. An agent-style conversation over a cached ~28k-token prefix, 8 turns, TTFT per turn by threshold:
|
lru_ensure dedups its query in a [K, K] block, so a chunk's tokens x top_k ids spilled past 128 and failed to compile past 1024. The chunk's distinct experts now go to it 128 at a time, the width its dedup stays flat to, each query's misses copied before the next query evicts, and a threshold of 0 never takes a chunk on demand. Co-authored-by: Gabriel A. Devenyi <gdevenyi@gmail.com>
sgl_kernel's moe_align_block_size counts experts in one 1024-thread block and reads out of bounds past it. A demand prefill chunk aligns against the slot count, so with sgl_kernel installed the first one poisoned the CUDA context. Co-authored-by: Gabriel A. Devenyi <gdevenyi@gmail.com>
|
Thanks for testing it at a real slot count, and for the TTFT run, which the description now cites. 4bea268 hands |
Problem
With
--moe-strategy offload,OffloadMoELayer._prefill_routedstreams every expert of each layer for every chunk, however short. On Qwen3.8-Flash-Next that is 1.42 GB a layer and 63.5 GiB a chunk, about 2.6 s over PCIe 4.0 x16 before the first token of any prompt. #579 has the trace. Decode already loads only the experts its tokens route to, through the slot cache.Solution
--moe-prefill-demand-tokens N, off (0) by default. A prefill chunk of at most N tokens loads the experts it routes to into the slot cache, as decode does, and runs the same grouped GEMM over the slot cache at their slots, so its output is bit-identical to the streamed layer's. A longer chunk streams as before.OffloadMoeCache.loads_prefill_on_demandand a branch at the top of_prefill_routed.OffloadMoeCache.load_prefill_expertshandslru_ensurethe chunk's distinct experts 128 at a time, the width its[K, K]dedup block stays flat to in flashlib's slot-cache cost model (it spills past that and does not compile past 1024), and copies each query's misses before the next query evicts. The distinct experts are a sorted list of fixed length, so there is no host sync, and the admissions stay out ofcollect_decode_freqand the LRU stats.sgl_kernelinstalled,moe_align_block_sizealigns on Triton past 1023 experts:sgl_kernelcounts experts in one 1024-thread block, and a demand chunk aligns against the slot count._DENSE_MOE_SETTINGS.EngineConfig, the cache construction,server/args.py,docs/cli.md.Closes #579.
Tests
tests/moe/test_offload.py::test_offload_moe_layer_short_prefill_loads_its_experts_through_the_slot_cache: a 96-token chunk routing to 160 experts admits them in queries of 128 and 32, copies each query's misses before the next, counts none of them as decode, and runs the GEMM over the slot cache at their slots without materializing the layer; a 97-token chunk streams.test_offload_moe_cache_takes_a_prefill_chunk_on_demand_only_within_its_threshold: a threshold of 0 takes no chunk on demand, and 2 takes chunks of 1 and 2 tokens.tests/moe/test_nvfp4_backends.py::test_triton_demand_prefill_matches_the_streamed_layer_bit_for_bit(GPU): a 600-token chunk, 1,200 routes, after decode left other experts resident, over a 1,100-slot cache and over a cache of one layer's experts filled three per query. The GEMM over the slots equals the streamed layer's exactly, and no LRU stat is recorded.test_adjust_config_drops_the_demand_prefill_threshold_for_a_dense_model: a dense model given 512 ends with 0.test_offload_moe_cache_rejects_demand_prefill_without_gpu_decode, cpu and hybrid: each raisesneeds GPU decode.tests/moe/test_fused_moe.py::test_moe_align_leaves_sgl_kernel_past_its_one_block_of_experts: withsgl_kernelinstalled, 1023 experts align on it, and 1024 and 2100 on Triton.test_moe_align_sorts_every_route_into_its_expert_blocks_past_1024_experts(GPU, gdevenyi's): at 1023 and 2100 experts every route lands once, in a block of its expert.Over
mainthe first six fail: the layer test with no admission (assert [] == [128, 32]), the align test sending 1024 and 2100 experts tosgl_kernel, and the others on the missing setting or method. On this PR's first head (a8f24b1), on Windows, the bit-identity test at 600 tokens fails to compile_lru_ensure_kernel(numel (4194304) exceeds triton maximum tensor numel (1048576)).A/B
RTX 3090 Ti 24 GB on PCIe 4.0 x16, Ryzen 9 9950X3D, driver 617.14, Windows 11,
RadixArk/Qwen3.8-Flash-Next-NVFP4. Two arms of one build, one run each: the flag at 0, which streams every chunk asmaindoes, and at 4,096. Every prompt is a streaming chat completion, timed from the request to its first token:--moe-prefill-demand-tokens 0--moe-prefill-demand-tokens 4096The arms cross near 700 tokens, and I serve with 512. The build is my FreeToken 0.1.3 with my other changes, measured on 2026-09-30; its expert cache admits by LRFU rather than LRU, a whole chunk in one call, so this table does not time the 128-wide admission. gdevenyi timed his versions of the two fixes in this PR on an RTX 5080 16 GB under Linux with a 2,100-slot cache (comment below): at 512, short turns over a cached 28k-token prefix reached their first token 40-70% sooner, longer turns were unchanged, and decode held at 55-58 tok/s.
The server command
--vram-reserve-mbis #574 and--kv-placementis #672;--pin-reasoning-effort,--token-embedding-placement,--moe-cache-policy lrfu,--mamba-host-slots,--moe-collect-statsand--bind-when-readycome from my local build.Verification
The test files that reach the changed code are
tests/moe/test_offload.py,test_nvfp4_backends.py,test_fused_moe.py,test_dsfp4_grouped_prefill.py,test_gpt_oss.py,test_prefill_hit_d2d.py,test_hybrid_fetch.py,test_fused_copy.pyandtests/engine/test_cache_budget.py.mainat9c635a1: 125 passed, 7 skipped, and the three tests that need flashinfer, which this environment lacks and which fail onmaintoo.test_prefill_hit_d2d.py's batch-memcpy tests, which fail now and then until fix(kernel): order the batch-memcpy probe against the current stream #414 lands.mainthe first six tests fail on both platforms, as above. Neither environment hassgl_kernel, so the out-of-bounds read itself is gdevenyi's measurement.The tracker has no other issue or PR that loads a prefill chunk's experts on demand: searched
prefill slow,nvfp4 prefill,prefill throughput,fused_nvfp4,prefill demand,experts on demand prefill.Known limits
DSV4OffloadMoELayeralready takes a chunk on demand whileT * top_k < num_experts. On Flash-Next that rule stops at 51 tokens (512 experts at top-10), far below the crossover above, which is why this is a token threshold. The DeepSeek-V4 path is left as it is.