vLLM Explained

All the ways to extend vLLM

IntermediatevLLM conceptsabout 16 min read + 5 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

In one sentence

vLLM has three kinds of extension points: entry-point groups that vLLM reads, registries that your plugin fills, and config fields that name your class.

The idea#

An extension point is a place where code from outside the vLLM repository can change what vLLM does. Such code is out-of-tree (OOT). vLLM has about twenty extension points. They come in three kinds, and each kind reaches your code in a different way.

  1. Entry-point groups. Your package publishes an entry point under a plugin group, for example vllm.platform_plugins. vLLM finds it with no help from you. There are six groups. The page How vLLM finds and loads plugins explains how and when each process loads them.
  2. Registries. vLLM keeps tables of models, layers, attention backends, quantization methods, connectors and more. A function such as ModelRegistry.register_model adds your class to one table. You call these functions from a general plugin, so that the call runs in every process.
  3. Config hooks. A config field or a CLI flag names your class or your module, for example --scheduler-cls my_pkg.MyScheduler. vLLM imports the name when it needs the class. No entry point is necessary.

The three kinds work together. A typical plugin package publishes one general plugin. Its function calls several register functions. A platform plugin also sets config fields, for example worker_cls, in its check_and_update_config hook.

An everyday comparison. Think of a large office building. The entry-point groups are the front desk: it has a list of named visitors, and it calls each one at a fixed time. The registries are the notice boards on each floor: a visitor pins a card there, and the staff on that floor read it. The config hooks are an order form: you write the name of a contractor, and the building calls that contractor. The comparison stops here. In vLLM each process is its own building with its own notice boards. A card that you pin in the API server is not visible in a worker.

The picture#

Figure: each extension point in the process that reads it. Green items are entry-point groups, blue items are registries, violet items are config fields or flags.
Open full size ↗

Read the diagram column by column. Each item names the line that reads the extension point, so it shows where your code must be visible. Green and violet items need no extra work in that process. A blue item works only if a general plugin (or your platform) has filled the registry in that same process.

The interactive timeline below shows the order of the start-up. Choose the settings, and read which plugin groups and Platform hooks run in each process.

InteractiveStart-up timeline: plugins and Platform hooks per process

This interactive visual needs JavaScript. Turn on JavaScript in your browser to use it. The text on this page explains the same idea.

Press a button to change a setting (Tab, then Enter or Space). The numbers give the start-up order across the three processes. Crossed-out items do not run. acme_hw, acme_admin and acme_metrics are made-up packages.

In the code#

Step 1: the six entry-point groups#

Five group names are constants in vllm/plugins/__init__.py. The sixth group is in the sampler code:

# 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 table gives the short form. The page How vLLM finds and loads plugins has the full detail for each group: the loader, the filter and the error handling.

GroupYour entry point givesLoaded atProcess, whenIt can changePage
vllm.general_pluginsa function with no argumentsvllm/plugins/__init__.py:77-90every process, early in start-upanything; usually it calls the register functions of Step 2How vLLM finds and loads plugins
vllm.platform_pluginsa detector that returns a Platform class name or Nonevllm/platforms/__init__.py:250every process, first read of current_platformthe whole hardware layerPlatform plugins and current_platform
vllm.stat_logger_pluginsa StatLoggerBase subclassvllm/v1/metrics/loggers.py:84-98front end, AsyncLLM.__init__where metrics goPlugins in the API server
vllm.endpoint_pluginsa factory that returns an EndpointPluginvllm/plugins/__init__.py:93-159front end, build_app; only when named in VLLM_PLUGINSHTTP routes and app statePlugins in the API server
vllm.io_processor_pluginsa function that returns an IO processor class name or Nonevllm/plugins/io_processors/__init__.py:42-61front end, only for a pooling model that names a plugin (vllm/plugins/io_processors/__init__.py:52-54)input and output of pooling requestsPlugins in the API server
vllm.logits_processorsa Model Runner V2 LogitsProcessor subclassvllm/v1/worker/gpu/sample/logits_processor/loader.py:37-74workers (load_model) and front end (request check); ignores VLLM_PLUGINSthe logits of a batchSampling and logits processors in Model Runner V2

Step 2: registries that a general plugin fills#

Most extension points are registries. A registry is a module-level table plus a register function. The KV connector factory is a short example:

