AFD Plugin Explained

AFD configuration: additional_config["afd"]

IntermediateAFD conceptsabout 5 min read + 2 min deep dives

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

In one sentence

AFD reads one key, additional_config["afd"]. The plugin renames aliases, coerces types, builds AFDConfig, then validates it.

The idea#

vLLM has one open config field for out-of-tree code, additional_config (vllm:vllm/config/vllm.py:434). afd-plugin reads one key inside it, "afd" (afd_plugin/config.py:20). The value is a mapping of AFD settings: the role, the connector, the topology sizes and the rendezvous host and port.

The plugin does not use the raw mapping directly. It renames old afd_* aliases to short names, coerces the types, builds a frozen dataclass, and validates the result. One flag decides whether AFD runs at all: AFD is active only when the "afd" key is present.

An everyday comparison. Think of a form with a few fields and some old field names kept for compatibility. A clerk renames the old names, checks each value, and files one clean record. The comparison stops here: a missing form here does not mean an error, it means AFD stays off.

The picture#

Figure: one function runs the parse pipeline (panel 1); the presence of the 'afd' key decides whether AFD is active (panel 2).
Open full size ↗

Read panel 1 left to top-down as the pipeline inside afd_config_from_mapping. Read panel 2 as the on/off rule. The full key tables are on the AFD config reference: the additional_config["afd"] keys page; this page explains the rules behind them.

In the code#

Step 1: The schema#

AFDConfig is a frozen dataclass. Its fields and defaults are the schema (afd_plugin/config.py:42-73). The table lists them; the full reference is on the AFD config reference: the additional_config["afd"] keys page.

    # Connector implementation name used to create the backend data path.
    connector: str = "P2pNcclAFDConnector"
    # Whether AFD owns async data-parallel runtime patches for this process.
    async_dp: bool = False
    # Role owned by this process: Attention sends hidden states; FFN receives.
    role: AFDRole = "attention"
    # Port used by the AFD connector rendezvous/control path.
    port: int = 1239
    # Host used by the AFD connector rendezvous/control path.
    host: str = "127.0.0.1"
    # Number of AFD Attention ranks participating in this topology.
    num_attention_ranks: int = 1
    # Number of AFD FFN ranks participating in this topology.
    num_ffn_ranks: int = 1
    # Whether Attention computes MoE gate outputs before sending to FFN.
    compute_gate_on_attention: bool = False
    # Timeout, in seconds, for the AFD world process group rendezvous.
    # The right value depends on rank count, placement, and model size, so
    # it is left to the caller (deployment tooling) rather than derived here.
    afd_process_group_timeout_s: int = 120
    connector_extra_config: dict[str, Any] = field(
        default_factory=dict, repr=False, hash=False
    )
The AFDConfig fields and their defaults.
FieldDefaultMeaning
connector"P2pNcclAFDConnector"Connector name (afd_plugin/config.py:52).
async_dpFalseNPU-only async DP (afd_plugin/config.py:54).
role"attention"Role of this process (afd_plugin/config.py:56).
port, host1239, "127.0.0.1"Rendezvous (afd_plugin/config.py:58-60).
num_attention_ranks, num_ffn_ranks1, 1Topology sizes (afd_plugin/config.py:62-64).
compute_gate_on_attentionFalseMoE gate placement (afd_plugin/config.py:66).
afd_process_group_timeout_s120Rendezvous timeout (afd_plugin/config.py:70).
connector_extra_config{}Per-connector options (afd_plugin/config.py:71-73).

Step 2: Aliases#

Five old afd_* names stay valid as read-only aliases. Normalization renames each to its short name (afd_plugin/config.py:32-38).

_ALIASES: Final[dict[str, str]] = {
    "afd_connector": "connector",
    "afd_role": "role",
    "afd_port": "port",
    "afd_host": "host",
    "async": "async_dp",
}
The alias map: afd_connector, afd_role, afd_port, afd_host, async.

So the recipe may write "afd_role" or "role"; both reach the role field (afd_plugin/config.py:128).

Step 3: Normalization and coercion#

