[Disagg] Layer-pipelined KV transfer: overlap RDMA with GPU compute#23515
[Disagg] Layer-pipelined KV transfer: overlap RDMA with GPU compute#23515michael7193 wants to merge 60 commits into
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
CC: @UNIDY2002 Could you check this? I haven't gone through this PR carefully yet, but this seems like a cleaner implementation. |
|
Nice work. We took a different approach in #19931 (callback-driven, per-layer notifications from inside We've been working on Qwen3.5-397B-A17B (hybrid linear attention + GQA + VL), and there are a couple of gaps we can help fill:
We'd like to collaborate on getting Qwen3.5 support into this PR (or a follow-up). |
|
Thanks for the thoughtful review and kind words, @UNIDY2002! Great to hear about your experience with #19931. The callback-driven approach is interesting — glad we converged on similar goals from different angles. Both issues you raised are very practical: forward_split_prefill for Qwen3.5 — Makes total sense. The current implementation assumes models provide forward_split_prefill, so hybrid models like Qwen3.5-397B-A17B would indeed need that. Would love to see your implementation — a follow-up PR sounds perfect. Multimodal fallback — Good catch. Adding a multimodal guard in _get_pipeline_group_size() to fall back to the normal path is straightforward and the right thing to do. Happy to include it in this PR if you'd like to send a patch, or we can handle it in the follow-up together. Very much looking forward to collaborating on Qwen3.5 support. Feel free to ping me anytime! |
39d680d to
155f9b7
Compare
|
@UNIDY2002 Thanks for the catch — applied your @ShangmingCai Gentle ping — this PR is ready for review whenever you have a chance. Summary of what's been done since your last look:
Happy to address any further feedback! |
b5267cb to
2547641
Compare
|
@ShangmingCai Friendly ping — this PR has been rebased onto the latest main (no conflicts). Would appreciate your review when you get a chance. Also, could a maintainer add the |
|
Great! Too busy lately, let me trigger the CI first, will start to review next week. Thank you so much for the PR. |
|
/tag-and-rerun-ci |
There was a problem hiding this comment.
This file has a lint error. Also, is this modification mis-added by cc?
There was a problem hiding this comment.
The falcon_h1.py change is intentional — FalconH1 is a Mamba/Attention hybrid model where SSM conv states need special handling during layer-pipelined transfer (sent once at the final group via maybe_send_extra()). I'll fix the lint error in the next push.
There was a problem hiding this comment.
Does is means that we need to impl this forward_split_prefill for every single model? This might not be a robust design. Will dive in next week.
There was a problem hiding this comment.
Good question! Actually this is not a new pattern we're introducing — there are already 15 models in the upstream codebase that implement forward_split_prefill (llama, qwen, qwen2, qwen3, gemma, gemma2, gemma3, glm4, exaone4, sarvam_moe, qwen2_moe, qwen3_moe, etc.), added for chunked prefill / PP support.
Our layer-pipelined feature simply reuses this existing interface. The design has two layers of safety:
- Guard fallback: If a model doesn't have
forward_split_prefill, the pipelined path is automatically skipped and the request goes through the normal path (no crash, no regression). - Pattern is mechanical: For standard transformer models, the implementation is identical —
embed → layers[start:end] → norm → logits. Only hybrid models (Mamba SSM, hybrid linear attention) need custom logic.
That said, if you'd prefer a more robust approach, we could add a default generic implementation in a base class that works for any standard transformer model, so new models get pipelined support for free without writing any code. Happy to explore that direction if you think it's worthwhile.
|
Fixed the |
|
Fixed the CI failure — root cause was @ShangmingCai Could you re-trigger CI when you get a chance? Thanks! |
|
@michael7193 No problem, will do, but please fix lint first, or all the CI will be aborted. |
7bc1715 to
75d6aa3
Compare
|
@ShangmingCai Lint is fixed and passing now. CI completed — our |
- Change SGLANG_PIPELINE_GROUP_SIZE default to EnvInt(0) with comment explaining adaptive formula is used when env var is not set - Fix formula comment: use SAT_TOKENS = MIN_TOKENS * 3 notation instead of opaque "MIN_TOKENS * 2" denominator - Update env var docs: note overlap schedule auto-disable behavior, clarify GROUP_SIZE is optional override Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The docs/ directory is deprecated; CI rejects changes there. Move pipelined KV transfer env var documentation to docs_new/docs/references/environment_variables.mdx and restore docs/references/environment_variables.md to upstream state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix division-by-zero when SGLANG_PIPELINE_MIN_TOKENS=0 (clamp to 1) - Skip pipelining for very small models (<=4 layers) where per-group overhead outweighs any compute/transfer overlap benefit - Move GROUP_SIZE user override before min_tokens check so explicit configuration always takes priority over heuristics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Disable pipelining when enable_dp_attention is set, because forward_split_prefill bypasses prepare_mlp_sync_batch() needed for DP buffer initialization - Disable pipelining when draft_worker is present (EAGLE/spec decode), because run_batch_pipelined only iterates target model layers and would never transfer draft KV to the decode side - Disable pipelining when any request has input_embeds, because forward_split_prefill always calls embed_tokens(input_ids) and silently ignores custom embeddings passed via API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When enable_eplb is set, pipelined mode bypasses the expert_distribution_recorder.with_forward_pass() context and experts_capturer.on_forward_end() call in model_runner.forward(). This causes EPLB to lose routing statistics for pipelined batches, leading to suboptimal expert rebalancing decisions. Disable pipelining when EPLB is active to ensure complete routing data collection. This affects MoE models (Qwen3MoE, Qwen2MoE, SarvamMoE, ExaoneMoE) that have both forward_split_prefill and EPLB. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@ShangmingCai Hi! We've done several rounds of self-review and addressed the issues we found (generality guards for DP Attention/EAGLE/input_embeds/EPLB, edge-case protections for small models, docs migration, etc.). The code should be in good shape now. Could you take another look when you get a chance? Thanks! |
|
Will find time to review tomorrow. |
Prevent ZeroDivisionError if batch.reqs is empty (defensive guard). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…multiplier 1. Early-abort in transfer_worker: skip queued chunks/batches whose room has already been marked Failed, avoiding redundant lock acquisitions, record_failure calls, and wasted RDMA doorbells after a layer fails. 2. Expose SGLANG_PIPELINE_SAT_MULTIPLIER env var (default 3.0) to make the adaptive formula's saturation point tunable across different network bandwidths (lower for 800G IB, higher for 200G-400G IB). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… 1.0 Clamp SAT_MULTIPLIER to at least 1.01 so sat_tokens > min_tokens, preventing ZeroDivisionError in the adaptive formula. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Hi @ShangmingCai, how's the review going? Are there any remaining concerns or anything else you'd like me to address before merging? Thanks! |
Ensure layer-pipelined disagg prefill preserves final completion metadata for zero-page and chunked requests, while falling back for unsupported heterogeneous staging paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts between upstream Mooncake tracing changes and layer-pipelined KV transfer worker fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Sorry, I was temporarily held up by a few more critical features that required immediate review and testing. I could begin moving this PR forward for review today and tomorrow. I will also be pinging a few reviews to support this as well, since this PR requires cross-module co-design. |
Send final metadata only after layer-pipelined KV chunks are enqueued so aux buffers cannot race with worker finalization, and gate pipelined split prefill to audited Llama models while preserving normal ModelRunner forward hooks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ensure the pipelined split-prefill path follows the normal forward path by resolving deferred input IDs before constructing ForwardBatch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoid generating tokens for prefill-only requests and keep pipelined transfer status/state handling consistent with the normal path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| self: Scheduler, | ||
| batch: ScheduleBatch, | ||
| result: GenerationBatchResult, | ||
| pipelined: bool = False, |
There was a problem hiding this comment.
Looks like this is not needed.
There was a problem hiding this comment.
Done in 5cbeca7 — removed the unused pipelined argument. The per-request state (req.pipelined_kv_sent) is enough to distinguish the pipelined path, so the extra function parameter is not needed.
| ] | ||
|
|
||
| self.capture_aux_hidden_states = False | ||
| self.supports_layer_pipelined_kv_transfer = type(self) is LlamaForCausalLM |
There was a problem hiding this comment.
Why only llama need this supports_layer_pipelined_kv_transfer, I wonder do we need to set it for all supported models? Such as flacon and qwen in this PR?
There was a problem hiding this comment.
Good point. The Llama-only opt-in was over-conservative and inconsistent with the supported model checklist/benchmark (including Qwen and Falcon). I removed the redundant supports_layer_pipelined_kv_transfer flag and restored forward_split_prefill as the model capability guard. Unsupported cases still fall back via the existing backend/PP/DP-attention/EPLB/speculative/input_embeds/heterogeneous-TP guards.
| state_indices: Optional[List] | ||
| chunk_id: Optional[int] = None | ||
| layer_id: Optional[int] = None | ||
| cuda_event: object = None |
There was a problem hiding this comment.
Should we make this Optional?
There was a problem hiding this comment.
Yes, done in c59d819 — changed cuda_event to Optional[object] since normal chunks/final metadata chunks may not carry a CUDA event and use None.
| Returns (LogitsProcessorOutput, event) for the final group | ||
| (when split_index reaches num_hidden_layers). | ||
| """ | ||
| out = self.model_runner.forward( |
There was a problem hiding this comment.
Should we better call forward_split_prefill here instead?
There was a problem hiding this comment.
I kept this routed through ModelRunner.forward intentionally. ModelRunner.forward still dispatches to forward_split_prefill for SPLIT_PREFILL, but it also preserves the common forward bookkeeping/context around it (e.g. recorder/profiler hooks and the _forward_raw preparation path). Calling forward_split_prefill directly here would bypass that shared wrapper. Added a short comment in c59d819 to make this explicit.
|
@UNIDY2002 @zhangxiaolei123456 Do you have time to take a look and test this PR? I think it is almost ready for production test. |
I'll find some time to test it next week. |
Remove the redundant model opt-in guard so pipelined KV transfer follows the PR's documented split-prefill capability contract, and drop an unused review-only parameter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clarify the optional CUDA event type and document why the split layer path goes through the common ModelRunner.forward wrapper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Motivation
In PD disaggregation mode, KV cache transfer happens after full prefill computation completes. For long prompts (≥1K tokens), this creates a significant TTFT bottleneck — the decode side must wait for all layers to be computed and then transferred sequentially.
This PR implements layer-pipelined KV transfer: instead of computing all layers then transferring all KV at once, we split layers into groups and transfer each group incrementally. Transfer of group N overlaps with GPU compute of group N+1, significantly reducing TTFT.
Related: #19931 (same direction, different approach)
Key Results
TTFT (ms) — Prompt Length Sweep (C=32, output=256)
TTFT p95 (ms)
Throughput (output tok/s)
Multi-turn Dialogue (16 sessions × 10 turns)
Extreme Stress (C=64, prompt=4096, output=1024)
Design
The feature is controlled by environment variables (registered in `environ.py`), disabled by default:
How it works
`_get_pipeline_group_size(batch)` — per-batch decision: returns adaptive group_size (>0) or 0 to skip pipeline. A universal guard ensures models without `forward_split_prefill` safely fallback to the normal path. Short prompts also fall back with zero overhead.
`run_batch_pipelined(batch, group_size)` in `Scheduler` — splits forward into layer groups using `model_runner.forward_split_prefill()`, enqueues per-layer KV transfer via `send_layer()` after each group. CUDA events synchronize GPU→transfer ordering. Pre-computes state indices for hybrid models via `_prepare_pipelined_state_indices()`.
`process_batch_result_pipelined_prefill()` — result handler that dispatches to `run_batch_pipelined` instead of `run_batch`, then follows the same downstream logic (including EAGLE spec_info propagation, staging sync, A2A MoE finalization).
`MooncakeKVManager.send_kvcache_layer()` — single-layer RDMA transfer supporting both MHA and MLA architectures via `get_mha_kv_ptrs_with_pp` / `get_mla_kv_ptrs_with_pp`.
`TpModelWorker.forward_batch_generation_split_{init,layer,sample}()` — three-phase split forward: init attention backend → run N layers per call → sample after last group.
Call chain
```
event_loop_normal_disagg_prefill
→ _get_pipeline_group_size(batch)
→ >0: run_batch_pipelined → split_init → [split_layer + send_layer] × N → split_sample
→ 0: run_batch (unchanged)
```
Event loop compatibility
Pipelined mode integrates with
event_loop_normal_disagg_prefillonly. Combining with overlap mode (event_loop_overlap_disagg_prefill) provides no additional benefit — pipelined already eliminates the.cpu()index sync that overlap mode defers (pipelined pre-computes indices before the forward loop andprocess_batch_resultwithpipelined=Trueskipssend_kv_chunkentirely), and the per-group indices must be available during the forward pass so they cannot be deferred to the next iteration.Adaptive group_size (E1)
Instead of a fixed
SGLANG_PIPELINE_GROUP_SIZE, group_size is automatically computed via a continuous formula that adapts to both prompt length and model depth:Why prompt length matters — Pipeline total time depends on which is the bottleneck:
total = C + T/N— last group's transfer is exposedtotal = C/N + T— first group's compute is exposedShort prompts have higher T/C ratio (attention is O(n²) but transfer is O(n)), so more groups (larger N) are needed to reduce the exposed
T/NorC/N. Long prompts have compute dominating (T≪C), even few groups hide most transfer.Why model depth is handled automatically —
target_iters(number of pipeline stages) determines the overlap fraction(N-1)/N, which depends on T/C ratio, not on total layer count. Thenum_layers // target_itersdivision naturally produces appropriate group sizes for any depth (e.g., 80-layer → 8 layers/group, 32-layer → 3 layers/group at target_iters=10).Configurable env vars (two new, both optional):
SGLANG_PIPELINE_MAX_ITERSSGLANG_PIPELINE_MIN_ITERSTuning guide: Poor bandwidth → increase
MAX_ITERS(e.g., 12) so network starts sooner; excellent bandwidth → decreaseMIN_ITERS(e.g., 3) to reduce dispatch overhead.User can still override with a fixed value via
SGLANG_PIPELINE_GROUP_SIZEenv var (backward compatible).Different TP support (E2)
`send_kvcache_layer()` supports MHA head slicing when prefill TP ≠ decode TP, using vectorized numpy addressing (same math as `send_kvcache_slice`). MLA is TP-invariant and needs no slicing.
Mamba/SWA/NSA state support (E4)
Hybrid models (Jamba, FalconH1, DeepSeek-R1 with SWA) are fully supported. `_prepare_pipelined_state_indices()` pre-computes state indices before the layer loop, then passes them through `send_layer(state_indices=...)` on the last layer to trigger `maybe_send_extra()`. This covers:
No decode-side changes needed — decode already waits for all data (KV + state) before starting.
Universal guard + FalconH1 support (E7)
A universal `hasattr(model, "forward_split_prefill")` guard replaces the previous multimodal-only guard. This ensures:
FalconH1 (Mamba hybrid) now has `forward_split_prefill`, enabling layer-pipelined transfer. Each layer's attention produces KV cache (transferred per-layer via pipeline), while SSM state is sent once at the end via `maybe_send_extra()` (SSM state is fixed-size, independent of sequence length — no benefit from per-layer pipelining).
MTP/EAGLE compatibility (E8)
Reviewed and confirmed that `process_batch_result_pipelined_prefill` correctly propagates EAGLE `spec_info` (`topk_p`, `topk_index`, `hidden_states`) to requests — identical to the normal path. MTP decode-side rollback is purely a decode-phase operation with no interaction with prefill-time pipelined transfer. Also aligned `copy_done.synchronize()`, `routed_experts_output.finalize()`, and `maybe_cache_unfinished_req` with the normal result handler.
Zero regression guarantee
When `SGLANG_PIPELINED_KV_TRANSFER=false` (default):
Code equivalence (v0.4.10.post2 → this PR)
Benchmarks were collected on v0.4.10.post2. This PR ports the same logic to upstream main with these adaptations:
Checklist
send_kvcache_layerMLA path), but DeepSeek models lackforward_split_prefilldue to complex TBO/A2A/CP interactions — safely fallback via universal guard. Note: MLA compresses KV ~4.6x (1152 B vs 4096 B/token/layer for GQA), so pipelined benefit is inherently smaller (~15-30% TTFT reduction vs 48-68% for GQA models); prioritizing GQA/MHA models is intentional.Modified Files
Future Work
send_layer()is ~160 LOC. Planned as follow-up PR after this merges.CI States
Latest PR Test (Base): ❌ Run #27007567360
Latest PR Test (Extra): ❌ Run #27007567259