class KVConnectorFactory:
    _registry: dict[str, Callable[[], type[KVConnectorBase]]] = {}

    @classmethod
    def register_connector(cls, name: str, module_path: str, class_name: str) -> None:
        """Register a connector with a lazy-loading module and class name."""
        if name in cls._registry:
            raise ValueError(f"Connector '{name}' is already registered.")

        def loader() -> type[KVConnectorBase]:
            module = importlib.import_module(module_path)
            return getattr(module, class_name)

        cls._registry[name] = loader
The register function stores a loader, not the class. The module is imported only when a config selects the name. A name that exists already raises ValueError.

vLLM reads the table later, in the process that builds the part. So the register call must run in that process before the read. A general plugin does this, because each process runs it (Step 1). The table lists the registries.

RegistryRegister withRead atProcessIt can changePage
Model registryModelRegistry.register_model(arch, cls or "module:Class") (vllm/model_executor/models/registry.py:1160-1203)inspect vllm/config/model.py:741; resolve vllm/model_executor/model_loader/utils.py:310front end (inspect), workers (build)add or replace a model architectureOut-of-tree models and the model registry
Out-of-tree ops and layers@MyOp.register_oot on a subclass of a CustomOp or PluggableLayer (vllm/model_executor/custom_op.py:335-356, vllm/model_executor/custom_op.py:82-99)__new__ swaps the class (vllm/model_executor/custom_op.py:117-126, vllm/model_executor/custom_op.py:56-65)workers, when layers are madethe kernel of one layer, or the whole layerCustomOp, out-of-tree ops and IR op providers
Attention backendsregister_backend(enum or "NAME", class_path) (vllm/v1/attention/backends/registry.py:348-420)get_path (vllm/v1/attention/backends/registry.py:210)workersreplace a backend, or add a new nameHow vLLM chooses an attention backend
Quantization methods@register_quantization_config("name") (vllm/model_executor/layers/quantization/__init__.py:61-109)get_quantization_config (vllm/model_executor/layers/quantization/__init__.py:112-114)front end: it checks the name and makes the config object (vllm/config/vllm.py:1586-1589); a worker gets that object inside the pickled VllmConfiga new --quantization valueQuantization methods as plugins
Model loaders@register_model_loader("format") (vllm/model_executor/model_loader/__init__.py:69-120)get_model_loader (vllm/model_executor/model_loader/__init__.py:123-128)workersa new --load-formatHooks in the engine core and the workers (register_model_loader); "Weight loading: from checkpoint files to GPU parameters" (the default loader)
KV connectorsKVConnectorFactory.register_connector(name, module, class) (vllm/distributed/kv_transfer/kv_connector/factory.py:31-40)scheduler role vllm/v1/core/sched/scheduler.py:177-181; worker role vllm/distributed/kv_transfer/kv_transfer_state.py:88-92; class checks vllm/config/vllm.py:1242, vllm/config/vllm.py:1966, vllm/config/vllm.py:2243, vllm/v1/metrics/loggers.py:132, vllm/distributed/kv_transfer/kv_connector/v1/metrics.py:161front end (class only), engine core and workerswhere KV blocks go (offload, transfer)KV connectors
Encoder-cache (EC) connectorsECConnectorFactory.register_connector (vllm/distributed/ec_transfer/ec_connector/factory.py:23-33)vllm/v1/core/sched/scheduler.py:203; vllm/distributed/ec_transfer/ec_transfer_state.py:37; metrics vllm/v1/metrics/loggers.py:134front end (class only), engine core and workerstransfer of encoder outputsKV connectors
Tool and reasoning parsersToolParserManager.register_module (vllm/tool_parsers/abstract_tool_parser.py:327-370), ReasoningParserManager.register_module (vllm/reasoning/abs_reasoning_parsers.py:330-368)--tool-call-parser, --reasoning-parser namesfront end; reasoning also engine corehow model text becomes tool calls or reasoningTool and reasoning parsers: from model text to tool_calls and reasoning
LoRA resolversLoRAResolverRegistry.register_resolver (vllm/lora/resolver.py:52-72)vllm/entrypoints/openai/models/serving.py:113-117front endwhere an unknown adapter name is foundPlugins in the API server
Sleep-mode backendsSleepModeBackendFactory.register_backend (vllm/device_allocator/sleep_mode_backend.py:170-180)vllm/v1/worker/gpu_worker.py:256-258workershow memory sleeps and wakesHooks in the engine core and the workers
KV cache dtypes@register_kv_cache_dtype("name") (vllm/config/cache.py:111-155)KV_CACHE_DTYPES, STR_DTYPE_TO_TORCH_DTYPE (vllm/config/cache.py:147-152)every process that reads the dtypea new --kv-cache-dtypeHooks in the engine core and the workers

