AFD configuration: additional_config["afd"]
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#
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
)| Field | Default | Meaning |
|---|---|---|
connector | "P2pNcclAFDConnector" | Connector name (afd_plugin/config.py:52). |
async_dp | False | NPU-only async DP (afd_plugin/config.py:54). |
role | "attention" | Role of this process (afd_plugin/config.py:56). |
port, host | 1239, "127.0.0.1" | Rendezvous (afd_plugin/config.py:58-60). |
num_attention_ranks, num_ffn_ranks | 1, 1 | Topology sizes (afd_plugin/config.py:62-64). |
compute_gate_on_attention | False | MoE gate placement (afd_plugin/config.py:66). |
afd_process_group_timeout_s | 120 | Rendezvous 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",
}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",
)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:
- the role is
"attention"or"ffn"(afd_plugin/config.py:318-321); - the connector name is one of the three supported names (
afd_plugin/config.py:326-330); async_dpneeds the async connector (afd_plugin/config.py:331-334);- the host is non-empty and the port is in
1..65535(afd_plugin/config.py:339-342); - both rank counts are positive (
afd_plugin/config.py:343-350).
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 diveTrace 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:
- Read. The plugin reads
additional_config["afd"], which holds the mapping (afd_plugin/config.py:227-234). - Normalize.
num_attention_ranksis not an alias and is a valid field, so it is kept (afd_plugin/config.py:127-145). - Coerce. It is in the integer list, so
coerce_extra_int(2)returns2; a float or a bool would raise (afd_plugin/config.py:153-162,afd_plugin/config_utils.py:35-45). - Build. The value becomes
AFDConfig.num_attention_ranks(afd_plugin/config.py:190). - Validate. The rank count must be positive
(
afd_plugin/config.py:343-346). - 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 diveasync_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.
- Validation.
async_dpwithout the async connector raises (afd_plugin/config.py:331-334). - GPU runtime. The GPU runner check needs the synchronous
P2pNcclAFDConnector(afd_plugin/validation.py:94-97). So the async connector, and thereforeasync_dp, cannot be active on GPU. - Where it takes effect. On NPU,
is_afd_async_dpselects the async-DP patches from the flag and the connector (afd_plugin/config.py:303-308). Those patches are NPU code; see Ascend NPU overview: the same contract, different transports.
What can go wrong#
- Unknown field. A key that is not a field and not an alias raises
ValueError; the message points toconnector_extra_config(afd_plugin/config.py:134-139). - Alias and canonical together. Passing both
afd_roleandroleraises a duplicate-field error (afd_plugin/config.py:140-144). - Wrong type for an integer. A float or a bool for
portor a rank count raisesTypeError, not a silent truncation (afd_plugin/config_utils.py:38-39). - async_dp on GPU. It fails validation unless the connector is the async CAM
connector, which GPU does not use (
afd_plugin/config.py:331-334).
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#
- 1What makes AFD active?easy
Show answer
The presence of the
"afd"key inadditional_config. Without it, parse returnsNoneand AFD stays off (afd_plugin/config.py:244-259,afd_plugin/config.py:289-292). - 2Where 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). - 3A recipe passesmedium
"port": 6269.0. What happens?Show answer
It raises
TypeError.coerce_extra_intrejects a float instead of truncating it (afd_plugin/config_utils.py:38-39). - 4On what platform doesmedium
async_dphave an effect?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). - 5The field default forhard
num_attention_ranksis 1, but the 2a2f recipe runs with 2. What final rule ties the value to the deployment?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).