AFD Plugin Explained

Compat patches: how AFD changes vLLM without editing it

AdvancedAFD conceptsabout 7 min read + 2 min deep dives

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

In one sentence

AFD changes no vLLM source. register_afd() imports patch modules that replace vLLM functions in place, each behind a version gate.

The idea#

AFD is an external afd-plugin. It must not edit the vLLM source. Instead, it replaces a few vLLM functions at run time with its own versions. Each replacement is a patch, in afd_plugin/compat/patches/. register_afd() imports the patch modules once at start-up (afd_plugin/__init__.py:172-181).

Every patch copies the matching vLLM function from the pinned release and marks only the AFD difference between PATCH START and PATCH END comments. The patched signature must match the original (AGENTS.md:161-168). A version gate decides whether a patch installs, so AFD does not break a vLLM it was not built for.

An everyday comparison. Think of a train that swaps one carriage for a custom one before it leaves, and swaps the original back when it returns. The comparison stops here: a patch checks the train model first, and some patches swap the carriage only for the length of one trip.

The picture#

Figure: a start-up check only warns; each patch has its own install gate. Patches install at import time, except the DP1 DBO seams, which are scoped to AFD calls.
Open full size ↗

Read panel 1 as two separate checks. One warns at start-up; the other gates each install. Panel 2 lists the GPU patch modules and how each one installs.

In the code#

Step 1: Patch discipline#

The patches package is a closed namespace: it raises on any unexpected attribute, so nothing leaks (afd_plugin/compat/patches/__init__.py:12-13). Each patch function carries comments that give the reason, the change, and the upstream target (AGENTS.md:165-170), and marks the AFD lines with PATCH START and PATCH END.

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the AFD plugin project
"""Isolated monkey-patch namespace.

Patches in this package must remain idempotent, version-aware, documented, and
covered by CPU-safe tests whenever possible.
"""

__all__: list[str] = []


def __getattr__(name: str):
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
The patches package: idempotent, version-aware, documented.

Step 2: Two version checks#

The first check runs in register_afd. It is not strict, so a vLLM that is not exactly the target release only warns (afd_plugin/compat/vllm.py:38-50, afd_plugin/__init__.py:162-170).

The second check is the install gate, is_target_vllm_compatible. It allows the target release and any dev build, and returns true when vLLM is absent so CPU imports still work (afd_plugin/compat/vllm.py:53-68).

def is_target_vllm_compatible() -> bool:
    """Return whether compatibility patches may install.

    Tolerates development builds of the target release and any import
    failure (no vLLM installed) so CPU-safe imports keep working.
    """
    try:
        import vllm

        version_value = vllm.__version__
    except (AttributeError, ImportError):
        return True
    version_text = str(version_value)
    if "dev" in version_text:
        return True
    return version_text.startswith(TARGET_VLLM_VERSION)
The install gate: dev builds pass; otherwise the version must start with the target.

Step 3: config_validation#

This patch replaces two vLLM functions: EngineArgs.create_engine_config (create_engine_config) and VllmConfig.__post_init__ (VllmConfig.__post_init__). It installs at import time, behind the gate (afd_plugin/compat/patches/config_validation.py:235-261).

What it changes: it rewrites worker_cls to the AFD role worker, but only when the value was "auto" (afd_plugin/compat/patches/config_validation.py:92-93, afd_plugin/compat/patches/config_validation.py:179-197). It also relaxes one vLLM ubatching backend check for AFD configs (afd_plugin/compat/patches/config_validation.py:200-214). See Worker selection: choosing the role worker.

Step 4: engine_core#

This patch replaces EngineCore.__init__, _initialize_kv_caches and shutdown, plus the busy loops of EngineCoreProc and DPEngineCoreProc (afd_plugin/compat/patches/engine_core.py:722-727). The vLLM targets are in vllm/v1/engine/core.py (EngineCore.__init__, EngineCoreProc.run_busy_loop).

What it changes: for an FFN config, __init__ returns right after the executor is built, so the FFN EngineCore has no real scheduler (a no-op stand-in) and no KV cache (afd_plugin/compat/patches/engine_core.py:56-68, afd_plugin/compat/patches/engine_core.py:614). The busy loop instead runs the connector loop (afd_plugin/compat/patches/engine_core.py:411-419). See Attention and FFN roles: what each server owns.

Step 5: gpu_mrv2_ubatching#

