Repository navigation
fix(moe): admit a wide offloaded decode step's experts 128 at a time - #680
Draft
YevheniiKotyrlo wants to merge 1 commit into
Draft
YevheniiKotyrlo wants to merge 1 commit into
YevheniiKotyrlo wants to merge 1 commit into
Conversation
…t a time lru_ensure dedups its query in a [K, K] block that stays flat to 128 ids, spills past that and does not compile past 1024, and offloaded decode handed it every route of a step: past 256 routes (26 requests at top-10) one call cost about 270 us a layer on an RTX 3090 Ti, and past 1024 (103 requests) Triton refused to compile it. A step past 256 routes now goes to it as its distinct experts, 128 at a time, each query's misses copied before the next query evicts, with the statistics counting the step once. DeepSeek-V4's on-demand prefill chunks take the same path.
This branch has not been deployed
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.
Problem
With
--moe-strategy offloadand GPU decode,OffloadMoELayer._decode_routedhands flashlib'slru_ensureevery route of a decode step in one call: at the largest captured batch,--max-running-requeststimestop_kids. The kernel dedups its query in a[K, K]register block, which flashlib's slot-cache cost model (flashlib/kernels/slot_cache/cost.py, fitted to H100 runs) keeps flat to K = 128 and measures spilling past it. Past 1,024 ids the block exceeds Triton's tensor limit and the kernel does not compile:At top-10 (Qwen3.8-Flash-Next) that is from 103 concurrent requests. Below it the spill is the cost: on an RTX 3090 Ti one admission takes 7-17 µs a layer up to 12 requests, 44 µs from 13 to 24, and 264 µs at 32 and 48, about 13 ms of every decode step across 48 MoE layers (Measurement below). DeepSeek-V4's on-demand prefill chunks (
DSV4OffloadMoELayer._prefill_routed, taken whileT * top_k < num_experts) reach 252 routes at 256 experts. #676 hit the same limit on a demand prefill chunk, where gdevenyi found it.Solution
OffloadMoeCache.load_experts(layer_id, topk_ids)replacesensure_expertspluscopy_missingat both call sites and returns the slot ids:ENSURE_ONE_CALL_WIDTH(256) routes is the single admission it was, rewritingtopk_idsin place: up to 25 requests at top-10 nothing changes, and up to 256 ids the spill costs less than splitting the query.lru_ensureas its distinct experts,ENSURE_QUERY_WIDTH(128) at a time, each query's misses copied before the next query evicts. The distinct experts are a sorted list of fixed length, so the step needs no host sync and stays CUDA-graph capturable. LRU evicts an earlier query's slots last, and a layer never routes to more experts than the cache holds, so no query evicts an earlier one's.decode_freqcounts every route.ENSURE_QUERY_WIDTHandensure_experts'srecord_statsparameter are the same lines as in #676, so whichever of the two lands second rebases cleanly.Measurement
RTX 3090 Ti 24 GB, Windows 11, torch 2.14.0, triton 3.8.0. One decode step's admission at top-10 over 512 experts, into a 2,100-slot cache that already holds all 512, so every step is all hits and the time is the admission alone. 48 admissions, one per MoE layer, are captured in a CUDA graph and replayed 40 times;
main's call andload_expertsalternate A B B A. Microseconds per admission:mainSplitting every step past 128 routes instead cost 59-70 µs from 13 to 24 requests, more than the one call it replaced, which is why one call keeps up to 256. Compiling
main's 512-wide variant took 45 s on its first call.Tests
tests/moe/test_offload.py::test_offload_moe_layer_wide_decode_step_loads_its_experts_128_at_a_time: a 150-row step routing 300 times to 160 experts, 7 of them resident, admits them in queries of 128 and 32, each followed by its copy, and decodes at their slots. The statistics read 160 met, 153 missed and one call, and the histogram counts each route once. An 8-route step stays one admission, in place.test_dsv4_offload_layer_wide_on_demand_chunk_loads_its_experts_128_at_a_time: DeepSeek-V4's on-demand chunk, 300 routes over 640 experts, admits in queries of 128, 128 and 44.tests/moe/test_nvfp4_backends.py::test_triton_wide_decode_step_matches_the_streamed_layer_bit_for_bit(GPU): a 600-row step, 1,200 routes, over a 1,100-slot cache and over a cache of one layer's experts filled three per query. The GEMV over the slots equals the one over the streamed layer exactly, and the statistics count one step.test_triton_wide_decode_step_replays_as_a_cuda_graph(GPU): with both widths at 3, the wide admission and the GEMV capture as a CUDA graph, and a replay on a cold cache decodes exactly as the streamed layer.Over
mainthe two layer tests fail on a single query of 300 (assert [300] == [128, 32],assert [300] == [128, 128, 44]) and the GPU tests on the missing method. Throughmain's admission, the 600-row step fails to compile_lru_ensure_kernelwith the error above.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.py,tests/engine/test_cache_budget.py,tests/models/deepseek_v41andtests/scheduler/test_scheduler_kv_usage.py.mainat9c635a1: 166 passed, 7 skipped, the three tests that need flashinfer, which this environment lacks and which fail onmaintoo (162 passed, 7 skipped), and one oftest_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.tests/models/deepseek_v41tests that fail the same way onmain, and one of the batch-memcpy tests.The tracker has no issue or PR on a wide decode step's admission: searched
lru_ensure,numel exceeds triton maximum,max-running-requests,ensure_experts batch,slot cache.Known limits