The Platform interface
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:
- Identity attributes. Values that describe the device: its name, its PyTorch device type, its dispatch key, its distributed backend, and more.
- 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_configis the most important one: it sets the worker class. - 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.
- 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#
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_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):
| Attribute | Default | CUDA | Used for |
|---|---|---|---|
_enum | none | PlatformEnum.CUDA | is_cuda(), is_out_of_tree() and the other checks (vllm/platforms/interface.py:196-222). An out-of-tree class uses PlatformEnum.OOT. |
device_name | none | "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_type | none | "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_var | a 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:
pre_register_and_update(parser)runs inAsyncEngineArgs.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 ofcreate_engine_config(vllm/engine/arg_utils.py:2246).apply_config_platform_defaults(vllm_config)runs inVllmConfig.__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).check_and_update_config(vllm_config)runs late inVllmConfig.__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 isworker_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 = Trueworker_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.
| Hook | Called from | Process | When in start-up | Default |
|---|---|---|---|---|
get_attn_backend_cls | vllm/v1/attention/selector.py:218 | worker | load_model: each attention layer asks in its __init__ (vllm/model_executor/layers/attention/attention.py:350) | "" (error) |
get_device_communicator_cls | vllm/distributed/parallel_state.py:549-552 | worker | init_device, for each group with more than one rank | DeviceCommunicatorBase (vllm/platforms/interface.py:1218-1221) |
get_punica_wrapper | vllm/lora/punica_wrapper/punica_selector.py:13-16 | worker | only 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_cls | vllm/compilation/backends.py:690-692 | worker | compilation: wraps each piecewise graph | AbstractStaticGraphWrapper (vllm/platforms/interface.py:1328-1331) |
get_pass_manager_cls, pass_key | vllm/compilation/backends.py:875-878 | worker | VllmBackend.__init__ (compilation) | PostGradPassManager (vllm/platforms/interface.py:246-251) |
get_compile_backend | vllm/config/compilation.py:1080-1081; vllm/compilation/backends.py:122-126 | front end (config); worker (compiler) | when backend is empty; when the backend is not inductor or eager | simple_compile_backend = "inductor" (vllm/platforms/interface.py:253-256) |
get_kv_cache_config_builder_cls | vllm/v1/core/kv_cache_config_builder.py:98-103 | engine core | KV 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_op | vllm/model_executor/layers/attention/attention.py:440 | worker | attention 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#
import_kernelsimports the compiled kernel libraries. The default importsvllm._Cand, if present,vllm._moe_C_stable_libtorch(vllm/platforms/interface.py:365-373). It runs when Python importsvllm._custom_ops, at module level (vllm/_custom_ops.py:21). CUDA imports its own set (vllm/platforms/cuda.py:238-250).register_triton_kernel_overrideslets a platform replace core Triton kernels. The worker calls it right after the general plugins, before it makes the worker class (vllm/v1/worker/worker_base.py:286-290). The default does nothing (vllm/platforms/interface.py:375-386). The CPU platform registers C++ versions, but only when Triton is missing (vllm/platforms/cpu.py:690-705).import_ir_kernelsimportsvllm.kernels, the IR op implementations. The docstring says that an out-of-tree platform should override it (vllm/platforms/interface.py:258-264). The kernel config calls it (vllm/config/kernel.py:54).
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- Lines 1303-1304. Dunder names raise
AttributeErrorat once. The comment says why: pickle looks for names such as__getstate__. - Lines 1306-1312. The method looks up
torch.cuda(for CUDA) and returns the attribute, if it is notNone. - 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 diveEvery 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.
| Hook | Called from | Process | When |
|---|---|---|---|
log_warnings | vllm/logger.py:295-303 | each process that configures logging | when 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:3257 | front end | while the CLI builds its parser, before it parses the flags |
pre_register_and_update() | vllm/engine/arg_utils.py:2246 | front end | start of create_engine_config, after the flags are parsed |
validate_environ | vllm/engine/arg_utils.py:2250 | front end | create_engine_config |
check_max_model_len | vllm/config/model.py:2653 | front end | while ModelConfig computes max_model_len |
verify_quantization | vllm/config/model.py:1470 | front end | while ModelConfig checks the quantization method |
apply_config_platform_defaults | vllm/config/vllm.py:1804 | front end | VllmConfig.__post_init__, before the compilation mode is set (lines 1806-1810) |
check_and_update_config | vllm/config/vllm.py:2046 | front end | late in VllmConfig.__post_init__ |
validate_request | vllm/v1/engine/input_processor.py:430 | front end | for each request |
register_triton_kernel_overrides | vllm/v1/worker/worker_base.py:290 | worker | init_worker |
get_device_communicator_cls | vllm/distributed/parallel_state.py:551 | worker | init_device |
verify_model_arch | vllm/model_executor/models/registry.py:1132 | where the model class is checked | model inspection |
get_attn_backend_cls, opaque_attention_op | vllm/v1/attention/selector.py:218, vllm/model_executor/layers/attention/attention.py:440 | worker | load_model |
update_block_size_for_backend | vllm/v1/executor/multiproc_executor.py:702, vllm/v1/executor/uniproc_executor.py:75 | worker (mp) or engine core (uni) | after load_model |
register_custom_kv_cache_specs | vllm/v1/core/single_type_kv_cache_manager.py:2719 | where KV cache specs are registered | KV cache spec registration |
get_kv_cache_config_builder_cls | vllm/v1/core/kv_cache_config_builder.py:101 | engine 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 diveCudaPlatform 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):
- Read the device capability, for example
(9, 0)on H100. - If the user selected a backend (
--attention-backend), validate only that one. An import error or an invalid configuration raisesValueError; there is no fallback (lines 430-449). - 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). - No valid backend:
ValueErrorwith every reason (lines 471-475). - Pick the valid backend with the smallest priority number (lines 479-485).
- If the user set
--block-sizeand that excluded a better backend, warn (lines 489-506). - Log the choice once and return the dotted class path (lines 508-518).
Other CUDA answers.
| Hook | CUDA answer | Lines |
|---|---|---|
check_and_update_config | worker_cls = vllm.v1.worker.gpu_worker.Worker; forces disable_chunked_mm_input for some multimodal models; a WSL warning | vllm/platforms/cuda.py:325-366 |
get_device_communicator_cls | CudaCommunicator | vllm/platforms/cuda.py:580-584 |
get_punica_wrapper | PunicaWrapperGPU | vllm/platforms/cuda.py:576-578 |
get_static_graph_wrapper_cls | CUDAGraphWrapper | vllm/platforms/cuda.py:598-600 |
opaque_attention_op | True | vllm/platforms/cuda.py:594-596 |
import_kernels | _C_stable_libtorch, optional _moe_C_stable_libtorch and _qutlass_C | vllm/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 diveThe 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:
_enum = PlatformEnum.OOTmakes an enabledCustomOpcallforward_oot(vllm/model_executor/custom_op.py:183,vllm/model_executor/custom_op.py:202-203).- If
worker_clsstays"auto", the worker passes"auto"toresolve_obj_by_qualname, which cannot split it at a dot (vllm/v1/worker/worker_base.py:293-296,vllm/utils/import_utils.py:121). Start-up fails in the worker. (Derived.) - The base
get_attn_backend_clsreturns"", and the selector raises "Invalid attention backend" (vllm/v1/attention/selector.py:223-226). - The base communicator class is generic. It is used only for groups with more than one rank (
vllm/distributed/parallel_state.py:549).
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 diveDocs and docstrings that do not match the code
- The docstring of
pre_register_and_updatesays that it is called "before global VllmConfig is initialized or cli arguments are parsed" (vllm/platforms/interface.py:644-645). This is true for the call inadd_cli_args. The second call, increate_engine_config, runs after the flags are parsed (vllm/engine/arg_utils.py:2246). - The design doc says that "triton ops" cannot use the custom way "now" (
docs/design/plugin_system.md:143-144). The code now hasregister_triton_kernel_overridesfor platforms that replace core Triton kernels (vllm/platforms/interface.py:375-386). - The design doc calls
check_and_update_config"very early" in the start-up (docs/design/plugin_system.md:105). In the code it runs late inVllmConfig.__post_init__, after the platform detection, the general plugins and bothpre_register_and_updatecalls (Step 2).
What can go wrong#
- A missing method returns
None.Platform.__getattr__logs "does not have ... attribute" once and returnsNone(vllm/platforms/interface.py:1314-1319). The caller then fails later with aTypeErrorif it calls the value. (Derived.) - LoRA on a new platform. The base
get_punica_wrapperraisesNotImplementedError(vllm/platforms/interface.py:1203-1206). Implement it, or do not use--enable-lora. - A selected backend is invalid. On CUDA,
--attention-backendwith an invalid backend raisesValueError; 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_backendraisesValueError(vllm/platforms/interface.py:740-747). - A quantization method is not supported. If the platform sets
supported_quantizationand the method is not in it,verify_quantizationraisesValueError(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#
- 1Which Platform hook sets the worker class, and in which process does it run?easy
Show answer
check_and_update_config. CUDA setsworker_clstovllm.v1.worker.gpu_worker.Workerwhen it is"auto"(vllm/platforms/cuda.py:330-331). It runs inVllmConfig.__post_init__(vllm/config/vllm.py:2046), in the front end forvllm serve. - 2Your platform class has noeasy
synchronizemethod, and itsdevice_typeis"cuda". What doescurrent_platform.synchronizereturn?Show answer
torch.cuda.synchronize.Platform.__getattr__looks up the name intorch.<device_type>(vllm/platforms/interface.py:1306-1312). - 3With TP = 1, which process callsmedium
get_attn_backend_cls, and isget_device_communicator_clscalled?Show answer
The engine core process: the
uniexecutor runs the worker there and loads the model (vllm/v1/executor/uniproc_executor.py:68-75).get_device_communicator_clsis not called, because the code asks for it only when the group has more than one rank (vllm/distributed/parallel_state.py:549-552). - 4Yourhard
check_and_update_configappends"+my_op"tocompilation_config.custom_opseach time it runs. Why can the list get the entry twice, and what is the fix?Show answer
check_and_update_configruns in everyVllmConfig.__post_init__(vllm/config/vllm.py:2046). vLLM'sreplace()makes a newVllmConfigfrom 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 oldcompilation_configobject, becausereplace()passes the same field values.)