AFD Plugin Explained

afd_plugin/validation.py: runtime wiring checks

IntermediateCode mapabout 4 min read + 1 min deep dives

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

In one sentence

This module holds the CPU-safe checks that wire the AFD runtime: the worker names, the model-runner validators, the worker mapper and the stack check.

Purpose#

The module validates that an AFD deployment is wired correctly, before the model runs. It uses duck typing, so unit tests and CPU runs do not need a real vLLM config.

Fileafd_plugin/validation.py (388 lines, shared).
Main functionsvalidate_gpu_model_runner_v2_config (afd_plugin/validation.py:76), assert_compatible_afd_stack (afd_plugin/validation.py:312), afd_worker_qualname_for_platform_default (afd_plugin/validation.py:280).
Runs inThe API server process (worker mapping) and each worker process (the stack and runner checks, in init_device).
Called byThe config patch (afd_plugin/compat/patches/config_validation.py:24-26) and the AFD workers (afd_plugin/v1/worker/attention_worker.py:16-19).
CallsThe config parser (afd_plugin/validation.py:12-17), the CUDA-graph helpers (afd_plugin/validation.py:18-21), and has_afd_model_registration.
Not in this fileThe NPU validator internals beyond naming them, the worker bodies, and the config schema (in config.py).

Where it sits#

Who calls it#

The config patch calls afd_worker_qualname_for_platform_default to rewrite the worker (afd_plugin/compat/patches/config_validation.py:192-197). Each AFD worker calls assert_compatible_afd_stack and the runner validator in init_device (afd_plugin/v1/worker/attention_worker.py:64-73).

What it calls#

It parses the AFD config with an expected role (afd_plugin/validation.py:93), and imports has_afd_model_registration lazily so the module stays independent of the model package (afd_plugin/validation.py:136-138).

Key classes and functions#

NameLineRole
Worker FQCN constantsafd_plugin/validation.py:26-41The qualified names of the AFD and native workers.
BATCH_SIZE_FACTOR / cam_receive_capacityafd_plugin/validation.py:45-54Compute the CAM receive capacity from an env factor.
validate_gpu_model_runner_v2_configafd_plugin/validation.py:76The GPU deployment rules.
validate_npu_model_runner_v2_configafd_plugin/validation.py:146The Ascend NPU deployment rules.
afd_worker_qualname_for_platform_defaultafd_plugin/validation.py:280Map the platform default worker to the AFD worker.
assert_compatible_afd_stackafd_plugin/validation.py:312Check the config and the worker class match the role.

Walk through the main methods#

The FQCN constants#

The module names each worker and runner as a string constant. The GPU workers, the NPU workers and the native platform workers all have their own names (afd_plugin/validation.py:26-41).

ATTENTION_WORKER_FQCN: Final[str] = "afd_plugin.v1.worker.AFDAttentionWorker"
FFN_WORKER_FQCN: Final[str] = "afd_plugin.v1.worker.AFDFFNWorker"
ATTENTION_MODEL_RUNNER_FQCN: Final[str] = (
    "afd_plugin.v1.worker.AFDAttentionModelRunnerV2"
)
FFN_MODEL_RUNNER_FQCN: Final[str] = "afd_plugin.v1.worker.GPUFFNModelRunner"
NPU_ATTENTION_WORKER_FQCN: Final[str] = "afd_plugin.v1.worker.npu.AFDNPUAttentionWorker"
NPU_FFN_WORKER_FQCN: Final[str] = "afd_plugin.v1.worker.npu.AFDNPUFFNWorker"
NPU_ATTENTION_MODEL_RUNNER_FQCN: Final[str] = (
    "afd_plugin.v1.worker.npu.AFDNPUAttentionModelRunnerV2"
)
NPU_FFN_MODEL_RUNNER_FQCN: Final[str] = "afd_plugin.v1.worker.npu.AFDNPUFFNModelRunner"
VLLM_GPU_WORKER_FQCN: Final[str] = "vllm.v1.worker.gpu_worker.Worker"
VLLM_ASCEND_NPU_WORKER_FQCN: Final[str] = "vllm_ascend.worker.worker.NPUWorker"
VLLM_ASCEND_310P_WORKER_FQCN: Final[str] = "vllm_ascend._310p.worker_310p.NPUWorker310"
VLLM_ASCEND_XLITE_WORKER_FQCN: Final[str] = "vllm_ascend.xlite.xlite_worker.XliteWorker"
GPU_MRV2_NUM_UBATCHES: Final[int] = 2
The worker and runner qualified names, plus the GPU ubatch count.

validate_gpu_model_runner_v2_config, rule by rule#

This function raises a RuntimeError for each broken rule (afd_plugin/validation.py:76-143).

  1. Line 90-91. It needs the V2 model runner (afd_plugin/validation.py:90-91).
  2. Line 93-97. The device must be CUDA and the connector the synchronous P2pNcclAFDConnector (afd_plugin/validation.py:93-97).
  3. Line 99-105. It rejects pipeline and context parallelism (afd_plugin/validation.py:99-105).
  4. Line 107-117. The role rank count must equal data-parallel size times tensor-parallel size (afd_plugin/validation.py:107-117).
  5. Line 118-125. It needs static expert parallelism, and rejects elastic EP, EPLB and sequence-parallel MoE (afd_plugin/validation.py:118-125).
  6. Line 126-131. With DBO or ubatching, the number of microbatches must be exactly two (afd_plugin/validation.py:126-131).
  7. Line 136-141. The model must resolve to a registered AFD model (afd_plugin/validation.py:136-141).
  8. Line 143. It checks the CUDA-graph mode for the role (afd_plugin/validation.py:143).

