vLLM Explained

The Platform interface

AdvancedvLLM conceptsabout 10 min read + 5 min deep dives

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

In one sentence

The Platform class is the contract for a hardware vendor: vLLM reads its attributes and calls its hooks at fixed points of the start-up.

The idea#

The page Platform plugins and current_platform explains how a process chooses its platform. This page explains what the chosen object must do. Platform in vllm/platforms/interface.py is a class with 90 methods (84 without a leading underscore) and 12 class attributes. Most have a default. A vendor subclass overrides the ones that differ on its hardware.

The members fall into four groups:

  1. Identity attributes. Values that describe the device: its name, its PyTorch device type, its dispatch key, its distributed backend, and more.
  2. Config hooks. A platform hook that vLLM calls while it builds VllmConfig. The platform can check the config, change defaults, or reject a setting. check_and_update_config is the most important one: it sets the worker class.
  3. Component pickers. A component picker returns the dotted name of a class: the attention backend, the device communicator, the LoRA kernel wrapper, the graph wrapper. vLLM calls it where it builds that component.
  4. Kernel hooks. Methods that import kernel libraries or replace Triton kernels.

Anything that the class does not define goes to a fallback: Platform.__getattr__ looks for the name in torch.<device_type>, for example torch.cuda.

An everyday comparison. Think of a franchise manual. Head office writes the chapters and a default answer for each one. A local shop rewrites only the chapters that differ in its country. Head office reads each chapter at a fixed moment: the opening checklist before the shop opens, the staff chapter when it hires. The comparison stops here. In vLLM each process reads its own copy of the manual. The front end reads the config chapters; a worker reads the chapters about the model and the GPU.

The picture#

Figure: the Platform hooks of a vllm serve start-up with TP = 2, numbered in order. Config hooks (violet) run in the front end; component pickers (blue) run where the component is built.
Open full size ↗

Read the numbers in order. Steps 1-6 run once, in the front end, and shape the VllmConfig. The engine core and the workers get that config through pickling, and they detect the platform again for themselves. The pickers in the worker column run in each worker.

In the code#

Step 1: identity attributes#

class Platform:
    _enum: PlatformEnum
    device_name: str
    device_type: str

    # available dispatch keys:
    # check https://github.com/pytorch/pytorch/blob/313dac6c1ca0fa0cde32477509cce32089f8532a/torchgen/model.py#L134 # noqa
    # use "CPU" as a fallback for platforms not registered in PyTorch
    dispatch_key: str = "CPU"

    # available ray device keys:
    # https://github.com/ray-project/ray/blob/10ba5adadcc49c60af2c358a33bb943fb491a171/python/ray/_private/ray_constants.py#L438 # noqa
    # empty string means the device does not support ray
    ray_device_key: str = ""

    @classmethod
    def log_warnings(cls) -> None:
        """Log platform-specific diagnostics after logging is configured."""

    # platform-agnostic way to specify the device control environment variable,
    # .e.g. CUDA_VISIBLE_DEVICES for CUDA.
    # hint: search for "get_visible_accelerator_ids_env_var" in
    # https://github.com/ray-project/ray/tree/master/python/ray/_private/accelerators # noqa
    device_control_env_var: str = "VLLM_DEVICE_CONTROL_ENV_VAR_PLACEHOLDER"

    # environment variables that need to be set to 1 to prevent ray from
    # setting the visible devices e.g.
    # RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES
    ray_noset_device_env_vars: list[str] = []

    # The torch.compile backend for compiling simple and
    # standalone functions. The default value is "inductor" to keep
    # the same behavior as PyTorch.
    # NOTE: for the forward part of the model, vLLM has another separate
    # compilation strategy.
    simple_compile_backend: str = "inductor"

    # The backend used for distributed communication.
    dist_backend: str = ""

    supported_quantization: list[str] = []

    additional_env_vars: list[str] = []

    _global_graph_pool: Any | None = None
The class attributes. _enum, device_name and device_type have no default: a subclass must set them.

The table compares the defaults with CUDA (vllm/platforms/cuda.py:226-236):