Step 3: config hooks that name your class#

A config hook is a field of VllmConfig (or a CLI flag) that holds the name of your class or module. The front end parses it. The other processes get it inside the pickled VllmConfig, because the start method is spawn and the config is a process argument (vllm/v1/engine/utils.py:256-287). The scheduler hook shows the pattern:

    def get_scheduler_cls(self) -> type["SchedulerInterface"]:
        if self.scheduler_cls is None:
            if self.async_scheduling:
                from vllm.v1.core.sched.async_scheduler import AsyncScheduler

                return AsyncScheduler
            from vllm.v1.core.sched.scheduler import Scheduler

            return Scheduler

        # The first half of this warning can be removed once the Scheduler interface is
        # finalized and we can maintain support for scheduler classes that implement it
        if not (
            isinstance(self.scheduler_cls, str)
            and self.scheduler_cls.startswith("vllm.")
        ):
            logger.warning_once(
                "Using custom scheduler class %s. This scheduler interface is not "
                "public and compatibility may not be maintained. If you have "
                "subclassed Scheduler instead of AsyncScheduler, you will see "
                "degraded performance due to async scheduling being disabled.",
                self.scheduler_cls,  # type: ignore[arg-type]
            )
        if not isinstance(self.scheduler_cls, str):
            return cast(type["SchedulerInterface"], self.scheduler_cls)
        return resolve_obj_by_qualname(self.scheduler_cls)
No value: vLLM picks its own class. A value outside vllm.: a warning, then resolve_obj_by_qualname imports the class from a dotted name.
Field or flagDefined atRead atProcessIt can changePage
scheduler_cls (--scheduler-cls)vllm/config/scheduler.py:177vllm/v1/engine/core.py:164engine corethe scheduling policyHooks in the engine core and the workers
worker_cls (--worker-cls)vllm/config/parallel.py:306vllm/v1/worker/worker_base.py:292-296workersthe whole worker class; a platform sets it when it is "auto"The Platform interface
worker_extension_cls (--worker-extension-cls)vllm/config/parallel.py:309vllm/v1/worker/worker_base.py:304-331workersextra methods for collective_rpcHooks in the engine core and the workers
--tool-parser-pluginvllm/entrypoints/common/cli_args.py:126vllm/entrypoints/common/cli_args.py:579-580front endimports a module or a file that registers a tool parserTool and reasoning parsers: from model text to tool_calls and reasoning
--reasoning-parser-pluginvllm/config/structured_outputs.py:37vllm/entrypoints/common/cli_args.py:582-583; vllm/v1/structured_output/__init__.py:100-104front end and engine coreimports a module or a file that registers a reasoning parserTool and reasoning parsers: from model text to tool_calls and reasoning
kv_connector_module_path (in --kv-transfer-config)vllm/config/kv_transfer.py:81vllm/distributed/kv_transfer/kv_connector/factory.py:103-115engine core and workersloads a connector class with no registration; it wins over the registryKV connectors

Two more fields name classes. logits_processors takes classes or "module:Class" strings (vllm/config/model.py:407, vllm/v1/worker/gpu/sample/logits_processor/loader.py:77-105). model_class_overrides registers a model class in every process, but its docstring says it is "for development and debugging purposes only" (vllm/config/model.py:351-357, vllm/config/model.py:1170-1193).

Step 4: which one do I need?#

Start from your goal. Each line names the extension point and the page that explains it.

The tours Tour: Life of a plugin and Tour: Build a toy plugin follow one plugin through these steps. The page Extension points will hold the full reference table.

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Registries live in one process

Each registry is a Python object at module level: a dict or a list in a module or a class. Examples: KVConnectorFactory._registry (vllm/distributed/kv_transfer/kv_connector/factory.py:28), _ATTN_OVERRIDES (vllm/v1/attention/backends/registry.py:344), _CUSTOMIZED_METHOD_TO_QUANT_CONFIG (vllm/model_executor/layers/quantization/__init__.py:58). A spawned child is a new interpreter, so it starts with the built-in entries only. (Derived from the start method, vllm/entrypoints/common/startup.py:147-149.)

