fix(PP): size the mamba pool per pipeline stage, not per whole model - #33666
Merged
Merged
Conversation
_handle_max_mamba_cache charges mamba_cache_per_req, which is sized from the whole model's mamba layer list, but each PP rank only allocates state for the layers in its own [start_layer, end_layer) slice. Every stage is therefore charged roughly pp_size times what it holds and the pool is starved -- on Kimi-K3 (93 layers, 69 linear-attention layers) at pp 8 the pool resolves to 26 slots and clamps max_running_requests to 6, where the same run with this change gets 210 slots and 52. Scale the per-request cost by the share held by the stage with the most mamba layers. That is the capacity the system is limited by anyway, and taking the maximum rather than each rank's own count keeps the derived max_mamba_cache_size, max_running_requests and pp_max_micro_batch_size identical on every stage without a collective. Scaling by each rank's own share instead makes those values diverge whenever the mamba layers do not split evenly, which is the common case. No-op at pp_size 1, where the rank holds every mamba layer.
YAMY1234
marked this pull request as ready for review
August 6, 2026 08:00
YAMY1234
requested review from
Ying1123,
alphabetc1,
hanming-lu,
hnyls2002,
hzh0425,
ispobock,
merrymercy,
xiezhq-hermann and
yizhang2077
as code owners
August 6, 2026 08:00
Collaborator
Author
|
/tag-and-rerun-ci |
ispobock
approved these changes
Aug 6, 2026
sagearc
pushed a commit
to sagearc/sglang
that referenced
this pull request
Aug 13, 2026
…eline stage, not per whole model (sgl-project#33666) (sgl-project#34035) Co-authored-by: YAMY <74099316+YAMY1234@users.noreply.github.com> Signed-off-by: Sage Ahrac <sagiahrak@gmail.com>
saturn-acc
pushed a commit
to saturn-acc/sglang
that referenced
this pull request
Aug 16, 2026
Atituiset
pushed a commit
to Atituiset/sglang
that referenced
this pull request
Sep 10, 2026
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.
Motivation
KVCacheConfigurator._handle_max_mamba_cachesizes the mamba state pool fromconfig.mamba2_cache_params.mamba_cache_per_req, which is computed over the whole model's mamba layer list:Under pipeline parallelism each rank only allocates state for the layers in its own
[start_layer, end_layer)slice — the allocation paths in this same file already filter on that range. Charging the whole model against a single stage over-estimates the per-slot cost by roughlypp_size, so the pool is sized for a fraction of the requests that actually fit.On Kimi-K3 (93 layers, 69 linear-attention layers) at
pp_size=8this resolves to a 26-slot pool, which clampsmax_running_requeststo 6. That propagates:resolve_max_num_reqsclampsmax_running_requeststomax_mamba_cache_size // ratio, which in turn setspp_max_micro_batch_size.Modifications
Scale the per-request cost (and the ReplaySSM ring, which is sized the same way) by the stage's share of the mamba layers.
The scale is taken from the stage holding the most mamba layers, not from this rank's own count. Both give the same system-wide capacity — the system is limited by the heaviest stage either way — but the maximum is identical on every rank, so
max_mamba_cache_size,max_running_requestsandpp_max_micro_batch_sizestay uniform across stages without a collective. Scaling by each rank's own share instead makes those values diverge whenever the mamba layers do not split evenly, which is the common case: atpp_size=8a 93-layer model distributes its 69 mamba layers as[9, 8, 8, 9, 9, 9, 9, 8].Sweeping 145 mamba budgets from 4 to 40 GiB on that layout:
pp_max_micro_batch_sizediverges across stagespp_max_micro_batch_sizehas to agree on every stage: it decides how a batch is split into microbatches, so a stage that picks a different split disagrees with its neighbours about which sequences are in flight.Every rank computes the layer distribution locally from
get_pp_indices, so nothing is communicated. Atpp_size=1the rank holds every mamba layer and the scale is exactly 1.Accuracy Tests
End to end on Kimi-K3 with real weights,
pp_size=8tp_size=1, 2 nodes,--attention-backend flashinfer, GSM8K 200 questions:max_mamba_cache_sizemax_running_requests--chunked-prefill-size 16384--chunked-prefill-size 16384--chunked-prefill-size 65536--chunked-prefill-size 65536The pool grows 8.1x and the concurrency ceiling 8.7x, while the score stays in the same band (the 0.985/0.990 spread is one question out of 200). The change only lifts a capacity limit; it does not touch any compute path.
What that "before" column looks like in the startup log: the unpatched tree allocates a per-stage pool but charges the whole model against it, so every stage lands on the same starved cap.
The allocated
ssm_statealready varies with what each stage holds — 1.42 GB on the stages with 9 mamba layers, 1.27 GB on those with 8 (the layers distribute as[9, 8, 8, 9, 9, 9, 9, 8]). The pool size does not: all eight stages are sized as if they held all 69.The same over-charge also applies when the pool size is given explicitly.
--max-mamba-cache-sizeskips the auto solve, so the pool itself comes out right, but the memory the solver then books for it still uses the whole-model per-request cost — leaving too little for the KV pool. Same layout, 70 GiB handed to the solver:--max-mamba-cache-sizeAt 328 the budget goes negative and there is no KV pool left to build, which is what an operator sees as "raising
--max-mamba-cache-sizemakes startup fail".Driving
_handle_max_mamba_cacheonce per rank on the same layer layout with an 8 GiB budget isolates the same effect:pp_size=1poolpp_size=8pool, per rankTestPPMambaPoolSizingintest/registered/unit/mem_cache/test_mamba_donated_alloc_ratio.pyadds two CPU-only tests.test_stage_is_not_charged_for_the_whole_modelfails on the unpatched tree and passes with this change; running that file goes from1 failed, 7 passedto8 passed.Speed Tests and Profiling
The pool size this PR unlocks is what an agentic serving configuration actually wants. On a 1P1D deployment (8 GPUs running PP8 prefill, 8 running TP8 decode) with a trace-replay workload of 98,827 requests — mean input 219k tokens, p90 549k, ~98% theoretical prefix reuse — a 144-slot mamba pool with a 14.5M-token KV pool sustains:
--decode-context-parallel-size 8"Effective total" counts prompt tokens including cache hits, which is what the client sees; at 93.6% reuse the recomputed share is far smaller. The per-GPU column divides by all 16 GPUs in the deployment. The last row is the mean of two runs; the drop at concurrency 32 is a decode-side KV limit, not a mamba one, and decode context parallelism removes it.
That operating point is not reachable without this change: the auto solve gives roughly
1/pp_sizeof it, and setting--max-mamba-cache-sizeby hand instead runs into the accounting problem above. The numbers are a capability statement, not a before/after delta — there is no matched "before" run at this pool size because the configuration does not come up.Per-token cost is unchanged; this PR only lifts a capacity limit.
Note for existing deployments
Correcting the over-charge frees real memory, so at an unchanged
--mem-fraction-staticthe solver hands the KV pool what the mamba pool was previously over-booking. On a Kimi-K3 PP8 prefill instance that movedmax_total_tokensfrom 2,123,072 to 21,287,872 at the same flag value, and the instance then failed to start. The old over-charge was acting as unintended headroom. Deployments tuned against the previous accounting may need to lower--mem-fraction-staticor pin--max-total-tokensafter this change.Checklist
Merge order
This PR raises the concurrency ceiling, which makes packed prefill batches larger. On the same hardware that turns out to be what makes the int32 overflow in #33665 reachable in practice: before this change
max_running_requestsis clamped to 6 and a batch never gets near the 2^31 threshold, after it the ceiling is 52. Worth landing that one first.CI States
Latest PR Test (Base): ✅ Run #30984169609
Latest PR Test (Extra): ✅ Run #31123531254