Update (see the comments below + PR #1495): the original recipe here uses protocol=tcp, but TCP is not the recommended single-node transport — it serializes under concurrency (the "TTFT blow-up" in the perf table below). The correct transport is protocol=hip (XGMI, GPU-direct IPC), which restores scaling (~4× throughput / ~7× TTFT at c4, matching aggregated TP=4). Also, the c16 "hang" was not a transport issue — it's a prefill-engine OOM from a per-rank KV-budget imbalance (fix in PR #1495). protocol=tcp still works out-of-the-box (the stock image's mooncake is Tcp+Rdma only), so the recipe below is valid as the slow fallback; use protocol=hip with a -DUSE_HIP=ON mooncake build for real performance. Full analysis in the comment thread.
Summary
atom/kv_transfer/disaggregation/README.md documents the MORI-IO RDMA path for prefill/decode (P/D) disaggregation, but PR #1308 ("Support PD disaggregation on Single node") also enables a single-node setup over the Mooncake connector that works without an RDMA NIC (protocol=tcp). That path is currently undocumented. This issue requests a short "single-node 4P4D" how-to in the README, and records a validated recipe + the gotchas hit while bringing it up.
Validated on one 8×MI355X (gfx950) node, ATOM main, model deepseek-ai/DeepSeek-V4-Pro, split into 4-GPU prefill + 4-GPU decode. Requests returned correct output with kv_transfer_params.do_remote_prefill=true, decode TPOT matching an aggregated TP=8 server.
Proposed how-to (validated recipe)
All three processes run on the same node. The prefill and decode engines each use TP=4; requests go to the proxy.
The recipe below uses protocol=hip (XGMI) — the recommended transport — which needs both engines to see all 8 GPUs in a rotated order so each engine's own 4 GPUs are cuda:0-3 while the peer's are visible for IPC. For the TCP fallback (works on the stock Tcp+Rdma-only mooncake build, but serializes under concurrency), set "protocol":"tcp" and use disjoint visibility instead (HIP_VISIBLE_DEVICES=0,1,2,3 / 4,5,6,7). --max-num-seqs 128 avoids the prefill-engine OOM described in the comments / PR #1495 (until that fix lands).
- Proxy
python -m atom.kv_transfer.disaggregation.proxy --port 10001
- Prefill (kv_producer, own GPUs = physical 0-3)
HIP_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
TORCHINDUCTOR_CACHE_DIR=/tmp/ind_p TRITON_CACHE_DIR=/tmp/tri_p HOME=/tmp/home_p \
python -m atom.entrypoints.openai_server \
--model deepseek-ai/DeepSeek-V4-Pro \
--kv_cache_dtype bf16 -tp 4 --gpu-memory-utilization 0.85 --max-num-seqs 128 \
--host 0.0.0.0 --server-port 8003 \
--kv-transfer-config '{"kv_connector":"mooncake","kv_role":"kv_producer","protocol":"hip","proxy_ip":"127.0.0.1","proxy_ping_port":36367,"http_port":8003,"handshake_port":6301}'
- Decode (kv_consumer, own GPUs = physical 4-7, rotated to
cuda:0-3)
HIP_VISIBLE_DEVICES=4,5,6,7,0,1,2,3 \
TORCHINDUCTOR_CACHE_DIR=/tmp/ind_d TRITON_CACHE_DIR=/tmp/tri_d HOME=/tmp/home_d \
python -m atom.entrypoints.openai_server \
--model deepseek-ai/DeepSeek-V4-Pro \
--kv_cache_dtype bf16 -tp 4 --gpu-memory-utilization 0.85 --max-num-seqs 128 \
--host 0.0.0.0 --server-port 8004 \
--kv-transfer-config '{"kv_connector":"mooncake","kv_role":"kv_consumer","protocol":"hip","proxy_ip":"127.0.0.1","proxy_ping_port":36367,"http_port":8004,"handshake_port":7401}'
- Send requests to the proxy
curl -s http://127.0.0.1:10001/v1/completions \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-ai/DeepSeek-V4-Pro","prompt":"The capital of France is","max_tokens":16,"temperature":0}'
The response's kv_transfer_params.do_remote_prefill should be true, confirming the KV was computed on the prefill engine and transferred to decode.
Setup notes / gotchas worth documenting
These were not obvious from the README and each cost time:
- Connector selection:
kv_connector defaults to moriio (RDMA). Single-node requires "kv_connector":"mooncake". Prefer "protocol":"hip" (XGMI GPU-direct IPC; needs a -DUSE_HIP=ON -DUSE_MNNVL=ON mooncake build + the all-8-GPU rotated visibility above). "protocol":"tcp" is the no-RDMA-NIC fallback that works on the stock build but serializes under concurrency (default protocol is rdma). TransferEngine.initialize(..., "tcp"/"hip", "") returns 0 and ignores the auto-selected rdma{gpu} device.
- Bind
--host 0.0.0.0: each engine reports its real IP (get_ip()) to the proxy. With --host 127.0.0.1 the proxy cannot reach the engine and returns 500.
- Distinct
handshake_port per engine on the same node (e.g. 6301 / 7401); otherwise the intra-node ZMQ side channels collide.
- Per-engine compile-cache isolation: two same-model engines on one node race on the inductor/triton cache (
torch.compile "Guard failed" crash) and on ~/.cache/atom. Set distinct TORCHINDUCTOR_CACHE_DIR / TRITON_CACHE_DIR (and separate HOME) per engine.
- cudagraph works for single-node PD over TCP (no
--enforce-eager needed; capture ~7s per engine).
- Restart the proxy after (re)launching engines: a long-lived proxy retains stale registrations for engines that were restarted and then returns empty responses for new requests. Restarting the proxy alone makes both engines re-register via the ping channel (no model reload). A stale-registration eviction / health check on the proxy would remove this footgun.
- DeepSeek-V4 specifics: the Mooncake connector already handles the V4 pools (
register_v4_kv_caches, block + slot regions) — registration logs 154 chunks (91 block regions, 62 slot regions), i.e. classical KV + SWA per-request slots + indexer cache are all transferred.
An "8-GPUs-visible reorder" variant (prefill HIP_VISIBLE_DEVICES=0,1,2,3,4,5,6,7, decode 4,5,6,7,0,1,2,3) also works over TCP and is the prerequisite for an intra-node XGMI/IPC transport; disjoint visibility (above) is the simpler default for TCP.
Observed behavior (for discussion)
Single-node 4P4D over TCP, deepseek-ai/DeepSeek-V4-Pro, ISL/OSL = 1024/1024, bf16 KV, cudagraph. Output throughput / median TTFT / median TPOT vs an aggregated TP=8 server on the same node:
| concurrency |
4P4D PD (TCP) |
Aggregated TP=8 |
| 1 |
70 tok/s / 0.23 s / 13.9 ms |
76 tok/s / 139 ms / 13.0 ms |
| 4 |
91 tok/s / 25.0 s / 13.9 ms |
270 tok/s / 127 ms / 14.2 ms |
| 16 |
140 tok/s / 56.0 s / 14.8 ms |
902 tok/s / 143 ms / 17.3 ms |
Decode TPOT tracks the aggregated server (~14 ms), but median TTFT grows steeply with concurrency (0.23 s → 25 s → 56 s) and end-to-end throughput stays low. It looks like requests serialize waiting on the single prefill engine + per-request proxy/transfer coordination rather than on prefill compute (a 1k-token prefill is ~150 ms). Is this expected for single-node TCP PD, or is there a recommended configuration (e.g. multiple prefill instances, DP-attention, batching of transfers) to avoid the TTFT blow-up? Documenting the intended operating point would help.
Ask
- Add a "single-node (no RDMA NIC) via Mooncake TCP" section to
atom/kv_transfer/disaggregation/README.md with the recipe above.
- Confirm the intended concurrency/operating point for single-node TCP PD (the TTFT scaling above).
- Consider a proxy stale-registration health check so a restarted engine doesn't require a manual proxy restart.
Happy to open a docs PR for item 1 if useful.
Summary
atom/kv_transfer/disaggregation/README.mddocuments the MORI-IO RDMA path for prefill/decode (P/D) disaggregation, but PR #1308 ("Support PD disaggregation on Single node") also enables a single-node setup over the Mooncake connector that works without an RDMA NIC (protocol=tcp). That path is currently undocumented. This issue requests a short "single-node 4P4D" how-to in the README, and records a validated recipe + the gotchas hit while bringing it up.Validated on one 8×MI355X (gfx950) node, ATOM
main, modeldeepseek-ai/DeepSeek-V4-Pro, split into 4-GPU prefill + 4-GPU decode. Requests returned correct output withkv_transfer_params.do_remote_prefill=true, decode TPOT matching an aggregated TP=8 server.Proposed how-to (validated recipe)
All three processes run on the same node. The prefill and decode engines each use TP=4; requests go to the proxy.
The recipe below uses
protocol=hip(XGMI) — the recommended transport — which needs both engines to see all 8 GPUs in a rotated order so each engine's own 4 GPUs arecuda:0-3while the peer's are visible for IPC. For the TCP fallback (works on the stock Tcp+Rdma-only mooncake build, but serializes under concurrency), set"protocol":"tcp"and use disjoint visibility instead (HIP_VISIBLE_DEVICES=0,1,2,3/4,5,6,7).--max-num-seqs 128avoids the prefill-engine OOM described in the comments / PR #1495 (until that fix lands).cuda:0-3)The response's
kv_transfer_params.do_remote_prefillshould betrue, confirming the KV was computed on the prefill engine and transferred to decode.Setup notes / gotchas worth documenting
These were not obvious from the README and each cost time:
kv_connectordefaults tomoriio(RDMA). Single-node requires"kv_connector":"mooncake". Prefer"protocol":"hip"(XGMI GPU-direct IPC; needs a-DUSE_HIP=ON -DUSE_MNNVL=ONmooncake build + the all-8-GPU rotated visibility above)."protocol":"tcp"is the no-RDMA-NIC fallback that works on the stock build but serializes under concurrency (default protocol isrdma).TransferEngine.initialize(..., "tcp"/"hip", "")returns 0 and ignores the auto-selectedrdma{gpu}device.--host 0.0.0.0: each engine reports its real IP (get_ip()) to the proxy. With--host 127.0.0.1the proxy cannot reach the engine and returns 500.handshake_portper engine on the same node (e.g. 6301 / 7401); otherwise the intra-node ZMQ side channels collide.torch.compile"Guard failed" crash) and on~/.cache/atom. Set distinctTORCHINDUCTOR_CACHE_DIR/TRITON_CACHE_DIR(and separateHOME) per engine.--enforce-eagerneeded; capture ~7s per engine).register_v4_kv_caches, block + slot regions) — registration logs154 chunks (91 block regions, 62 slot regions), i.e. classical KV + SWA per-request slots + indexer cache are all transferred.An "8-GPUs-visible reorder" variant (prefill
HIP_VISIBLE_DEVICES=0,1,2,3,4,5,6,7, decode4,5,6,7,0,1,2,3) also works over TCP and is the prerequisite for an intra-node XGMI/IPC transport; disjoint visibility (above) is the simpler default for TCP.Observed behavior (for discussion)
Single-node 4P4D over TCP,
deepseek-ai/DeepSeek-V4-Pro, ISL/OSL = 1024/1024, bf16 KV, cudagraph. Output throughput / median TTFT / median TPOT vs an aggregated TP=8 server on the same node:Decode TPOT tracks the aggregated server (~14 ms), but median TTFT grows steeply with concurrency (0.23 s → 25 s → 56 s) and end-to-end throughput stays low. It looks like requests serialize waiting on the single prefill engine + per-request proxy/transfer coordination rather than on prefill compute (a 1k-token prefill is ~150 ms). Is this expected for single-node TCP PD, or is there a recommended configuration (e.g. multiple prefill instances, DP-attention, batching of transfers) to avoid the TTFT blow-up? Documenting the intended operating point would help.
Ask
atom/kv_transfer/disaggregation/README.mdwith the recipe above.Happy to open a docs PR for item 1 if useful.