[Core][KV-transfer] MoRIIO: heterogeneous TP<->DP prefill/decode read routing - #46116
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Once the PR is approved and ready to go, your PR reviewer(s) can run CI to test the changes comprehensively before merging. To run CI, PR reviewers can either: Add If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
|
Documentation preview: https://vllm--46116.org.readthedocs.build/en/46116/ |
|
Tagging co-author chaeminlim-mb (Chaemin Lim, MangoBoost), who co-authored the fixes tracked in this issue, so he is looped in and can follow progress here. |
|
This pull request has merge conflicts that must be resolved before it can be |
90da56a to
72a6583
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
72a6583 to
faaca6d
Compare
…, not hardcoded DP0 The READ path resolved the remote session and the read-completion notify port from prefill DP rank 0, regardless of which prefill DP rank actually computed the KV. Under heterogeneous DP prefill (e.g. DP8EP prefill -> TP decode) the proxy forwards the owning rank in kv_transfer_params["remote_dp_rank"], but the read still targeted DP0's memory registration -- wrong data, and out-of-range block ids where per-rank num_blocks differ. Thread remote_dp_rank through ReqMeta into _read_blocks and key the session + notify port by it. remote_dp_rank == 0 (the symmetric single-DP case) is byte-identical to prior behaviour. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Edwin Lim <edwin.lim@mangoboost.io>
…ill DP ranks Under heterogeneous DP prefill, each decode TP worker reads KV from whichever prefill DP rank owns the request, so across requests every worker must reach several prefill DP ranks. A lazy per-rank handshake on the read path lets the TP workers diverge -- one races ahead into a forward TP collective while a peer is still blocked in a handshake recv() -> 600s NCCL timeout. Handshake every referenced remote prefill DP rank up front in start_load_kv, gated once per engine, with a CPU all-reduce(MIN) barrier so all TP workers finish together and fail fast and uniformly on error. See _eager_handshake_all_dp_ranks for the full rationale. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Edwin Lim <edwin.lim@mangoboost.io>
…> DP-decode For a TP-prefill + MLA feeding a DP decode (the mirror config), the latent KV is replicated across every prefill TP rank, so any is a valid read source. Instead of every decode DP rank funnelling onto prefill tp0, round-robin each decode rank's reads across tp0..N-1 to spread RDMA/prefill-NIC load. The eager handshake pre-warms a session to every (dp, tp) rank; _resolve_read_source picks the source TP rank (deterministic staggered round-robin when the gate holds -- world_size==1 + MLA + pure-TP prefill + tp_size>1 -- else the fixed local-rank mapping, byte-identical to prior behaviour); and the chosen rank is threaded identically into the (dp, tp) session key, the handshake dial and the notify port so a read and its completion notify always address the same prefill rank. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Edwin Lim <edwin.lim@mangoboost.io>
…a published host list Stacked on vllm-project#46116 (heterogeneous TP<->DP read routing): consumes its remote_dp_rank plumbing instead of duplicating it. Adds: - ReqMeta.remote_hosts + node_hosts / trusted_remote_hosts config plumbing (moriio_common) so a decode worker dials the prefill host that owns its KV slice (remote_hosts[node index]); single-host / monolithic flow is byte-identical (remote_host unchanged). - request_id-derived and directly-supplied peer-host validation against an opt-in trusted_remote_hosts allowlist (no enforcement when unset, so the default cross-host flow is not rejected). - toy proxy forwards node_hosts to prefill and decode. The read path composes vllm-project#46116's chosen_tp (which rank) with this PR's _pick_remote_host (which host); the notify/release path targets the prefill node that owns the read rank's KV so it is freed on the right host. Signed-off-by: Chaemin Lim <chaemin.lim@mangoboost.io>
|
hi @tjtanaa this is one of the last PRs we require to get through. Do you giving it a review? |
What was tested
Functional and focused-test results
Accuracy: GSM8K 25-shotStrict exact match over all 1,319 prompts:
Performance: fixed-shape P/D benchmark
Commands used# Router
vllm-router \
--host 0.0.0.0 \
--port 30000 \
--vllm-pd-disaggregation \
--kv-connector moriio \
--vllm-discovery-address 0.0.0.0:36367 \
--policy consistent_hash \
--prefill-policy consistent_hash \
--decode-policy consistent_hash \
--log-level info
# Run once for the TP8 producer and once for the TP8 consumer with the
# corresponding role-specific MoRIIO configuration.
VLLM_MORIIO_CONNECTOR_READ_MODE=1 \
VLLM_USE_V1=1 \
VLLM_ROCM_USE_AITER=1 \
VLLM_USE_BREAKABLE_CUDAGRAPH=0 \
vllm serve "$MODEL_SNAPSHOT" \
--served-model-name MiniMaxAI/MiniMax-M3-MXFP8 \
--host 0.0.0.0 \
--port "$ROLE_HTTP_PORT" \
--trust-remote-code \
--tensor-parallel-size 8 \
--block-size 128 \
--language-model-only \
--kv-cache-dtype fp8 \
--attention-backend TRITON_ATTN \
--no-enable-prefix-caching \
--gpu-memory-utilization 0.90 \
--tool-call-parser minimax_m3 \
--reasoning-parser minimax_m3 \
--enable-auto-tool-choice \
--kv-transfer-config "$ROLE_SPECIFIC_MORIIO_CONFIG"
# Run twice per arm; lm_eval==0.4.12
lm_eval run \
--model local-completions \
--model_args model=MiniMaxAI/MiniMax-M3-MXFP8,base_url=http://127.0.0.1:30000/v1/completions,num_concurrent=200,max_retries=10,max_gen_toks=2048,max_length=1048576,timeout=60000 \
--tasks gsm8k \
--num_fewshot 25 \
--batch_size auto \
--device cpu \
--seed 0,1234,1234,1234 \
--output_path "$GSM8K_OUTPUT" \
--log_samples# Warmup: NUM_PROMPTS=128 and SAVE_ARGS=().
# Measured: NUM_PROMPTS=1280 and
# SAVE_ARGS=(--save-result --save-detailed --result-dir "$PERF_OUTPUT" --result-filename result.json).
vllm bench serve \
--backend openai \
--host 127.0.0.1 \
--port 30000 \
--endpoint /v1/completions \
--model "$MODEL_SNAPSHOT" \
--served-model-name MiniMaxAI/MiniMax-M3-MXFP8 \
--tokenizer "$MODEL_SNAPSHOT" \
--dataset-name random \
--random-input-len 8192 \
--random-output-len 1024 \
--random-range-ratio 0.0 \
--num-prompts "$NUM_PROMPTS" \
--num-warmups 0 \
--max-concurrency 128 \
--request-rate inf \
--seed 0 \
--ignore-eos \
--trust-remote-code \
--percentile-metrics ttft,tpot,itl,e2el \
"${SAVE_ARGS[@]}" |
|
@tjtanaa @tanpinsiang Thank you so much for the review, testing, and approval!!! Currently I see that the CI fails, but the failing lanes are unrelated to this PR. The diff here is three MoRIIO files, and is unrelated to the failures.
Will re-merge main and retrigger once it stabilizes (picking up #49450 in the process). |
|
Hi @edwinlim0919, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
Signed-off-by: Edwin Lim <edwin.lim@mangoboost.io>
Head branch was pushed to by a user without write access
Part of RFC #46107.
Purpose
MoRIIO today assumes the prefill and decode engines share the same parallelism layout. This PR enables heterogeneous disaggregated prefill/decode, where the two phases run different parallelism (TP vs DP+EP). The performance results in the RFC which encompasses this PR shows that heterogeneous parallel PD setups can achieve higher throughput and lower TTFT at high concurrency levels than their homogeneous parallel PD counterparts on MI300X (gfx942). This PR contains the support for both mixed-parallel configurations below:
P_DP8EP → D_TP8(DP/EP prefill, TP decode)P_TP8 → D_DP8EP(TP prefill, DP/EP decode)Three focused changes, one per commit:
1. Read from the request's prefill DP rank, not hardcoded DP0
Previously the READ path resolved the remote session and read-completion notify port from a hardcoded prefill DP rank 0, regardless of which prefill DP rank actually computed the KV. Under heterogeneous DP prefill the proxy forwards the owning rank in
kv_transfer_params["remote_dp_rank"]; the read now targets it.remote_dp_rank == 0(the symmetric single-DP case) is byte-identical to prior behaviour.2. Eager pre-forward handshake (deadlock fix)
A decode TP worker reads from whichever prefill rank owns each request, so across requests every worker must reach several remote ranks. The previous lazy per-rank handshake on the read path lets the TP workers diverge. One rank can race ahead into a forward collective (TP all-gather, or the DP+EP all-to-all on the decode side) while a peer is still blocked in a handshake
recv()→ 600s NCCL timeout / hang. We handshake every referenced remote rank up front instart_load_kv, once per engine, behind a CPUall_reduce(MIN)barrier so all workers finish together and fail fast + uniformly on error.3. Flexible prefill-TP read (mirror)
For a TP-replicated prefill + MLA feeding a DP decode, the latent KV is replicated across every prefill TP rank, so any is a valid source. Instead of every decode DP rank funnelling onto prefill tp0 (a single-GPU NIC bottleneck), each decode rank round-robins its reads across
tp0..N-1to spread the RDMA/prefill-NIC load. The choice is gated (all four must hold, so non-mirror configs are untouched):world_size == 1: decode is TP1, no local TP collective to keep in lockstep;use_mla: latent KV replicated across prefill TP ranks → any rank valid;remote_dp_size == 1: pure-TP prefill (no DP partition constraining source);remote_tp_size > 1: more than one prefill TP rank to spread across.Outside the gate the source rank is the fixed local-rank mapping (
_remote_tp_rank), byte-identical to prior behaviour. The chosen rank is threaded identically through the(dp, tp)session key, the handshake dial, and the notify port, so a read and its completion notify always address the same prefill rank. A drift would read one rank but notify another, leaving that rank's prefill buffer unfreed (KV leak → MR overflow).The round-robin is deterministic (staggered by the decode rank's
dp_rank), not random: it gives exactly-uniform NIC spread and is trivially testable.Affected files
vllm/distributed/kv_transfer/kv_connector/v1/moriio/moriio_connector.pyvllm/distributed/kv_transfer/kv_connector/v1/moriio/moriio_common.pytests/v1/kv_connector/unit/test_moriio_routing_fairness.pyTest Plan
tests/v1/kv_connector/unit/test_moriio_routing_fairness.py: dependency-light (no GPU/ROCm/mori), drives the real routing functions (_resolve_read_source,_next_flex_tp_rank) and asserts hardware-fair prefill-read distribution acrossevery homogeneous and mixed P/D config in the RFC, plus the flexible gate, the symmetric-TP bijection, owner-DP coverage, round-robin determinism/stagger, and the read/notify same-rank threading.
AI assistance
This PR was prepared with AI assistance (Claude). The submitter has reviewed every
changed line and is responsible for the change end-to-end.