So the place of the register call matters:

  1. A general plugin function runs in each process (vllm/v1/engine/core.py:132, vllm/v1/worker/worker_base.py:284). Its register calls reach every registry. This is the normal way.
  2. A platform module can register at import time, or in a hook. The test platform uses a second entry point in vllm.general_plugins to import its custom ops (tests/plugins/vllm_add_dummy_platform/setup.py:10-17).
  3. Code in your own launch script runs only in that process. With vllm serve the workers never see it.

Config hooks do not have this problem. The value is a string in VllmConfig, and each process imports the class itself. For example, a worker resolves worker_cls with resolve_obj_by_qualname (vllm/v1/worker/worker_base.py:293-296).

Worked example. One general plugin function registers a model, a quantization method and a KV connector. You run vllm serve with TP = 2. The function runs 4 times: front end, engine core, worker 0, worker 1. The front end uses the model entry for inspect_model_cls and the quantization entry to make the quantization config object. It uses the connector entry to check the connector class. The engine core uses the connector in the scheduler role. Each worker uses the model entry and the connector entry. It gets the quantization config as an object inside the pickled VllmConfig; Python imports the module of its class by name when it unpickles it. (Derived from the read lines in Step 2. A model registry subprocess, if one runs, is one more process.)

Deep dive

What a second registration does, per registry

A general plugin runs in each process once. But two packages can use the same name, or a name can collide with a built-in entry. The registries do not agree on what happens then:

Register functionName exists alreadyWhere
ModelRegistry.register_modeloverwrites; DEBUG logvllm/model_executor/models/registry.py:1179-1185
CustomOp.register_oot, PluggableLayer.register_ootAssertionError ("Duplicate op name" or "Duplicate layer name")vllm/model_executor/custom_op.py:338, vllm/model_executor/custom_op.py:86
register_backendan enum member: overwrites the override, no log; a new string name that exists already: ValueErrorvllm/v1/attention/backends/registry.py:406-418, vllm/v1/attention/backends/registry.py:180-185
register_quantization_configoverwrites; DEBUG logvllm/model_executor/layers/quantization/__init__.py:89-95, vllm/model_executor/layers/quantization/__init__.py:106
register_model_loaderoverwrites; WARNING logvllm/model_executor/model_loader/__init__.py:101-107
KVConnectorFactory.register_connector, ECConnectorFactory.register_connectorValueErrorvllm/distributed/kv_transfer/kv_connector/factory.py:33-34, vllm/distributed/ec_transfer/ec_connector/factory.py:26-27
ToolParserManager.register_module(module=...)overwrites when force=True (the default); KeyError when force=Falsevllm/tool_parsers/abstract_tool_parser.py:307-311
ReasoningParserManager.register_module(module=...)same rule as the tool parser managervllm/reasoning/abs_reasoning_parsers.py:310-314
LoRAResolverRegistry.register_resolveroverwrites; WARNING logvllm/lora/resolver.py:64-72
SleepModeBackendFactory.register_backendValueErrorvllm/device_allocator/sleep_mode_backend.py:173-174
register_kv_cache_dtypeoverwrites the handler; WARNING logvllm/config/cache.py:139-152

A ValueError or an AssertionError inside a general plugin function stops the start-up of that process, because load_general_plugins() does not catch it (vllm/plugins/__init__.py:89-90).

register_quantization_config has one more effect. A new name is added to QUANTIZATION_METHODS. It is also added to current_platform.supported_quantization, but only when that list is not empty (vllm/model_executor/layers/quantization/__init__.py:96-100). An empty list means "no limit" in verify_quantization (vllm/platforms/interface.py:1153-1158). The module imports current_platform at module level (vllm/model_executor/layers/quantization/__init__.py:11), so its import resolves the platform, if no code has done so before.

Deep dive

Name formats: dots, colons and paths

The extension points do not use one format for a class name. Use the right one, or the import fails.

WhereFormatParsed by
Platform detector result, worker_cls, worker_extension_cls, scheduler_cls, register_backend(class_path=...)pkg.module.Class (dots only)resolve_obj_by_qualname: split at the last dot (vllm/utils/import_utils.py:119-123)
register_model, logits_processors, model_class_overridespkg.module:Class (one colon)split at ":"; other counts raise ValueError (vllm/model_executor/models/registry.py:1187-1193, vllm/v1/worker/gpu/sample/logits_processor/loader.py:99-104)
KV and EC connectorsmodule path and class name as two stringsimportlib.import_module and getattr (vllm/distributed/kv_transfer/kv_connector/factory.py:36-38)
--tool-parser-plugin, --reasoning-parser-plugina module name, or a path to a .py fileimport_plugin: a module first; if no such module exists, a file. A failure of the file import is logged, not raised (vllm/utils/import_utils.py:81-98)