_normalize_mapping renames the aliases, splits out connector_extra_config, rejects an unknown field, and coerces the typed fields (afd_plugin/config.py:120-176). The integer fields use coerce_extra_int, which rejects a bool or a float instead of truncating it (afd_plugin/config_utils.py:35-45).

    if "async_dp" in normalized:
        normalized["async_dp"] = _coerce_bool(
            normalized["async_dp"],
            field_name="async",
        )

    for field_name in (
        "port",
        "num_attention_ranks",
        "num_ffn_ranks",
    ):
        if field_name in normalized:
            normalized[field_name] = _coerce_int(
                normalized[field_name],
                field_name=field_name,
            )

    if "afd_process_group_timeout_s" in normalized:
        normalized["afd_process_group_timeout_s"] = _coerce_positive_int(
            normalized["afd_process_group_timeout_s"],
            field_name="afd_process_group_timeout_s",
        )

    if "compute_gate_on_attention" in normalized:
        normalized["compute_gate_on_attention"] = _coerce_bool(
            normalized["compute_gate_on_attention"],
            field_name="compute_gate_on_attention",
        )
Coerce async_dp, the integer fields and the positive-int timeout.

Step 4: The validation rules#

validate_afd_config checks the built config without importing vLLM or CUDA (afd_plugin/config.py:311-350). The rules:

Step 5: AFD is active only when the key is present#

is_afd_active parses the optional config and returns true only when a present "afd" key passes validation (afd_plugin/config.py:289-292). When the key is absent, parse_optional_afd_config returns None and AFD stays off (afd_plugin/config.py:244-259).

Step 6: Per-connector options#

Options that belong to one connector go under connector_extra_config, not next to the AFD fields. Normalization keeps this sub-mapping aside (afd_plugin/config.py:129-133). Each connector parses its own keys later, through parse_extra_config (afd_plugin/connectors/base.py:64-72). The AFD config reference: the additional_config["afd"] keys page lists each connector's keys.

Deep dive#

Deep dive

Trace one recipe value end to end: num_attention_ranks = 2

The 2a2f recipe sets num_attention_ranks to 2 in the attention server's config (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_eager_dbo_dp1tp2.sh:17). Follow that value:

  1. Read. The plugin reads additional_config["afd"], which holds the mapping (afd_plugin/config.py:227-234).
  2. Normalize. num_attention_ranks is not an alias and is a valid field, so it is kept (afd_plugin/config.py:127-145).
  3. Coerce. It is in the integer list, so coerce_extra_int(2) returns 2; a float or a bool would raise (afd_plugin/config.py:153-162, afd_plugin/config_utils.py:35-45).
  4. Build. The value becomes AFDConfig.num_attention_ranks (afd_plugin/config.py:190).
  5. Validate. The rank count must be positive (afd_plugin/config.py:343-346).
  6. Use. At device init, the runtime check needs the attention rank count to equal data-parallel size times tensor-parallel size. This recipe runs DP1 TP2, so the value must be 2 (afd_plugin/validation.py:107-117).

So a field default of 1 (afd_plugin/config.py:62) is only the start; the final value comes from the recipe, and a runtime rule ties it to the parallel sizes.

Deep dive

async_dp is NPU-only, and false on GPU

async_dp turns on AFD's async data-parallel patches. It is read by is_afd_async_dp, which returns true only when the flag is set and the connector is the async CAM connector (afd_plugin/config.py:295-308). That connector is NPU-only, so the flag has no effect on GPU.

What can go wrong#

Why it matters. Both roles parse the same schema. A typo or a wrong type stops start-up with a clear error, instead of a silent mismatch between the two servers.

Check yourself#

  1. 1
    What makes AFD active?
    easy
    Show answer

    The presence of the "afd" key in additional_config. Without it, parse returns None and AFD stays off (afd_plugin/config.py:244-259, afd_plugin/config.py:289-292).

  2. 2
    Where do connector-specific options go?
    easy
    Show answer

    Under connector_extra_config, not next to the AFD fields. Each connector parses its own keys (afd_plugin/config.py:71-73, afd_plugin/connectors/base.py:64-72).

  3. 3
    A recipe passes "port": 6269.0. What happens?
    medium
    Show answer

    It raises TypeError. coerce_extra_int rejects a float instead of truncating it (afd_plugin/config_utils.py:38-39).

  4. 4
    On what platform does async_dp have an effect?
    medium
    Show answer

    NPU only. It needs the async CAM connector (afd_plugin/config.py:295-308), which GPU does not use (afd_plugin/validation.py:94-97).

  5. 5
    The field default for num_attention_ranks is 1, but the 2a2f recipe runs with 2. What final rule ties the value to the deployment?
    hard
    Show answer

    At device init, the attention rank count must equal data-parallel size times tensor-parallel size. With DP1 TP2 the value must be 2 (afd_plugin/validation.py:107-117). The default is only the start; the recipe sets the real value (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_eager_dbo_dp1tp2.sh:17).