assert_compatible_afd_stack#

This check compares the resolved worker with the expected AFD worker for the role. It raises when the field is still "auto", or when the worker does not match (afd_plugin/validation.py:337-367).

    if worker_cls_raw.strip() == "auto":
        raise ValueError(
            "parallel_config.worker_cls remained 'auto' after AFD config "
            "normalization; ensure the AFD general plugin is loaded before "
            f"VllmConfig is created{_ctx()}",
        )

    expected_qualname = (
        async_expected_worker
        or expected_worker_qualname_override
        or expected_worker_qualname(config.role)
    )
    worker_fqcn = normalize_qualname(worker_cls_raw.strip())
    expected_fqcn = normalize_qualname(expected_qualname)
    if worker_fqcn != expected_fqcn:
        prefix = (
            "CAMAsyncAFDConnector requires Ascend NPU worker class: "
            if async_expected_worker is not None
            else "invalid worker class for AFD runtime stack: "
        )
        raise ValueError(
            prefix + f"got={worker_fqcn!r} expected={expected_qualname!r}; "
            "remove --worker-cls to let AFD select it automatically, or pass "
            f"--worker-cls {expected_qualname}{_ctx()}",
        )
Reject a leftover 'auto', then reject a mismatched worker with both names.

afd_worker_qualname_for_platform_default#

This function maps the normalized platform default worker to the AFD worker for the role. CUDA maps to the GPU workers; the standard Ascend worker maps to the NPU workers; the 310P and Xlite Ascend workers are rejected (afd_plugin/validation.py:280-309). The Worker selection: choosing the role worker page gives the table.

The NPU validator (NPU scope)#

validate_npu_model_runner_v2_config holds the Ascend rules (afd_plugin/validation.py:146-239). It accepts the sync and async CAM connectors, and checks eager execution, the DP rule and the ACL graph modes. This is NPU code; the Ascend NPU overview: the same contract, different transports page covers it. Other pages cite it only to name a rule.

Deep dive#

Deep dive

The GPU rules as an error-path table

Every rule in validate_gpu_model_runner_v2_config raises a RuntimeError with a clear message.

RuleLinesMessage hint
No V2 runnerafd_plugin/validation.py:90-91"AFD GPU requires ModelRunnerV2".
Wrong device or connectorafd_plugin/validation.py:93-97"requires CUDA and synchronous P2pNcclAFDConnector".
PP or CP setafd_plugin/validation.py:99-105"does not support PP or CP".
Rank mismatchafd_plugin/validation.py:107-117"ranks must match DP * TP".
Not static EPafd_plugin/validation.py:118-125"requires static expert parallelism".
Wrong ubatch countafd_plugin/validation.py:126-131"requires exactly two microbatches".
Not an AFD modelafd_plugin/validation.py:136-141"requires a registered AFD model".
Deep dive

BATCH_SIZE_FACTOR and the CAM receive capacity

cam_receive_capacity scales a fixed token cap by an environment factor. It reads BATCH_SIZE_FACTOR (default "1"), packs it as a 32-bit float to match the native code, and multiplies CAM_MAX_RECEIVE_TOKENS (262144) (afd_plugin/validation.py:45-54).

CAM_MAX_RECEIVE_TOKENS = 262144
CAM_MAX_SOURCE_ROWS = (1 << 16) - 1


def cam_receive_capacity() -> int:
    # Match the native std::stof + float multiplication, including rounding.
    factor = struct.unpack(
        "f", struct.pack("f", float(os.environ.get("BATCH_SIZE_FACTOR", "1")))
    )[0]
    return int(CAM_MAX_RECEIVE_TOKENS * factor)
cam_receive_capacity: match the native float math exactly.

This value bounds the async CAM connector on NPU. The 32-bit pack and unpack reproduce the rounding of the native std::stof and float multiply, so Python and the kernel agree on the capacity.

Check yourself#

  1. 1
    What does validate_gpu_model_runner_v2_config check first?
    easy
    Show answer

    That the V2 model runner is on. It raises "AFD GPU requires ModelRunnerV2" otherwise (afd_plugin/validation.py:90-91).

  2. 2
    Why does the module import has_afd_model_registration inside the function, not at the top?
    medium
    Show answer

    To keep the CPU-safe validation module independent of the model-wrapper package; the validator runs only after the vLLM config is built (afd_plugin/validation.py:133-138).

  3. 3
    Which two connectors does the NPU validator accept?
    medium
    Show answer

    The synchronous CAMP2pAFDConnector and the async CAMAsyncAFDConnector (afd_plugin/validation.py:159-178). It is NPU code; see Ascend NPU overview: the same contract, different transports.

  4. 4
    Why does cam_receive_capacity pack the factor as a 32-bit float before multiplying?
    hard
    Show answer

    To reproduce the native std::stof and float multiply exactly, including the rounding, so Python and the kernel compute the same capacity (afd_plugin/validation.py:49-54).