AttributeDefaultCUDAUsed for
_enumnonePlatformEnum.CUDAis_cuda(), is_out_of_tree() and the other checks (vllm/platforms/interface.py:196-222). An out-of-tree class uses PlatformEnum.OOT.
device_namenone"cuda"log and error texts; for an out-of-tree platform also the torch.device name of a process group (vllm/distributed/parallel_state.py:540-543)
device_typenone"cuda"the DeviceConfig device (vllm/engine/arg_utils.py:2248) and the torch.<device_type> fallback
dispatch_key"CPU""CUDA"the PyTorch dispatcher key of the device; the comment says "CPU" is a fallback for unregistered platforms
ray_device_key"" (no Ray)"GPU"the Ray resource name
device_control_env_vara placeholder name"CUDA_VISIBLE_DEVICES"the mapping of logical, visible and physical device ids (vllm/platforms/interface.py:287-315)
dist_backend"""nccl"the torch.distributed backend
supported_quantization[] (no limit)not set (no limit)verify_quantization (vllm/platforms/interface.py:1153-1158); ROCm and XPU set a list (vllm/platforms/rocm.py:625, vllm/platforms/xpu.py:113)

Step 2: config hooks, and when each one runs#

Four hooks change the config. Three of them run in the front end, while vllm serve parses the flags and builds VllmConfig:

  1. pre_register_and_update(parser) runs in AsyncEngineArgs.add_cli_args, after the general plugins and after the parser has its flags (vllm/engine/arg_utils.py:3243-3257). The docstring names the use: register an out-of-tree quantization config (vllm/platforms/interface.py:638-651). It runs a second time, with no parser, at the start of create_engine_config (vllm/engine/arg_utils.py:2246).
  2. apply_config_platform_defaults(vllm_config) runs in VllmConfig.__post_init__ (vllm/config/vllm.py:1804). It changes defaults, for example the list of custom ops. ROCm uses it to turn on custom ops for its AITER kernels (vllm/platforms/rocm.py:1074-1084).
  3. check_and_update_config(vllm_config) runs late in VllmConfig.__post_init__ (vllm/config/vllm.py:2046). The method goes on after it, to line 2318 (vllm/config/vllm.py:2318). It can raise for a setting that the hardware does not support, or change the config. Its most important job is worker_cls. CUDA shows the pattern:
    @classmethod
    def check_and_update_config(cls, vllm_config: VllmConfig) -> None:
        parallel_config = vllm_config.parallel_config
        model_config = vllm_config.model_config

        if parallel_config.worker_cls == "auto":
            parallel_config.worker_cls = "vllm.v1.worker.gpu_worker.Worker"

        scheduler_config = vllm_config.scheduler_config
        # Note: model_config may be None during testing
        if (
            model_config is not None
            and model_config.is_mm_prefix_lm
            and scheduler_config.is_multimodal_model
            and not scheduler_config.disable_chunked_mm_input
        ):
            logger.warning_once(
                "Forcing --disable_chunked_mm_input for models "
                "with multimodal-bidirectional attention."
            )
            scheduler_config.disable_chunked_mm_input = True
The default of worker_cls is "auto" (vllm/config/parallel.py:306). The platform replaces it only when the user did not set a class.

The fourth config hook, update_block_size_for_backend, runs later, after the model is loaded, because it needs the attention backends of the layers (vllm/platforms/interface.py:749-794). With worker processes, each worker calls it (vllm/v1/executor/multiproc_executor.py:701-702). With the uni executor, the engine core process calls it (vllm/v1/executor/uniproc_executor.py:74-75). When the user did not set --block-size, it picks a block size that all attention backends accept. That is the preferred size of a single backend, else the default 16 if all accept it, else the smallest common size (vllm/platforms/interface.py:771-781, vllm/platforms/interface.py:721-739).

The front end also calls validate_environ, which warns about unknown VLLM_* variables (vllm/engine/arg_utils.py:2250, vllm/platforms/interface.py:1454-1468).

Step 3: component pickers#

A picker returns a dotted class name, and the caller imports it with resolve_obj_by_qualname. The attention selector is the main example:

@cache
def _cached_get_attn_backend(
    backend,
    attn_selector_config: AttentionSelectorConfig,
    num_heads: int | None = None,
    *,
    _run_kv_cache_dtype: CacheDType | None = None,
) -> type[AttentionBackend]:
    # Some platform selectors inspect the run-wide cache dtype. Keep it in the
    # cache key even though they read the value from the current config.
    from vllm.platforms import current_platform

    attention_cls = current_platform.get_attn_backend_cls(
        backend,
        attn_selector_config=attn_selector_config,
        num_heads=num_heads,
    )
    if not attention_cls:
        raise ValueError(
            f"Invalid attention backend for {current_platform.device_name}"
        )
    backend = resolve_obj_by_qualname(attention_cls)
    return backend
@cache keeps one answer per argument set. An empty string from the platform raises ValueError; the base class returns an empty string (vllm/platforms/interface.py:388-396).

The table lists every picker that this page covers, with its caller.

HookCalled fromProcessWhen in start-upDefault
get_attn_backend_clsvllm/v1/attention/selector.py:218workerload_model: each attention layer asks in its __init__ (vllm/model_executor/layers/attention/attention.py:350)"" (error)
get_device_communicator_clsvllm/distributed/parallel_state.py:549-552workerinit_device, for each group with more than one rankDeviceCommunicatorBase (vllm/platforms/interface.py:1218-1221)
get_punica_wrappervllm/lora/punica_wrapper/punica_selector.py:13-16workeronly with LoRA, when the LoRA model manager is made (vllm/lora/model_manager.py:208)NotImplementedError (vllm/platforms/interface.py:1203-1206)
get_static_graph_wrapper_clsvllm/compilation/backends.py:690-692workercompilation: wraps each piecewise graphAbstractStaticGraphWrapper (vllm/platforms/interface.py:1328-1331)
get_pass_manager_cls, pass_keyvllm/compilation/backends.py:875-878workerVllmBackend.__init__ (compilation)PostGradPassManager (vllm/platforms/interface.py:246-251)
get_compile_backendvllm/config/compilation.py:1080-1081; vllm/compilation/backends.py:122-126front end (config); worker (compiler)when backend is empty; when the backend is not inductor or eagersimple_compile_backend = "inductor" (vllm/platforms/interface.py:253-256)
get_kv_cache_config_builder_clsvllm/v1/core/kv_cache_config_builder.py:98-103engine coreKV cache planning (vllm/v1/engine/core.py:311 through vllm/v1/core/kv_cache_planning.py:1497)DefaultKVCacheConfigBuilder (vllm/platforms/interface.py:1470-1490)
opaque_attention_opvllm/model_executor/layers/attention/attention.py:440workerattention layer __init__False (vllm/platforms/interface.py:1285-1290)

opaque_attention_op is a yes/no question, not a class name, but it picks a path too. True means that vLLM registers attention as one opaque custom op for torch.compile. False means that the layer calls the backend directly, and torch.compile traces it (vllm/model_executor/layers/attention/attention.py:436-440).

Step 4: kernel hooks#

Step 5: the fallback to torch.<device_type>#

    def __getattr__(self, key: str):
        # Pickle checks dunder methods like __getstate__. If we return None
        # for them, pickle treats it like a real value and tries to call it.
        if key.startswith("__") and key.endswith("__"):
            raise AttributeError(key)

        device = getattr(torch, self.device_type, None)
        if device is not None and hasattr(device, key):
            attr = getattr(device, key)
            # NOTE: `hasattr(device, key)=True` can only avoid AttributeError,
            # but the value of this attr could be `None`.
            if attr is not None:
                return attr

        logger.warning_once(
            "Current platform %s does not have '%s' attribute.",
            self.device_type,
            key,
        )
        return None
  1. Lines 1303-1304. Dunder names raise AttributeError at once. The comment says why: pickle looks for names such as __getstate__.
  2. Lines 1306-1312. The method looks up torch.cuda (for CUDA) and returns the attribute, if it is not None.
  3. Lines 1314-1319. Otherwise it logs a warning once and returns None. It does not raise.

So current_platform.synchronize is torch.cuda.synchronize on CUDA, and the forward context checks for None before the call (vllm/forward_context.py:368-370). CudaPlatform does not define synchronize itself; the fallback finds it. (Derived from the method list of cuda.py.)

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

Every hook with its caller, process and start-up moment

This table covers the hooks that shape the start-up of vllm serve. "Front end" means the process that parses the flags and builds VllmConfig.

HookCalled fromProcessWhen
log_warningsvllm/logger.py:295-303each process that configures loggingwhen the process configures logging, and only when the logging config changed (vllm/logger.py:287-292, vllm/logger.py:306-309); for example at vllm/engine/arg_utils.py:2244
pre_register_and_update(parser)vllm/engine/arg_utils.py:3257front endwhile the CLI builds its parser, before it parses the flags
pre_register_and_update()vllm/engine/arg_utils.py:2246front endstart of create_engine_config, after the flags are parsed
validate_environvllm/engine/arg_utils.py:2250front endcreate_engine_config
check_max_model_lenvllm/config/model.py:2653front endwhile ModelConfig computes max_model_len
verify_quantizationvllm/config/model.py:1470front endwhile ModelConfig checks the quantization method
apply_config_platform_defaultsvllm/config/vllm.py:1804front endVllmConfig.__post_init__, before the compilation mode is set (lines 1806-1810)
check_and_update_configvllm/config/vllm.py:2046front endlate in VllmConfig.__post_init__
validate_requestvllm/v1/engine/input_processor.py:430front endfor each request
register_triton_kernel_overridesvllm/v1/worker/worker_base.py:290workerinit_worker
get_device_communicator_clsvllm/distributed/parallel_state.py:551workerinit_device
verify_model_archvllm/model_executor/models/registry.py:1132where the model class is checkedmodel inspection
get_attn_backend_cls, opaque_attention_opvllm/v1/attention/selector.py:218, vllm/model_executor/layers/attention/attention.py:440workerload_model
update_block_size_for_backendvllm/v1/executor/multiproc_executor.py:702, vllm/v1/executor/uniproc_executor.py:75worker (mp) or engine core (uni)after load_model
register_custom_kv_cache_specsvllm/v1/core/single_type_kv_cache_manager.py:2719where KV cache specs are registeredKV cache spec registration
get_kv_cache_config_builder_clsvllm/v1/core/kv_cache_config_builder.py:101engine core (also MRV2 workers, vllm/v1/worker/gpu/cudagraph_utils.py:1045)KV cache planning

Not every hook runs once. pre_register_and_update runs twice in the front end of vllm serve (the two lines above). check_and_update_config and apply_config_platform_defaults run again whenever code makes a new VllmConfig. vLLM's replace() helper does that: it calls the class again with the old fields (vllm/config/utils.py:121-129). For example, the EAGLE drafter loader in a worker calls it when the draft load config differs (vllm/v1/worker/gpu/spec_decode/eagle/utils.py:90-91). So these hooks must give the same result when they run again on a config that they have already changed.

Deep dive

CudaPlatform as the reference implementation

cuda.py has two classes and picks one when the module loads. If pynvml.nvmlInit() works, CudaPlatform is NvmlCudaPlatform; otherwise (for example on Jetson) it is NonNvmlCudaPlatform (vllm/platforms/cuda.py:1040-1054). Both subclass CudaPlatformBase (vllm/platforms/cuda.py:226). The module docstring says that the code may use pynvml but must not start a CUDA context (vllm/platforms/cuda.py:3-5).

get_attn_backend_cls, step by step (vllm/platforms/cuda.py:419-518):

  1. Read the device capability, for example (9, 0) on H100.
  2. If the user selected a backend (--attention-backend), validate only that one. An import error or an invalid configuration raises ValueError; there is no fallback (lines 430-449).
  3. Else get the priority list for the case (MLA or not, capability, head count, KV cache dtype) and validate each backend in order (get_valid_backends, vllm/platforms/cuda.py:377-417).
  4. No valid backend: ValueError with every reason (lines 471-475).
  5. Pick the valid backend with the smallest priority number (lines 479-485).
  6. If the user set --block-size and that excluded a better backend, warn (lines 489-506).
  7. Log the choice once and return the dotted class path (lines 508-518).

Other CUDA answers.

HookCUDA answerLines
check_and_update_configworker_cls = vllm.v1.worker.gpu_worker.Worker; forces disable_chunked_mm_input for some multimodal models; a WSL warningvllm/platforms/cuda.py:325-366
get_device_communicator_clsCudaCommunicatorvllm/platforms/cuda.py:580-584
get_punica_wrapperPunicaWrapperGPUvllm/platforms/cuda.py:576-578
get_static_graph_wrapper_clsCUDAGraphWrappervllm/platforms/cuda.py:598-600
opaque_attention_opTruevllm/platforms/cuda.py:594-596
import_kernels_C_stable_libtorch, optional _moe_C_stable_libtorch and _qutlass_Cvllm/platforms/cuda.py:238-250

CUDA does not override apply_config_platform_defaults or pre_register_and_update; ROCm overrides both (vllm/platforms/rocm.py:1064-1084). ROCm's pre_register_and_update sets an environment variable with os.environ.setdefault, so a second call does nothing new (vllm/platforms/rocm.py:1072).

Deep dive

The minimum for a new platform

The design doc lists the members that a platform must implement at least: _enum, device_type, device_name, check_and_update_config (which must set worker_cls), get_attn_backend_cls and get_device_communicator_cls (docs/design/plugin_system.md:100-108). The code shows why each one matters:

The test platform overrides only check_and_update_config (it sets custom_ops = ["all"]) and get_attn_backend_cls (tests/plugins/vllm_add_dummy_platform/vllm_add_dummy_platform/dummy_platform.py:15-32). It leaves worker_cls as it is, because the tests do not start real workers on it. (Derived.)

Deep dive

Docs and docstrings that do not match the code

What can go wrong#

  • A missing method returns None. Platform.__getattr__ logs "does not have ... attribute" once and returns None (vllm/platforms/interface.py:1314-1319). The caller then fails later with a TypeError if it calls the value. (Derived.)
  • LoRA on a new platform. The base get_punica_wrapper raises NotImplementedError (vllm/platforms/interface.py:1203-1206). Implement it, or do not use --enable-lora.
  • A selected backend is invalid. On CUDA, --attention-backend with an invalid backend raises ValueError; vLLM does not fall back (vllm/platforms/cuda.py:430-446).
  • The block sizes do not fit together. When the attention backends share no block size, update_block_size_for_backend raises ValueError (vllm/platforms/interface.py:740-747).
  • A quantization method is not supported. If the platform sets supported_quantization and the method is not in it, verify_quantization raises ValueError (vllm/platforms/interface.py:1153-1158).

Why it matters. A vendor port of vLLM is mostly a Platform subclass plus a worker class. Knowing which hook runs in which process, and when, tells you where to put each change and where to look when start-up fails.

Check yourself#

  1. 1
    Which Platform hook sets the worker class, and in which process does it run?
    easy
    Show answer

    check_and_update_config. CUDA sets worker_cls to vllm.v1.worker.gpu_worker.Worker when it is "auto" (vllm/platforms/cuda.py:330-331). It runs in VllmConfig.__post_init__ (vllm/config/vllm.py:2046), in the front end for vllm serve.

  2. 2
    Your platform class has no synchronize method, and its device_type is "cuda". What does current_platform.synchronize return?
    easy
    Show answer

    torch.cuda.synchronize. Platform.__getattr__ looks up the name in torch.<device_type> (vllm/platforms/interface.py:1306-1312).

  3. 3
    With TP = 1, which process calls get_attn_backend_cls, and is get_device_communicator_cls called?
    medium
    Show answer

    The engine core process: the uni executor runs the worker there and loads the model (vllm/v1/executor/uniproc_executor.py:68-75). get_device_communicator_cls is not called, because the code asks for it only when the group has more than one rank (vllm/distributed/parallel_state.py:549-552).

  4. 4
    Your check_and_update_config appends "+my_op" to compilation_config.custom_ops each time it runs. Why can the list get the entry twice, and what is the fix?
    hard
    Show answer

    check_and_update_config runs in every VllmConfig.__post_init__ (vllm/config/vllm.py:2046). vLLM's replace() makes a new VllmConfig from the old fields, so __post_init__ runs again on a list that already has the entry (vllm/config/utils.py:121-129). An example is the EAGLE drafter loader (vllm/v1/worker/gpu/spec_decode/eagle/utils.py:90-91). Append only when the entry is missing, as the CPU platform does for +gelu (vllm/platforms/cpu.py:394-399). (Derived: the new config shares the old compilation_config object, because replace() passes the same field values.)