AFD Plugin Explained

Topology and role ranks: the AFD world

AdvancedAFD conceptsabout 6 min read + 2 min deep dives

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#

Figure: FFN ranks come first in the world. The data-plane subgroups use contiguous blocks; the control plane uses only the first F attention ranks.
Open full size ↗

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.

InteractiveMap a topology

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).

Set 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, then a range check against the 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_rank
Attention world rank = ffn_size + role_rank; FFN world rank = role_rank.

Step 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)
The subgroup roster: the FFN rank first, then its attention peers.

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}",
        )
num_attention_ranks must be ≥ num_ffn_ranks.

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_size
The DP-metadata destinations for one rank.

The 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 group and the control-plane group.

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 dive

Worked 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.

Rankrole rankworld ranksubgroupdata planeDP metadata
F000{F0, A0}receives from A0from A0
F111{F1, A1}receives from A1from A1
A002{F0, A0}sends to F0sends to F0
A113{F1, A1}sends to F1sends 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 dive

Worked 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.

Rankrole rankworld ranksubgroup (data plane)DP metadata
F000{F0, A0, A1}from A0
F111{F1, A2, A3}from A1
A002in F0 (0*2//4 = 0)sends to F0
A113in F0 (1*2//4 = 0)sends to F1
A224in F1 (2*2//4 = 1)none
A335in 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 dive

The process groups and their timeouts

The connector owns two NCCL process groups, both rendezvoused at the same host:port.

Groupnameworld sizetimeoutwho joins
AFD world"afd"F + Aafd_process_group_timeout_s (120 s) (afd_plugin/connectors/gpu/p2p.py:275)every rank
control plane"p2p"F + min_size30 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#

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#

  1. 1
    In 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 is ffn_size + role_rank (afd_plugin/distributed/topology.py:135).

  2. 2
    Why 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_topology raises when num_attention_ranks < num_ffn_ranks (afd_plugin/distributed/topology.py:52-58).

  3. 3
    In 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, so 0*2//4 = 0 and 1*2//4 = 0 both map to F0; A2 and A3 map to F1 (afd_plugin/distributed/topology.py:136).

  4. 4
    In 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.

  5. 5
    Which 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).