AFD Plugin Explained

How vLLM plugins work

IntermediateLearn the basicsabout 7 min read + 2 min deep dives

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

In one sentence

vLLM reads entry points in the vllm.general_plugins group and calls each one at start-up. afd-plugin registers itself there.

The idea#

vLLM can load extra code that is not in its own source tree. The extra code is a plugin. A plugin declares an entry point in its packaging metadata. vLLM finds the entry point by its group name and calls it. afd-plugin uses this to add AFD, and it changes no vLLM file.

A general plugin is the simplest kind. It is one function. vLLM calls the function with no arguments. The function then registers models, patches vLLM functions, or sets up whatever the plugin needs. For afd-plugin the function is register_afd().

An everyday comparison. Think of a start-up checklist with named tasks. The host reads the list and runs each task once. The comparison stops here: vLLM runs the list again in every process it starts, and each run starts fresh. So one plugin function runs many times, in many processes.

The picture#

Figure: vLLM calls load_general_plugins() from 5 call sites, in 4 kinds of process. The guard stops a repeat inside one process, not across processes.
Open full size ↗

Read the top panel left to right. Process 0 starts first, then the engine core, then the workers. Each process loads the plugins once. The lower panel shows why a plugin must run safely more than once.

In the code#

Step 1: The five entry-point groups#

vLLM defines five plugin groups. Each group loads in a different set of processes. The comments in the file say where (vllm:vllm/plugins/__init__.py:16-30). afd-plugin uses only the first group, vllm.general_plugins.

# Default plugins group will be loaded in all processes(process0, engine core
# process and worker processes)
DEFAULT_PLUGINS_GROUP = "vllm.general_plugins"
# IO processor plugins group will be loaded in process0 only
IO_PROCESSOR_PLUGINS_GROUP = "vllm.io_processor_plugins"
# Platform plugins group will be loaded in all processes when
# `vllm.platforms.current_platform` is called and the value not initialized,
PLATFORM_PLUGINS_GROUP = "vllm.platform_plugins"
# Stat logger plugins group will be loaded in process0 only when serve vLLM with
# async mode.
STAT_LOGGER_PLUGINS_GROUP = "vllm.stat_logger_plugins"
# Endpoint plugins group is loaded in the API server front end process only.
# Each entry point resolves to a factory returning an `EndpointPlugin`
# (see `vllm/plugins/endpoint_plugins/interface.py`).
ENDPOINT_PLUGINS_GROUP = "vllm.endpoint_plugins"
The five plugin groups and where each one loads.
GroupLoaded in
vllm.general_pluginsAll processes: process 0, the engine core and the workers (vllm:vllm/plugins/__init__.py:16-18).
vllm.io_processor_pluginsProcess 0 only (vllm:vllm/plugins/__init__.py:19-20).
vllm.platform_pluginsAll processes, when current_platform is first used (vllm:vllm/plugins/__init__.py:21-23).
vllm.stat_logger_pluginsProcess 0 only, in async serving mode (vllm:vllm/plugins/__init__.py:24-26).
vllm.endpoint_pluginsThe API server front-end process only (vllm:vllm/plugins/__init__.py:27-30).

Step 2: load_plugins_by_group and load_general_plugins#

Two functions do the work. load_plugins_by_group finds the entry points of one group, filters them, and loads each one (vllm:vllm/plugins/__init__.py:36-74). load_general_plugins loads the general group, then calls each loaded function (vllm:vllm/plugins/__init__.py:87-90).

def load_general_plugins():
    """WARNING: plugins can be loaded for multiple times in different
    processes. They should be designed in a way that they can be loaded
    multiple times without causing issues.
    """
    global plugins_loaded
    if plugins_loaded:
        return
    plugins_loaded = True

    plugins = load_plugins_by_group(group=DEFAULT_PLUGINS_GROUP)
    # general plugins, we only need to execute the loaded functions
    for func in plugins.values():
        func()
load_general_plugins: the guard, then load and call each general plugin.

The filter is the VLLM_PLUGINS environment variable. vLLM parses it in envs.py. When the variable is not set, the value is None and vLLM loads every plugin (vllm:vllm/envs.py:1146-1150). When the variable is an empty string, the value is [""], which matches no plugin name, so vLLM loads none (vllm:vllm/plugins/__init__.py:63-64).

    # if this is not set, it means all plugins will be loaded
    # if this is set to an empty string, no plugins will be loaded
    "VLLM_PLUGINS": lambda: (
        None
        if "VLLM_PLUGINS" not in os.environ
        else os.environ["VLLM_PLUGINS"].split(",")
    ),
VLLM_PLUGINS: unset means all; an empty string means none.

Step 3: When and in which processes#

vLLM calls load_general_plugins() from five call sites. The diagram above shows the four kinds of process. Each call site runs in the process that first needs the plugins. The excerpt below is the engine core.

        # plugins need to be loaded at the engine/scheduler level too
        from vllm.plugins import load_general_plugins

        load_general_plugins()
EngineCore loads the plugins in its own process (v1/engine/core.py).

The deep dive below lists all five call sites and the process of each one.

Step 4: How a plugin registers models, workers and config#

