Topology and role ranks: the AFD world
Code version: afd-plugin @ 7452d52 · 2026-10-27 · vLLM @ ced6857
In one sentence
The P2P connector builds one AFD world, ordered [F0..A0..]. The topology sizes decide the subgroups and the DP metadata routes.
The idea#
Every AFD rank has a role rank: its position inside its own role group. The role rank comes from the data-parallel, context-parallel and tensor-parallel placement. The P2P connector lays all ranks out in one world: the FFN ranks first, then the attention ranks.
Inside that world, each FFN rank owns a subgroup: itself plus the attention ranks mapped to it. The subgroups carry hidden states (the data plane). The control plane is narrower: only some attention ranks send DP metadata. The connector needs at least as many attention ranks as FFN ranks.
An everyday comparison. Think of delivery routes. Each van (FFN rank) serves a block of addresses (attention ranks). The addresses are split into blocks that differ in size by at most one. The comparison stops here: the "addresses" also report back to the depot on a second, smaller list of routes (the control plane), which does not match the delivery blocks one-to-one.
The picture#
Read panel 1 as the world order and the numbering rule. Read panels 2 and 3 as two examples: the planes agree for 2A2F, but differ for 4A2F.
This interactive visual needs JavaScript. The rules it shows are in the steps below:
the world order (afd_plugin/distributed/topology.py:135), the subgroups
(afd_plugin/distributed/topology.py:136) and the DP-metadata routes
(afd_plugin/distributed/topology.py:164-170).
A, F and the attention TP. The visual shows the world order, the subgroups and the DP-metadata routes. It uses the rules from topology.py and p2p.py; it folds DP and TP into the role rank.In the code#
Step 1: The role rank#
resolve_role_rank linearizes the global data-parallel rank and the local context-parallel and
tensor-parallel ranks (afd_plugin/distributed/topology.py:61-105). The formula is one line
(afd_plugin/distributed/topology.py:92).
role_rank = (dp_rank * pcp_size + pcp_rank) * tp_size + tp_rank
if config.role == "attention":
role_size = config.num_attention_ranks
elif config.role == "ffn":
role_size = config.num_ffn_ranks
else:
raise ValueError(f"unknown AFD role {config.role!r}")
if not 0 <= role_rank < role_size:
raise ValueError(
"AFD role rank derived from distributed ranks is out of range: "
f"role={config.role!r}, dp_rank={dp_rank}, pcp_rank={pcp_rank}, "
f"tp_rank={tp_rank}, role_size={role_size}",
)The role rank must be inside the role size (num_attention_ranks for the attention role, or
num_ffn_ranks for the FFN role). An out-of-range rank raises
(afd_plugin/distributed/topology.py:99-104).
Step 2: The world order#
build_rank_mapping turns the role rank into a world rank
(afd_plugin/distributed/topology.py:118-184). FFN ranks keep their role rank; attention ranks come
after all FFN ranks (afd_plugin/distributed/topology.py:135,
afd_plugin/distributed/topology.py:143).
if config.role == "attention":
if role_rank >= attention_size:
raise ValueError(
"Attention role rank must be within attention size "
f"(rank={role_rank}, size={attention_size})",
)
world_rank = ffn_size + role_rank
subgroup_index = role_rank * ffn_size // attention_size
elif config.role == "ffn":
if role_rank >= ffn_size:
raise ValueError(
"FFN role rank must be within FFN size "
f"(rank={role_rank}, size={ffn_size})",
)
world_rank = role_rank
subgroup_index = role_rankStep 3: The subgroups#
Each attention rank a joins the subgroup of FFN rank a * F // A
(afd_plugin/distributed/topology.py:136). The reverse lookup lists a subgroup's attention ranks as a
contiguous block (afd_plugin/distributed/topology.py:108-115). The FFN rank is always subgroup rank
0 (afd_plugin/distributed/topology.py:153-161).
min_size = min(ffn_size, attention_size)
subgroup_attention_ranks = [
ffn_size + attention_rank
for attention_rank in subgroup_attention_block(
subgroup_index, attention_size, ffn_size
)
]
subgroup_ranks = tuple([subgroup_index] + subgroup_attention_ranks)
rank_in_subgroup = subgroup_ranks.index(world_rank)Step 4: The A ≥ F rule#
The P2P connector needs at least as many attention ranks as FFN ranks, so each FFN rank owns at least one
attention peer. validate_p2p_topology raises otherwise
(afd_plugin/distributed/topology.py:52-58).
def validate_p2p_topology(config: AFDConfig) -> None:
attention_size, ffn_size = topology_from_config(config)
if attention_size < ffn_size:
raise ValueError(
"P2pNcclAFDConnector currently requires num_attention_ranks >= "
f"num_ffn_ranks, got {attention_size} < {ffn_size}",
)Step 5: The DP-metadata routes#
The control plane uses fewer ranks than the data plane. Only the first min_size attention ranks
send DP metadata, where min_size = min(F, A). Under the A ≥ F rule,
min_size equals F, so the first F attention ranks are the senders, one
per FFN rank (afd_plugin/distributed/topology.py:164-170).
destinations: list[int] = []
if ffn_size <= world_rank < ffn_size + min_size:
local_attention_rank = world_rank - ffn_size
destination = local_attention_rank
while destination < ffn_size:
destinations.append(destination)
destination += min_sizeThe group that joins the control plane is the FFN ranks plus those first attention ranks
(afd_plugin/distributed/topology.py:42-43).
Step 6: The plugin-owned process groups#
The connector builds its process groups without patching vLLM. A helper creates each one from a rendezvous
at tcp://host:port (afd_plugin/distributed/afd_process_group.py:62-120). The GPU
connector calls it twice: once for the AFD world, and once for the control plane
(afd_plugin/connectors/gpu/p2p.py:269-314).
afd_pg = init_afd_process_group(
backend="nccl",
init_method=f"tcp://{self.afd_config.host}:{self.afd_config.port}",
world_size=self.ffn_size + self.attn_size,
rank=self.world_rank,
group_name="afd",
timeout=timedelta(seconds=self.afd_config.afd_process_group_timeout_s),
)
with DefaultProcessGroupSwitcher(_get_default_group(), afd_pg):
# The subgroup only has to hand rank 0's ncclUniqueId to its
# members, so it reuses the store the AFD world already
# rendezvoused on. StatelessProcessGroup.create would stand up a
# second store bound to ``host``, which is only correct while
# every subgroup's rank 0 sits on that host.
subgroup_store = PrefixStore(
f"afd_subgroup_{self.mapping.subgroup_index}",
afd_pg.get_group_store(),
)
subgroup_store.set_timeout(timedelta(seconds=300))
self.a2e_group = StatelessProcessGroup(
rank=self.mapping.rank_in_subgroup,
world_size=len(self.mapping.subgroup_ranks),
store=subgroup_store,
)
self.e2a_group = self.a2e_group
self.a2e_pynccl = PyNcclCommunicator(
group=self.a2e_group,
device=self.local_rank,
)
self.a2e_comm_id = _register_comm(self.a2e_pynccl)
self.e2a_pynccl = PyNcclCommunicator(
group=self.e2a_group,
device=self.local_rank,
)
self.e2a_comm_id = _register_comm(self.e2a_pynccl)
if self.mapping.participates_in_dp_metadata_group:
self.p2p_pg = init_afd_process_group(
backend="nccl",
init_method=f"tcp://{self.afd_config.host}:{self.afd_config.port}",
world_size=self.ffn_size + self.min_size,
rank=self.p2p_rank,
group_name="p2p",
timeout=timedelta(minutes=30),
)The AFD world rendezvous uses the afd_process_group_timeout_s config value (default 120)
(afd_plugin/connectors/gpu/p2p.py:275, afd_plugin/config.py:67-70). The control-plane group
uses a fixed 30-minute timeout (afd_plugin/connectors/gpu/p2p.py:313).
Deep dive#
Deep diveWorked example: 2A2F (DP1, TP2)
Take the DeepSeek-V2-Lite GPU recipe: two attention ranks and two FFN ranks, each role with
--data-parallel-size 1 and --tensor-parallel-size 2
(recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_eager_dbo_dp1tp2.sh:8-18).
The two attention ranks are TP peers, so their role ranks are 0 and 1.
| Rank | role rank | world rank | subgroup | data plane | DP metadata |
|---|---|---|---|---|---|
| F0 | 0 | 0 | {F0, A0} | receives from A0 | from A0 |
| F1 | 1 | 1 | {F1, A1} | receives from A1 | from A1 |
| A0 | 0 | 2 | {F0, A0} | sends to F0 | sends to F0 |
| A1 | 1 | 3 | {F1, A1} | sends to F1 | sends to F1 |
Here min_size = min(2, 2) = 2, so both attention ranks send DP metadata. The data plane and the
control plane agree: A0 pairs with F0, A1 pairs with F1
(afd_plugin/distributed/topology.py:135-136).
Deep diveWorked example: 4A2F (the planes differ)
Take four attention ranks and two FFN ranks. The data-plane subgroups use contiguous blocks; the control plane uses only the first two attention ranks.
| Rank | role rank | world rank | subgroup (data plane) | DP metadata |
|---|---|---|---|---|
| F0 | 0 | 0 | {F0, A0, A1} | from A0 |
| F1 | 1 | 1 | {F1, A2, A3} | from A1 |
| A0 | 0 | 2 | in F0 (0*2//4 = 0) | sends to F0 |
| A1 | 1 | 3 | in F0 (1*2//4 = 0) | sends to F1 |
| A2 | 2 | 4 | in F1 (2*2//4 = 1) | none |
| A3 | 3 | 5 | in F1 (3*2//4 = 1) | none |
So F0 fans in A0 and A1 for the data plane, but reads its DP metadata from A0 only. A2 and A3 send hidden
states to F1, but send no DP metadata: min_size = 2, so only A0 and A1 are senders
(afd_plugin/distributed/topology.py:136,
afd_plugin/distributed/topology.py:164-170). The The control plane: DP metadata once per step page reads why one
sender per FFN rank is enough.
Deep diveThe process groups and their timeouts
The connector owns two NCCL process groups, both rendezvoused at the same host:port.
| Group | name | world size | timeout | who joins |
|---|---|---|---|---|
| AFD world | "afd" | F + A | afd_process_group_timeout_s (120 s) (afd_plugin/connectors/gpu/p2p.py:275) | every rank |
| control plane | "p2p" | F + min_size | 30 min (afd_plugin/connectors/gpu/p2p.py:313) | FFN ranks + first min_size attention ranks |
The per-subgroup data communicators are not full process groups. They are
StatelessProcessGroup instances built over a prefix of the AFD world's rendezvous store, so each
subgroup does not need a port of its own (afd_plugin/connectors/gpu/p2p.py:278-304). The
connectors/gpu/p2p.py: the GPU P2P connector page reads this setup.
What can go wrong#
- Fewer attention ranks than FFN ranks. The P2P connector raises:
num_attention_ranksmust be ≥num_ffn_ranks(afd_plugin/distributed/topology.py:52-58). - A role rank out of range. A role rank that is not inside the role size raises; the
topology sizes must match the parallel placement
(
afd_plugin/distributed/topology.py:99-104). - Mismatched host, port or rank counts. The two roles must agree on
host,portand the rank counts, or the rendezvous fails or times out (afd_plugin/connectors/gpu/p2p.py:50-54).
Why it matters. The subgroups and the DP-metadata routes come only from the topology sizes. If the two roles do not agree on them, the collective setup cannot complete.
Check yourself#
- 1In what order does the P2P world place the ranks?easy
Show answer
FFN ranks first, then attention ranks:
[F0, F1, ..., A0, A1, ...](afd_plugin/distributed/topology.py:16-23). An attention rank's world rank isffn_size + role_rank(afd_plugin/distributed/topology.py:135). - 2Why must there be at least as many attention ranks as FFN ranks?easy
Show answer
So every FFN rank owns at least one attention peer.
validate_p2p_topologyraises whennum_attention_ranks < num_ffn_ranks(afd_plugin/distributed/topology.py:52-58). - 3In 4A2F, which attention ranks does FFN rank 0 own for the data plane?medium
Show answer
A0 and A1. The subgroup index is
a * F // A, so0*2//4 = 0and1*2//4 = 0both map to F0; A2 and A3 map to F1 (afd_plugin/distributed/topology.py:136). - 4In 4A2F, which attention ranks send DP metadata, and to whom?hard
Show answer
Only A0 and A1.
min_size = min(2, 4) = 2, so only the first two attention ranks are senders; A0 sends to F0 and A1 sends to F1 (afd_plugin/distributed/topology.py:164-170). A2 and A3 send hidden states but no DP metadata. - 5Which timeout controls the AFD world rendezvous, and which controls the control plane?hard
Show answer
The AFD world uses
afd_process_group_timeout_s(default 120 s) (afd_plugin/connectors/gpu/p2p.py:275,afd_plugin/config.py:67-70). The control-plane group uses a fixed 30-minute timeout (afd_plugin/connectors/gpu/p2p.py:313).