This patch is different: it installs through scoped context managers, not at import time (afd_plugin/compat/patches/gpu_mrv2_ubatching.py:263-282). It replaces maybe_build_ubatch_runner (maybe_build_ubatch_runner) and dispatch_cg_and_sync_dp (dispatch_cg_and_sync_dp) only inside AFD's own initialize_kv_cache and execute_model calls. It re-enables DBO at one data-parallel rank. See Dual-batch overlap (DBO): two stages per step.

Step 6: async_dp patches#

Two patches support async-DP, which is NPU-only. They install at import time but only change behavior for async configs, gated at run time (afd_plugin/config.py:303-308). On GPU, async_dp must be false, so these patches run as transparent re-implementations.

Step 7: NPU patches#

The patches under afd_plugin/compat/patches/npu/ install only when the device is NPU (afd_plugin/__init__.py:204-209). They are covered on Ascend NPU overview: the same contract, different transports. In one line each:

Deep dive#

The blocks below give the install gate and its guards, the monkey-patch rule, and a pointer to the full table.

Deep dive

The install gate and the hasattr guards

The config patch installs at the bottom of its module, inside an if on the gate (afd_plugin/compat/patches/config_validation.py:235-261). It saves each original function once, using a hasattr guard, then stores it and assigns the patched function.

  1. Save once. The hasattr guard writes the original only if it is not already saved, so a reload does not overwrite it (afd_plugin/compat/patches/config_validation.py:236, afd_plugin/compat/patches/config_validation.py:243).
  2. Read the saved original. The module keeps a reference to the saved original (afd_plugin/compat/patches/config_validation.py:250-257).
  3. Assign the patch. The assignment runs once per process, because Python caches the module import (afd_plugin/compat/patches/config_validation.py:259-260).

On a non-matching, non-dev vLLM the gate is false, so no patch installs. Then worker_cls is never rewritten and AFD does not take effect; register_afd still registers models, the routing strategy and the DBO op.

Deep dive

The monkey-patch rule: why order matters

Python binds names in two ways. A from module import name binds the name to the original object. A later change to module.name changes only new attribute lookups, not names already bound by a from-import.

So a patch must run before the code that reads the attribute. The config patch installs at register_afd time, before any engine or worker builds a config (afd_plugin/__init__.py:172-181). The set_forward_context patch also rebinds the symbol in modules that already imported it, so those modules see the patched version (afd_plugin/compat/patches/async_dp_forward_context.py:208-212).

Deep dive

The full patch table

The reference page lists every patch with its vLLM target line at the pinned commit, its install mechanism and gate, when it is active, and what it changes. See Reference: compat patches.

What can go wrong#

Why it matters. The patches are how AFD runs inside unmodified vLLM. The version gate keeps AFD from breaking a vLLM it was not built for, and the scoped patches keep AFD changes out of native requests.

Check yourself#

  1. 1
    How does AFD change vLLM behavior without editing vLLM?
    easy
    Show answer

    It imports patch modules that replace vLLM functions in place at run time (afd_plugin/__init__.py:172-181).

  2. 2
    What does the config_validation patch rewrite, and only when?
    easy
    Show answer

    It rewrites worker_cls to the AFD role worker, only when it was "auto" (afd_plugin/compat/patches/config_validation.py:92-93, afd_plugin/compat/patches/config_validation.py:175-176).

  3. 3
    What is the difference between the two version checks?
    medium
    Show answer

    The start-up check (assert_vllm_version_supported, strict false) only warns (afd_plugin/compat/vllm.py:38-50). The install gate (is_target_vllm_compatible) decides whether each patch installs, and allows dev builds (afd_plugin/compat/vllm.py:53-68).

  4. 4
    Which GPU patch does not install at import time, and how does it install?
    medium
    Show answer

    gpu_mrv2_ubatching. It installs through scoped context managers, only inside AFD's initialize_kv_cache and execute_model calls (afd_plugin/compat/patches/gpu_mrv2_ubatching.py:263-282).

  5. 5
    The async-DP patches install on GPU too. Why do they not change GPU behavior?
    hard
    Show answer

    They change behavior only for async configs, gated at run time by is_afd_async_dp (afd_plugin/config.py:303-308). On GPU async_dp must be false, so the patches run as transparent re-implementations of the native functions (afd_plugin/compat/patches/async_dp_forward_context.py:88-127).