A general plugin registers things through vLLM APIs that run in the same process.

    def register_model(
        self,
        model_arch: str,
        model_cls: type[nn.Module] | str,
    ) -> None:
        """
        Register an external model to be used in vLLM.

        `model_cls` can be either:

        - A [`torch.nn.Module`][] class directly referencing the model.
        - A string in the format `<module>:<class>` which can be used to
          lazily import the model. This is useful to avoid initializing CUDA
          when importing the model and thus the related error
          `RuntimeError: Cannot re-initialize CUDA in forked subprocess`.
        """
register_model: a class, or a lazy "module:Class" string.

Step 5: What a plugin can and cannot change#

A general plugin runs real Python in the vLLM process. So it can change vLLM behaviour by monkey-patching: it replaces a vLLM function or method with its own. afd-plugin does this with its compat patches.

A plugin cannot do some things:

  • It cannot change a name that another module already bound with from ... import. That name keeps the original object. A patch that replaces a module attribute (module.func) changes only later attribute lookups. So the patch must run before the code that reads the attribute.
  • It cannot change compiled C++ or CUDA code. It can only change the Python that calls it.
  • It cannot add a new command-line flag on its own. The plugin function gets no parser.

There is one near-exception for the command line. vLLM calls load_general_plugins() at the start of add_cli_args (vllm:vllm/engine/arg_utils.py:3003-3009). So a plugin that registers a new quantization method or a new platform makes it a valid choice for the existing --quantization or --device flag.

Deep dive#

Deep dive

The 5 call sites, and the process each runs in

vLLM calls load_general_plugins() from five places. The guard makes it run at most once per process, but a new process resets the guard. So the plugin runs in each of these processes.

#Call siteFunctionProcessWhen
1vllm:vllm/engine/arg_utils.py:3009AsyncEngineArgs.add_cli_args Process 0 (launch / API)While the CLI parser is built, so a plugin can extend it.
2vllm:vllm/engine/arg_utils.py:839EngineArgs.__post_init__ Process 0 (launch / API)When the engine config is built from the args.
3vllm:vllm/v1/engine/core.py:119EngineCore.__init__ EngineCore processWhen the engine core starts.
4vllm:vllm/v1/worker/worker_base.py:271WorkerBase.init_worker Each worker processBefore the worker class is resolved and built.
5vllm:vllm/model_executor/models/registry.py:1570_run Model-inspection subprocessIn the short-lived subprocess that resolves a model class (vllm:vllm/model_executor/models/registry.py:1536-1564).

Call sites 1 and 2 are both in process 0. The guard means the second one returns early. The other three run in separate processes, so each one loads the plugins again.

Deep dive

The guard, and why idempotence matters

vLLM keeps a module-level flag, plugins_loaded, that starts at False (vllm:vllm/plugins/__init__.py:33). The first call sets it to True; a later call in the same process returns at once (vllm:vllm/plugins/__init__.py:82-85).

The flag is Python module state. vLLM starts workers with spawn in many setups, which copies no Python state. So each new process imports the modules again, the flag is False again, and the plugin runs again. The function's own docstring warns about this (vllm:vllm/plugins/__init__.py:78-81).

afd-plugin handles it with a second guard of its own, _registered, inside register_afd (afd_plugin/__init__.py:150-153). The guard stops repeated work in one process; it does not stop the many processes. So the registration steps must be safe to run more than once.

In afd-plugin#

afd-plugin is a single general plugin. Three facts connect it to this page:

What can go wrong#

  • VLLM_PLUGINS set to empty. An empty string loads no plugins, so AFD does not activate (vllm:vllm/envs.py:1146-1150).
  • A plugin that is not idempotent. The function runs in many processes. State kept in one process does not reach the others.
  • A patch that runs too late. A monkey-patch cannot change a name that another module already bound with from ... import. The patch must run before that import, or before the attribute lookup it changes.

Why it matters. AFD is installed only through this plugin mechanism. If the plugin does not load, or a patch runs too late, the two roles do not form and the run fails.

Check yourself#

  1. 1
    Which entry-point group does afd-plugin use, and what is its entry point?
    easy
    Show answer

    The vllm.general_plugins group. The entry point is afd = "afd_plugin:register_afd" (pyproject.toml:45-46).

  2. 2
    What does VLLM_PLUGINS do when it is set to an empty string?
    easy
    Show answer

    It loads no plugins. The empty string parses to [""], which matches no plugin name (vllm:vllm/envs.py:1146-1150, vllm:vllm/plugins/__init__.py:63-64).

  3. 3
    Why must a general plugin be safe to run more than once?
    medium
    Show answer

    vLLM calls it in several processes (process 0, the engine core, each worker and a model-inspection subprocess). The plugins_loaded guard resets in every new process (vllm:vllm/plugins/__init__.py:82-85).

  4. 4
    How does a plugin set the worker class, since there is no register API for it?
    medium
    Show answer

    It sets parallel_config.worker_cls. vLLM reads that field and imports the class by its qualified name (vllm:vllm/v1/worker/worker_base.py:273-277).

  5. 5
    A plugin cannot add a new CLI flag. How can it still change what --quantization accepts?
    hard
    Show answer

    vLLM calls load_general_plugins() at the start of add_cli_args. So a plugin that registers a new quantization method before the parser is built makes it a valid choice for the existing flag (vllm:vllm/engine/arg_utils.py:3003-3009).