AFD Plugin Explained

connectors/gpu/p2p.py: the GPU P2P connector

AdvancedCode mapabout 5 min read + 1 min deep dives

Code version: afd-plugin @ 7452d52 · 2026-10-27 · vLLM @ ced6857

In one sentence

The GPU connector is P2pNcclAFDConnector. It moves hidden states with vLLM's PyNcclCommunicator over point-to-point NCCL.

Purpose#

This file implements the connector contract for CUDA. It builds the AFD world, the per-subgroup communicators and the control-plane group, and moves tensors through four custom ops. The contract itself is on The connector contract: the Attention/FFN transport; this page reads the implementation.

ItemValue
File (lines, origin)connectors/gpu/p2p.py (1125); AFD-owned
Main classesP2pNcclAFDConnector (afd_plugin/connectors/gpu/p2p.py:124), P2pNcclAFDControlPlane (afd_plugin/connectors/gpu/p2p.py:724)
Runs inevery CUDA attention and FFN worker; transfers on the current CUDA stream
Called bythe role runners, through the factory (afd_plugin/v1/worker/ffn_model_runner.py:80-85)
CallsvLLM's PyNcclCommunicator (vllm:vllm/distributed/device_communicators/pynccl.py:91), StatelessProcessGroup, and the AFD topology helpers
Not in this filethe ABC (afd_plugin/connectors/base.py:37), the metadata objects (afd_plugin/connectors/metadata.py:171), the NPU connectors

Where it sits#

Who calls it#

The factory loads this module by name and builds the connector for a runner (afd_plugin/connectors/factory.py:88-92). The runner then calls init_afd_connector once (afd_plugin/v1/worker/ffn_model_runner.py:108-109), then the four data calls per layer and stage, and close at shutdown (afd_plugin/v1/worker/ffn_model_runner.py:455).

What it calls#

The data transfers go through vLLM's PyNcclCommunicator.send and recv (vllm:vllm/distributed/device_communicators/pynccl.py:91). The process groups come from the plugin's own helper (afd_plugin/distributed/afd_process_group.py:62-120) and the rank mapping from the topology module (afd_plugin/distributed/topology.py:118).

Key classes and functions#

NameLineRole
P2pNcclAFDConnectorafd_plugin/connectors/gpu/p2p.py:124the CUDA connector
parse_extra_configafd_plugin/connectors/gpu/p2p.py:145-158accepts no options; rejects a non-empty config
init_afd_connectorafd_plugin/connectors/gpu/p2p.py:245the collective setup (three steps)
send_attn_outputafd_plugin/connectors/gpu/p2p.py:323attention sends hidden states (+ optional extras)
recv_ffn_outputafd_plugin/connectors/gpu/p2p.py:392attention receives its FFN slice
recv_attn_outputafd_plugin/connectors/gpu/p2p.py:428FFN receives and concatenates its peers
send_ffn_outputafd_plugin/connectors/gpu/p2p.py:554FFN splits and sends each slice back
P2pNcclAFDControlPlaneafd_plugin/connectors/gpu/p2p.py:724the DP-metadata control plane
_register_p2p_custom_opsafd_plugin/connectors/gpu/p2p.py:979registers the four afd_p2p_* ops

Walk through the main methods#

init_afd_connector: the collective setup#

This is a collective call: every attention and FFN rank must run it with matching host, port and rank counts (afd_plugin/connectors/gpu/p2p.py:245-262). It does three things. First, it registers the custom ops and joins the AFD world group, rendezvoused at tcp://host:port (afd_plugin/connectors/gpu/p2p.py:267-276).

        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),
        )
Step 1: the AFD world group, world size = ffn_size + attn_size.

Second, it builds this rank's subgroup communicators over a prefix of the world's rendezvous store (afd_plugin/connectors/gpu/p2p.py:278-304). It creates two PyNcclCommunicator instances, attention-to-FFN and FFN-to-attention, and registers each for the custom ops.

            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)
Step 2: the subgroup StatelessProcessGroup and the two communicators.

Third, on ranks that join the control plane, it creates the "p2p" group (afd_plugin/connectors/gpu/p2p.py:306-314). The Topology and role ranks: the AFD world page reads the world order and the group sizes.

The four data calls#

The data path is four methods. The attention side calls two, the FFN side calls two.

The Data on the wire: tensors per layer and stage page reads the shapes, the fan-in and the split in full.

P2pNcclAFDControlPlane: the DP-metadata path#

