[Feature] Support arbitrary attention-to-FFN rank ratios in P2pNcclAFDConnector - #309
[Feature] Support arbitrary attention-to-FFN rank ratios in P2pNcclAFDConnector#309swjeong9 wants to merge 1 commit into
Conversation
…DConnector `validate_p2p_topology` rejected every layout except `A >= F` with `A % F == 0`, so `A < F` and non-divisible ratios could not be expressed. Generalize the subgroup partition to a balanced block distribution over `G = min(A, F)` groups: attention rank `a` joins group `a * G // A` and FFN rank `f` joins group `f * G // F`. Both maps are surjective, so no group is left without a peer. The previously supported layouts are the `G == F` case of this formula and produce the same groups, so their rank mapping and execution path are unchanged. On the transport side a group is always one member of the smaller role plus several of the larger one. For `A >= F` this is the existing gather. For `A < F` the attention rank splits its tokens across its FFN members and concatenates the results back. Split sizes come from `split_send_sizes`, a closed form that avoids branching on the remainder so the arithmetic survives `torch.compile` tracing with a symbolic token dimension. Slices are passed to the custom ops as (start, size) pairs rather than as views, because a view with a non-zero storage offset reached the op at offset 0 in generated inductor code. An FFN rank whose share is zero still runs a one-token dummy batch so it can take part in the expert all-to-all inside the FFN role. This also fixes subgroup rendezvous for multi-node deployments. Every subgroup used the configured `afd.host` for its `StatelessProcessGroup`, but the socket is opened by that group's first member, so a second subgroup whose first member lives on another node could never be reached. Each rank now exchanges its own address once over the existing AFD group, and each subgroup dials its first member. On a single host the resolved address equals the configured host, leaving existing recipes unchanged. The GPU results for this change were all collected across several nodes and are not reproducible without it. vLLM sources are untouched and no compat patch is added. Signed-off-by: swjeong9 <swjeong25@gmail.com>
|
cc @specture724 |
|
Thanks for sharing the implementation and measurements. I am not sure eager-mode AFD performance with P2pNcclAFDConnector is particularly meaningful for its intended use case: the decode stage of a prefill/decode-disaggregated deployment, with CUDA graphs in FULL_DECODE_ONLY mode. So far, we have not identified a concrete use case for A < F. Our typical deployments use A > F to improve FFN compute utilization, which is why we have not implemented A < F support. Without a concrete application scenario and performance evidence for it under the intended execution mode, we would be reluctant to merge the additional software complexity and maintenance burden. If you have a use case that benefits from A < F, along with corresponding performance results, please share them—we would be happy to revisit this with that evidence. |
|
Thank you for the thoughtful review. The changes in this PR are of three different kinds, so we have laid them out one by one below. On the measurementsTo clarify the measurement setup, since the PR description may not have made this clear: we measured in both eager and graph mode (
The three changes in this PR1. Allowing We fully agree with your point about the intended regime: since AFD batches tokens from several attention ranks to fill the FFN, We are a research group working on optimizing LLM serving systems on heterogeneous GPU clusters. We expected that if attention is placed on GPUs with high memory bandwidth and capacity, and the FFN on GPUs with better compute efficiency, a layout with fewer attention ranks than FFN ranks could arise. Our reference point was MegaScale-Infer, which introduced attention-FFN disaggregation:
That said, we recognize this is a special case specific to our research setting, not a requirement for typical deployments. 2. Relaxing This one is separate from
3. Multi-node deployment Currently
Two questionsOur understanding is that your concern centers on item 1. Most of this PR's complexity is also in item 1 (the slice ops, the dummy batch for a rank that receives zero tokens, and their handling under So we would like to ask:
We would appreciate your guidance on this, and will gladly reshape the PR whichever way you prefer. Thank you again for your time. |
|
Thanks for the clarification. I agree with item 2—relaxing the divisibility constraint while keeping A >= F is something we can support. For item 3, could you open a separate bug issue with a reproducer and the deployment details? We would like to reproduce and investigate it on our side as well. Regarding the eager versus FULL_DECODE_ONLY + DBO measurements, could you verify that FULL_DECODE_ONLY is actually hitting CUDA graph replay? The reported numbers make me suspect that graph replay may not be taking place and execution may be falling back to eager mode. Please check the actual execution path to confirm. |
Purpose
This implements what we discussed in RFC #308. It removes the
A >= FandA % F == 0constraints invalidate_p2p_topologyand generalizes the subgroup partition to a balanced block distribution overG = min(A, F)groups. The currently supported topologies are the special case of that formula and produce the same groups. With no mode switch and no new config,A < Fand non-divisible ratios become expressible. We measured1A2Fand1A4F.It also carries a fix for subgroup rendezvous failing in multi-node deployments. Since
docs/gpu/NCCL_P2P_CONNECTOR_USER_GUIDE.mdline 178 records cross-node use as "not established by the current recipes", we went and checked. Initialization hangs whenevernum_ffn_ranks >= 2and the FFN ranks sit on different nodes, and it reproduces on the currently supported4A2Fas well. The cause is that every subgroup'sStatelessProcessGroupuses the singleadditional_config.afd.hostas its rendezvous address. The socket for a group is opened by that group's first member (an FFN rank); on one host every first member shares the address so nothing goes wrong, but once the ranks are spread out, the first member of the second subgroup cannot bind that address and the other members cannot find it there. We considered sending this separately, since it is a different kind of change, but all of the GPU evidence in this PR is multi-node and none of it is reproducible without the fix. We are happy to split it out if you prefer.Issue
Scope
distributed/topology.py: drop the constraint, generalize the partition arithmetic, addsplit_send_sizes, correct the rank offset for the DP metadata channel.connectors/gpu/p2p.py: drive the boundary transfer from the group roster and the split sizes; add two slice ops used only underA < F; the address used to create each subgroup.v1/worker/ffn_metadata.py: aggregate per-FFN token counts with the same partition formula.CAMP2pAFDConnector,CAMAsyncAFDConnector). They carry the same constraint, but we have no hardware to verify a change there.p2pcontrol-plane group. They keep using the configuredhost, which is correct because world rank 0 owns that address.Implementation Notes
G = min(A, F)groups, attentionajoins groupa*G//Aand FFNfjoins groupf*G//F. Both maps are surjective, so no subgroup is left empty. WhenA >= F && A % F == 0the grouping is the same as today.A >= Fbehaves as before; underA < Fthe attention rank splits its tokens across the FFN members and concatenates the results back. Sizes are computed in one place,split_send_sizes; the sender uses the whole list and the receiver its own entry, so both reach the same answer without extra communication.torch.compile: the attention-side transfer sits inside the compiled region, so we kept to two rules. First, slice boundaries are passed as op arguments and the slicing happens inside the op implementation. When we passed a view with a non-zero storage offset to the existing op, the generated code delivered it at offset 0 and the second FFN member received the first member's tokens; we hit this on GPU. Second, the split sizes use a closed form rather than a branch on the remainder, and we do not skip the transfer to a peer whose share is zero, because a branch on the token count freezes at its trace-time value.A < F.connecton a UDP socket makes the kernel pick a route without sending a packet, so the peer need not exist yet and nothing goes on the wire. We judged this safer than a name lookup on a node with several interfaces. On a single host the resolved value equals the configuredhost(127.0.0.1stays127.0.0.1), so existing recipes behave exactly as before. Having each rank take its own address from config would also work; we chose automatic resolution because the alternative grows the config surface and has to be filled in per deployment. We are glad to switch to the explicit form if you prefer it.Test Plan
A, Fin1..6, plus literal examples.split_send_sizes.2A2Fspread across nodes, before and after the rendezvous fix.Test Result
CPU:
pytest tests/unit/— 1075 passed, 51 skipped, 2 failed. The two failures are intests/unit/test_e2e_process_utils.py, which needsos.pidfd_open; our development machine is macOS, and they fail there onmainas well, without this change. We modified one existing test: the one asserting the list of registered custom ops, to include the two new ops.GPU: L4 (g6.xlarge), one rank per pod, vLLM multi-node DP, DeepSeek-V2-Lite. Node counts differ per configuration (
1A2Fthree,2A2Ffour,1A4Ffive). We sent 32 prompts sequentially attemperature 0. Below,graph+DBOmeans CUDA graph capture (cudagraph_mode=FULL_DECODE_ONLY) together with--enable-dbo.Rendezvous fix
On
2A2Facross four nodes, the members of the second subgroup timed out at the rendezvous after 120 s before the fix, and came up and served after it. Everything below was collected with the fix in place.Existing topology (
2A2F, current code vs this PR)We checked output equality as follows. We took the token sequence the current code had generated (128 tokens per prompt), fed it back to both runs as input, and collected the probability each run assigned to that token at each position. This scores rather than generates, so both runs look at the same positions and all 32 x 128 = 4096 of them line up one to one.
Latency and throughput are measured with 1024 input and 32 output tokens. Repeating the same configuration moves the number by roughly 8%, so we read the differences above as noise.
New topologies
A different topology means a different batch composition and therefore different bf16 rounding, so the generated token sequences diverge in places. Every divergence we saw was at a position where the top two candidates had nearly equal probability. To measure the size of the difference we used the same method as above, feeding the token sequence generated by the current code at
2A2Feager into each configuration and comparing per-position probabilities.1A2Feager1A2Fgraph+DBO1A4Feager1A4Fgraph+DBOTo establish a reference point, we kept the topology at
2A2Fand changed only the execution mode of the current code to graph+DBO, then measured the same way. That gave 0.9%. In other words, the difference introduced by changing the topology is the same size as the difference that changing only the execution mode already produces.We are also recording the latency and throughput each configuration produced. The GPU counts and batch compositions differ, so these are not values to compare against each other. Latency is 4 requests sent one at a time, and throughput is 256 requests sent at once.
1A2Fgraph+DBO1A4Fgraph+DBO1A2Fand1A4Fcannot start on the current code — they raise theValueErrorattopology.py:56— and came up on this PR to produce the results above.Docs Impact
docs/design/module/connector_contracts.mdstatesnum_attention_ranks >= num_ffn_ranksas a contract at line 113, anddocs/gpu/NCCL_P2P_CONNECTOR_USER_GUIDE.mdalso describes the supported topologies, so both look like they need updating. Tell us the shape you would like and we will include it in this PR.Essential PR Checklist