The parser plugin flags have one more rule: vLLM imports the value only when it is longer than 3 characters (vllm/entrypoints/common/cli_args.py:579, vllm/entrypoints/common/cli_args.py:582). The code does not say why.

Module path versus registry. The KV and EC connector factories use the two sources in the opposite order. For KV connectors, kv_connector_module_path wins over the registry (vllm/distributed/kv_transfer/kv_connector/factory.py:106-108). For EC connectors, the registry wins, and ec_connector_module_path is the fallback (vllm/distributed/ec_transfer/ec_connector/factory.py:66-73).

Deep dive

Lazy registration: why many registries store strings

Several registries store a name or a loader, not a class:

The general plugin runs in the front end too, where no model runs. A lazy entry keeps heavy imports (CUDA code, kernels) out of the front end. Only the process that uses the entry imports the module.

What can go wrong#

  • The registry is empty in a worker. You registered from a launch script or from code that runs only in the front end. Register from a general plugin instead (first deep dive).
  • An allowlist filters your general plugin. VLLM_PLUGINS applies to general plugins too (vllm/plugins/__init__.py:63-64). Then no register call runs, with no error. Use the timeline above with VLLM_PLUGINS=acme_admin to see it.
  • A short parser plugin value is ignored. A value of 3 characters or fewer is not imported (vllm/entrypoints/common/cli_args.py:579). A path that fails to import only writes an ERROR log (vllm/utils/import_utils.py:94-98).
  • A custom scheduler loses async scheduling. vLLM warns: if you subclass Scheduler instead of AsyncScheduler, async scheduling is off and performance drops (vllm/config/scheduler.py:247-253).
  • A worker extension hides a method. If the worker class already has an attribute of the extension, an assert stops start-up (vllm/v1/worker/worker_base.py:310-319).

Why it matters. The right extension point depends on the process that must see your code. An entry-point group or a config hook reaches its process by itself. A registry reaches only the process where your register call ran. Most "it works in the front end but not in the worker" problems come from this difference.

Check yourself#

  1. 1
    What are the three kinds of extension points, and which kind needs a general plugin?
    easy
    Show answer

    Entry-point groups, registries and config hooks. Registries need a general plugin (or another way to run the register call in each process). Each registry is a module-level object of one process. The general plugin runs in every process (vllm/plugins/__init__.py:77-90).

  2. 2
    You want a new HTTP route. Which extension point do you use, and what must you also set?
    easy
    Show answer

    An endpoint plugin in vllm.endpoint_plugins. You must also name it in VLLM_PLUGINS, because endpoint plugins load only when the list names them (vllm/plugins/__init__.py:122-131).

  3. 3
    Your general plugin calls KVConnectorFactory.register_connector("NixlConnector", ...). What happens at start-up?
    medium
    Show answer

    vLLM already registers a connector with this name when the factory module loads (vllm/distributed/kv_transfer/kv_connector/factory.py:177-181). The register function raises ValueError for a name that exists (vllm/distributed/kv_transfer/kv_connector/factory.py:33-34). The plugin function does not catch it, so start-up stops (vllm/plugins/__init__.py:89-90).

  4. 4
    You set --scheduler-cls my_pkg.sched:MySched. Why does it fail, and what is the right form?
    medium
    Show answer

    scheduler_cls is parsed by resolve_obj_by_qualname, which splits at the last dot (vllm/config/scheduler.py:256, vllm/utils/import_utils.py:119-123). The colon form is for register_model. Write my_pkg.sched.MySched.

  5. 5
    You serve with TP = 2. You set kv_connector_module_path and also register a connector with the same class name through a general plugin. Which class runs, and in which processes is it made?
    hard
    Show answer

    The class from kv_connector_module_path: for KV connectors the module path wins over the registry (vllm/distributed/kv_transfer/kv_connector/factory.py:106-108). It is made in the engine core, in the scheduler role (vllm/v1/core/sched/scheduler.py:177-181), and in each of the 2 workers, in the worker role (vllm/distributed/kv_transfer/kv_transfer_state.py:88-92). The front end only gets the class, for config checks (vllm/config/vllm.py:2243). For EC connectors the order is the opposite (vllm/distributed/ec_transfer/ec_connector/factory.py:66-73).