The connector owns one control-plane instance and exposes it as control_plane (afd_plugin/connectors/gpu/p2p.py:222, afd_plugin/connectors/gpu/p2p.py:724-733). It has three methods: apply a payload locally, send one, receive one.

    def recv_dp_metadata_list(self) -> AFDControlPayload:
        """Receive a DP metadata payload on an FFN rank.

        Blocks on the ``p2p`` process group until the mapped Attention sender
        rank transmits the next payload.

        Returns:
            The DP metadata payload sent by the Attention side.

        Raises:
            RuntimeError: If the DP metadata process group is not initialized
                on this rank.
        """
        connector = self.connector
        if connector.p2p_pg is None:
            raise RuntimeError("P2P DP metadata process group is not initialized")

        src = connector.p2p_rank % connector.min_size + connector.ffn_size
recv_dp_metadata_list: an FFN rank reads from its mapped attention sender.

update_state_from_dp_metadata derives the wire-tensor shapes and preallocates the graph-mode buffers (afd_plugin/connectors/gpu/p2p.py:744-851); send_dp_metadata_list transmits only on the designated sender ranks (afd_plugin/connectors/gpu/p2p.py:870-883). The The control plane: DP metadata once per step page reads these.

_register_p2p_custom_ops: four ops, once per process#

The module registers four custom ops: afd_p2p_send, afd_p2p_recv, and the _group variants (afd_plugin/connectors/gpu/p2p.py:979-1122). Each op wraps the NCCL send or receive on the current stream and has a fake implementation for tracing.

        if hasattr(torch.ops.vllm, op_name):
            return
        direct_register_custom_op(
            op_name=op_name,
            op_func=op_func,
            mutates_args=mutates_args,
            fake_impl=fake_impl,
        )
An op already registered by another instance is reused, not re-defined.
  1. Line 1089-1090. If the op is already on torch.ops.vllm, return; torch rejects a re-definition.
  2. Line 1091-1096. Otherwise register the op, its mutated args and its fake implementation.
  3. Line 1122. Set the module flag so the registration runs once per process.

Deep dive#

Deep dive

The process groups and communicators

The connector owns three communication objects with different scopes.

ObjectScopeBuilt at
AFD world group ("afd")all ranks; the rendezvous storeafd_plugin/connectors/gpu/p2p.py:269-276
subgroup communicators (a2e, e2a)one FFN rank + its attention peersafd_plugin/connectors/gpu/p2p.py:289-304
control-plane group ("p2p")FFN ranks + first min_size attention ranksafd_plugin/connectors/gpu/p2p.py:306-314

The subgroup communicators are built while the AFD world is the default process group, so the stateless group reuses the world's store instead of opening a second one (afd_plugin/connectors/gpu/p2p.py:278-288). The custom ops look the communicators up by integer id, because an op cannot capture a Python object (afd_plugin/connectors/gpu/p2p.py:965-976).

Deep dive

Error paths

ConditionResultLine
a non-empty connector_extra_configValueErrorafd_plugin/connectors/gpu/p2p.py:154-157
hidden-state shape != total_tokens (not compiling)ValueErrorafd_plugin/connectors/gpu/p2p.py:347-353
a CPU hidden-state tensorValueErrorafd_plugin/connectors/gpu/p2p.py:634-635
the connector not initializedRuntimeErrorafd_plugin/connectors/gpu/p2p.py:628-629
a control-plane receive without the "p2p" groupRuntimeErrorafd_plugin/connectors/gpu/p2p.py:899-900

Check yourself#

  1. 1
    Which vLLM class moves the tensors?
    easy
    Show answer

    PyNcclCommunicator, vLLM's NCCL wrapper (vllm:vllm/distributed/device_communicators/pynccl.py:91). The connector wraps its send and recv in custom ops.

  2. 2
    What three groups does init_afd_connector set up?
    medium
    Show answer

    The AFD world group (afd_plugin/connectors/gpu/p2p.py:269-276), the per-subgroup communicators (afd_plugin/connectors/gpu/p2p.py:289-304), and the control-plane "p2p" group on ranks that join it (afd_plugin/connectors/gpu/p2p.py:306-314).

  3. 3
    Why does the op registration check hasattr(torch.ops.vllm, op_name) first?
    hard
    Show answer

    Because torch keeps the op in a process-global registry. After a module reload, the module flag can be False while the op is still defined, so re-defining it would fail; the code reuses the existing op instead (afd_plugin/connectors/gpu/p2p.py:1089-1096).