vLLM Explained

CustomOp, out-of-tree ops and IR op providers

AdvancedvLLM conceptsabout 12 min read + 3 min deep dives

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

In one sentence

A CustomOp picks one forward method when vLLM builds it: forward_native when the op is disabled, else the method of the current platform.

The idea#

Some layers, such as RMSNorm, rotary embedding or the SiLU-and-multiply activation, have a fast kernel on each kind of hardware and a plain PyTorch version. vLLM writes such a layer as a CustomOp: an nn.Module subclass with methods forward_cuda, forward_hip, forward_cpu, forward_xpu, forward_tpu, forward_oot and forward_native. When the model builds the layer, the layer picks one method and keeps it.

The choice has two inputs. The first is a switch: is the op enabled? The list compilation_config.custom_ops decides. With torch.compile and Inductor, which is the default of vllm serve, ops are disabled by default. Then forward_native runs. For most ops this is plain PyTorch code, and Inductor generates fused kernels for it. The second input is the platform: an enabled op calls the method for current_platform.

An out-of-tree package can replace an op class with its own subclass through register_oot. vLLM then builds the replacement wherever model code builds the in-tree class. A PluggableLayer works the same way for a whole layer, such as a linear layer, but has no per-platform methods.

An everyday comparison. Think of a power tool with exchangeable heads. At assembly, the factory fits the head for the local power grid, or a manual hand crank when the "motor" switch is off. A vendor can ship a whole replacement tool with the same model name. The comparison stops here: the head is fixed at assembly. Changing the switch later does not change a tool that is already built.

The picture#

Figure: one CustomOp object from construction (panel 1) through the enabled check (panel 2) to the bound forward method (panel 3).
Open full size ↗

Read the panels as one call of the constructor. The decision happens once, in __init__. Each call of forward() then calls the bound method directly.

In the code#

Step 1: registering an op#

An in-tree op class gets a name with a decorator. The name goes into the op registry:

    # Decorator to register custom ops.
    @classmethod
    def register(
        cls,
        name: str,
        dynamic_arg_dims: dict[str, int | list[int]] | None = None,
    ):
        def decorator(op_cls):
            assert name not in op_registry, f"Duplicate op name: {name}"
            op_cls.name = name
            op_cls._dynamic_arg_dims = dynamic_arg_dims
            op_registry[name] = op_cls
            return op_cls

        return decorator
# --8<-- [start:rms_norm]
@CustomOp.register("rms_norm")
class RMSNorm(CustomOp):
    """Root mean square normalization.

    Computes x -> w * x / sqrt(E[x^2] + eps) where w is the learned weight.
    Refer to https://arxiv.org/abs/1910.07467
    """

    # --8<-- [end:rms_norm]
RMSNorm is the op rms_norm. The +rms_norm and -rms_norm switches use this name.

A duplicate name fails the assert (line 317). dynamic_arg_dims marks dimensions as dynamic when vLLM compiles forward_native alone (see the deep dive). At this commit 47 places in vllm/ use @CustomOp.register(...), outside the second registry of Step 6 (from git grep).

Step 2: the forward methods and their defaults#

    def __init__(self, *, enforce_enable: bool = False, compile_native: bool = False):
        super().__init__()
        self._enforce_enable = enforce_enable
        self._forward_method = self.dispatch_forward(compile_native=compile_native)

    def forward(self, *args, **kwargs):
        return self._forward_method(*args, **kwargs)

    def forward_native(self, *args, **kwargs):
        """PyTorch-native implementation of the forward method.
        This method is optional. If implemented, it can be used with compilers
        such as torch.compile or PyTorch XLA. Also, it can be used for testing
        purposes.
        """
        raise NotImplementedError

    def forward_cuda(self, *args, **kwargs):
        raise NotImplementedError

    def forward_hip(self, *args, **kwargs):
        # By default, we assume that HIP ops are compatible with CUDA ops.
        return self.forward_cuda(*args, **kwargs)

    def forward_xpu(self, *args, **kwargs):
        # By default, we assume that XPU ops are compatible with the
        # PyTorch-native implementation.
        return self.forward_native(*args, **kwargs)

    def forward_cpu(self, *args, **kwargs):
        # By default, we assume that CPU ops are compatible with the
        # PyTorch-native implementation.
        return self.forward_native(*args, **kwargs)

    def forward_tpu(self, *args, **kwargs):
        # By default, we assume that TPU ops are compatible with the
        # PyTorch-native implementation.
        # NOTE(woosuk): This is a placeholder for future extensions.
        return self.forward_native(*args, **kwargs)

    def forward_oot(self, *args, **kwargs):
        # By default, we assume that OOT ops are compatible with the
        # PyTorch-native implementation.
        return self.forward_native(*args, **kwargs)
  1. Line 131. __init__ picks the method once and keeps it in _forward_method.
  2. Lines 133-134. forward() only calls it.
  3. Lines 136-145. forward_native and forward_cuda raise NotImplementedError. A subclass must give them.
  4. Lines 147-149. forward_hip calls forward_cuda by default.
  5. Lines 151-170. forward_xpu, forward_cpu, forward_tpu and forward_oot call forward_native by default.

Step 3: the dispatch decision#

    def dispatch_forward(self, compile_native: bool):
        # NOTE(woosuk): Here we assume that vLLM was built for only one
        # specific backend. Currently, we do not support dynamic dispatching.
        compilation_config = get_cached_compilation_config()

        # NOTE(shen-shanshan): CustomOp object can be enforce enabled, e.g.,
        # enable device-specific kernels in ViT models when enabling graph
        # mode. By default, it will follow the compilation_config to determine
        # whether enable itself.
        # This enforce_enable mechanism will be removed after we adding a
        # separate compilation_config for multi-modal part.
        enabled = self._enforce_enable or self.enabled()
        if enabled:
            compilation_config.enabled_custom_ops.update([self.__class__.name])
        else:
            compilation_config.disabled_custom_ops.update([self.__class__.name])

        if not enabled:
            # Compile forward_native to avoid eager torch ops if inside
            # opaque torch custom op (e.g. fused_moe, unified_attention, etc.)
            return self.maybe_compile(self.forward_native, enable=compile_native)

        if current_platform.is_rocm():
            return self.forward_hip
        elif current_platform.is_cpu():
            return self.forward_cpu
        elif current_platform.is_tpu():
            return self.forward_tpu
        elif current_platform.is_xpu():
            return self.forward_xpu
        elif current_platform.is_out_of_tree():
            return self.forward_oot
        else:
            return self.forward_cuda
  1. Line 183. The op is enabled when enforce_enable=True was passed, or when enabled() says so.
  2. Lines 184-187. The op name goes into a counter of enabled or disabled ops. vLLM logs these counters at DEBUG level after it builds the model (vllm/config/compilation.py:1325-1330).
  3. Lines 189-192. Disabled: forward_native. maybe_compile wraps it in torch.compile only when the layer asked for compile_native=True (line 218).
  4. Lines 194-205. Enabled: one method per platform. The order matters: ROCm, CPU, TPU, XPU, then any out-of-tree platform, else CUDA.

The design doc gives forward_native as the "final fallback for all platforms" (docs/design/custom_op.md:21). In the code, the final else is forward_cuda (lines 204-205). forward_native is only the default body of four platform methods (Step 2).

So an out-of-tree platform reaches forward_oot only for an enabled op. is_out_of_tree() is true for a platform with _enum = PlatformEnum.OOT (vllm/platforms/interface.py:218-219, The Platform interface).

Step 4: enabled() and the custom_ops list#

    @classmethod
    def enabled(cls) -> bool:
        # if no name, then it was not registered
        compilation_config = get_cached_compilation_config()
        custom_ops = compilation_config.custom_ops
        if not hasattr(cls, "name"):
            logger.warning_once(
                "Custom op %s was not registered, which means it won't appear "
                "in the op registry. It will be enabled/disabled based on the "
                "global settings.",
                cls.__name__,
            )
            return CustomOp.default_on()

        enabled = f"+{cls.name}" in custom_ops
        disabled = f"-{cls.name}" in custom_ops
        if enabled and disabled:
            raise ValueError(
                "custom_ops cannot both enable and disable the same operation: "
                f"{cls.name}. Remove either the '+' or '-' directive"
            )

        return (CustomOp.default_on() or enabled) and not disabled

    @staticmethod
    def default_on() -> bool:
        """Behavior controlled by `CompilationConfig.custom_ops`: On by default if
        'all', off by default if 'none'.
        When PyTorch Inductor is used, 'none' is the default value,
        otherwise 'all'.
        """
        compilation_config = get_cached_compilation_config()
        count_none = compilation_config.custom_ops.count("none")
        count_all = compilation_config.custom_ops.count("all")
        if count_none + count_all != 1:
            raise ValueError(
                "custom_ops must contain exactly one base mode: 'all' or 'none'"
            )

        return not count_none > 0 or count_all > 0
  1. Lines 273-280. A class without a name (not registered) logs a WARNING once and follows the base mode.
  2. Lines 282-283. The switches are the strings "+" + name and "-" + name.
  3. Line 290. The result is (base mode on, or +name) and not -name.
  4. Lines 300-307. The list must hold exactly one of "all" and "none", else ValueError.

The user does not normally write the base mode. VllmConfig.__post_init__ adds it:

        if all(s not in self.compilation_config.custom_ops for s in ("all", "none")):
            if (
                self.compilation_config.backend == "inductor"
                and self.compilation_config.mode != CompilationMode.NONE
            ):
                self.compilation_config.custom_ops.append("none")
            else:
                self.compilation_config.custom_ops.append("all")

For vllm serve at the default level O2, the compilation mode is VLLM_COMPILE (vllm/config/vllm.py:1806-1810). The backend is the platform's simple_compile_backend, "inductor" by default (vllm/config/compilation.py:1080-1081, vllm/platforms/interface.py:172). So the base mode is "none". An op is then disabled unless one of three things turns it on:

With --enforce-eager, the mode is NONE (vllm/config/vllm.py:1745-1746), so the base mode is "all". Two more cases set the mode to NONE before line 1819: TORCH_COMPILE_DISABLE=1 (vllm/config/vllm.py:1765-1770) and breakable CUDA graphs (vllm/config/vllm.py:849-851). vLLM turns on breakable CUDA graphs by default for a list of architectures, for example DeepseekV4ForCausalLM (vllm/config/vllm.py:75-108, vllm/config/vllm.py:825-833). So these models get the base mode "all" by default. LlamaForCausalLM is not in the list.

Some code adds switches for you. A model with block-quantized weights gets +quant_fp8 (vllm/config/vllm.py:1799-1802). If you turn on one of three RoPE fusion passes in the compilation config, vLLM adds +rotary_embedding (vllm/config/compilation.py:964-984). A platform can change the list in its config hooks. ROCm adds some switches, for example +sparse_attn_indexer (vllm/platforms/rocm.py:1111-1112); the CUDA platform adds none (from git grep). The test platform replaces the list with ["all"] (tests/plugins/vllm_add_dummy_platform/vllm_add_dummy_platform/dummy_platform.py:21-23).

To change the list yourself, use the compilation config flag (vllm/engine/arg_utils.py:1881):

vllm serve Qwen/Qwen3-0.6B --compilation-config '{"custom_ops": ["none", "+rms_norm"]}'

Step 5: out-of-tree replacement with register_oot#

A plugin subclasses an in-tree op and decorates the subclass. vLLM's test platform does this for rotary embedding:

# Register CustomRotaryEmbedding to CustomOP.
@RotaryEmbedding.register_oot
class DummyRotaryEmbedding(RotaryEmbedding):
    """Original rotary positional embedding."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.addition_config = True

    def forward_oot(self, *args, **kwargs) -> tuple[torch.Tensor, torch.Tensor]:
        return super().forward_oot(*args, **kwargs)

The plugin imports this module from a general plugin, so the registration runs in each process (tests/plugins/vllm_add_dummy_platform/vllm_add_dummy_platform/__init__.py:12-13; entry point in tests/plugins/vllm_add_dummy_platform/setup.py:14-16). The decorator:

    @classmethod
    def register_oot(cls, _decorated_op_cls=None, name: str | None = None):
        def decorator(op_cls):
            reg_name = name if name is not None else cls.__name__
            assert reg_name not in op_registry_oot, f"Duplicate op name: {reg_name}"
            op_cls.name = reg_name
            op_registry_oot[reg_name] = op_cls
            return op_cls

        if _decorated_op_cls is None:
            # Called with parentheses: @CustomOP.register_oot()
            # or @CustomOP.register_oot(name="...")
            # So, _decorated_op_cls is None.
            # We return the actual decorator function.
            return decorator
        elif isinstance(_decorated_op_cls, type):  # Check if it's a class
            # Called without parentheses: @CustomOP.register_oot
            # The first argument is the class itself.
            # We call the 'decorator' function immediately with the class.
            return decorator(_decorated_op_cls)
        else:
            # Handle other unexpected cases if necessary
            raise TypeError("Decorator can only be applied to classes.")
  1. Line 337. The key is name if given, else the name of the class on which you call the decorator: RotaryEmbedding.register_oot uses "RotaryEmbedding".
  2. Lines 338-340. A second registration with the same key fails the assert. The OOT class gets name = key.
  3. Lines 343-353. Two forms work: @X.register_oot and @X.register_oot() or @CustomOp.register_oot(name="X").
  4. Lines 354-356. A string as the first argument is neither None nor a class, so register_oot("X") raises TypeError at once.

The swap happens in __new__, when model code builds the in-tree class:

    def __new__(cls, *args, **kwargs):
        try:
            op_name = cls.__name__
        except AttributeError:
            raise TypeError(
                f"Cannot instantiate '{cls.__name__}': its 'name' attribute "
                f"was not set, possibly because it was not decorated with "
                f"@CustomOp.register, or it's the CustomOp base class itself."
            ) from None

        if op_name not in op_registry_oot:
            op_cls_to_instantiate = cls
        else:
            op_cls_to_instantiate = op_registry_oot[op_name]
            logger.debug(
                "Instantiating custom op: %s using %s",
                op_name,
                str(op_cls_to_instantiate),
            )
        return super().__new__(op_cls_to_instantiate)

Line 109 reads cls.__name__, the Python class name, not the registered op name. Line 126 makes an object of the OOT class. The OOT class is a subclass of the in-tree class, so Python then calls its __init__. (General Python rule: __init__ runs only when __new__ returns an instance of the called class.) A test checks the swap (tests/plugins_tests/test_platform_plugins.py:51-64).

Step 6: PluggableLayer, and the second CustomOp registry#

PluggableLayer uses the same op_registry and op_registry_oot, and the same __new__ swap (vllm/model_executor/custom_op.py:46-65). Its docstring says that it has no per-platform forward_* methods; it allows the replacement of the whole layer, with its own sub-modules (vllm/model_executor/custom_op.py:33-44). The linear layers use it, for example column_parallel_linear (vllm/model_executor/layers/linear.py:434-435). LoRA code finds the replacement class with maybe_get_oot_by_class, so that it still wraps a swapped layer (vllm/model_executor/custom_op.py:25-29, vllm/lora/layers/column_parallel_linear.py:169).

vllm/model_executor/hw_agnostic/custom_op.py is a second copy of the module with its own op_registry and op_registry_oot (vllm/model_executor/hw_agnostic/custom_op.py:16-22). Its dispatch has only two methods: an enabled op uses forward_oot on an out-of-tree platform; all other cases use forward_native (vllm/model_executor/hw_agnostic/custom_op.py:163-170). Only two layers use it: RMSNorm and SiluAndMul in hw_agnostic/layers/. Only the Transformers modeling backend picks them, and only with VLLM_USE_HW_AGNOSTIC set to 1 or true (vllm/model_executor/models/transformers/layers.py:23-38, vllm/model_executor/models/transformers/layers.py:46-56, vllm/envs.py:1218-1220). An OOT class registered with the main CustomOp.register_oot does not replace these classes, because the registries are separate (derived).

Step 7: vllm.ir, a second level of selection#

The package vllm/ir defines IR ops: a function with a native PyTorch body, registered with @register_op. At this commit there are three: rms_norm, fused_add_rms_norm and gelu_and_mul_sparse (vllm/ir/ops/__init__.py:2-6). Each kernel provider adds an implementation with register_impl, for example vllm_c:

@ir.ops.rms_norm.register_impl(
    "vllm_c", supports_args=rms_no_var_size, supported=GPGPU_DEVICE
)
def rms_norm(
    x: Tensor, weight: Tensor | None, epsilon: float, variance_size: int | None = None
) -> Tensor:
supported is a static platform check; supports_args is checked per call (here: no variance size, matching dtypes).

A priority list per op selects the provider at call time:

    def dispatch(self, *args, **kwargs) -> "IrOpImpl":
        """Dispatch to the appropriate implementation based on current priority
        and argument support checks. Returns the selected IrOpImpl.

        THIS FUNCTION IS ON THE HOT PATH (OP DISPATCH), MUST BE FAST.
        """
        if not self._priority_impls:
            if not torch.compiler.is_compiling():
                # Logging not compatible with Dynamo tracing
                # (this code is exposed when torch wrapping is disabled)
                logger.warning_once(
                    "Priority not set for op %s, using native implementation.",
                    self.name,
                )
            return self.impls["native"]

        for impl in self._priority_impls:
            if not impl.supported:
                raise ValueError(
                    f"Implementation {impl.provider} for op {self.name} not supported. "
                    f"All implementations in priority list must be supported."
                )
            if impl.supports_args(*args, **kwargs):
                return impl

            if not torch.compiler.is_compiling():
                logger.debug(
                    "Skipping provider %s because it does not support "
                    "%s with args=%s kwargs=%s",
                    impl.provider,
                    self.name,
                    lazy(lambda: tensors_str_no_data(args)),
                    lazy(lambda: tensors_str_no_data(kwargs)),
                )

        raise RuntimeError(
            "Priority set incorrectly: the last implementation must "
            "support all args (can be native). This is likely an internal bug"
        )
  1. Lines 327-335. No priority set: WARNING once, and the native body runs.
  2. Lines 337-344. Walk the list. The first provider whose supports_args accepts the arguments wins. The ValueError for an unsupported provider is only a guard: when vLLM installs a list, it removes the providers that the platform does not support (vllm/ir/op.py:387-391).
  3. Lines 356-359. No provider accepts: RuntimeError. When vLLM installs a list, it adds native at the end if no provider supports all arguments (vllm/ir/op.py:395-406).

The lists live in IrOpPriorityConfig, one field per op (vllm/config/kernel.py:20-38); the flag is --ir-op-priority (vllm/engine/arg_utils.py:1827). The platform adds its defaults after any user list (vllm/config/kernel.py:458-475). On CUDA the default for rms_norm and fused_add_rms_norm is ["native"] with Inductor compilation, else ["vllm_c", "native"] (vllm/platforms/cuda.py:716-732). gelu_and_mul_sparse always gets ["triton", "native"] (line 733). Each worker installs the lists once at start-up (vllm/v1/worker/worker_base.py:96-101).

The two systems stack. RMSNorm is a CustomOp, and its forward_native calls the IR op (vllm/model_executor/layers/layernorm.py:79-99). Its forward_cuda calls forward_native unless batch invariance is on (vllm/model_executor/layers/layernorm.py:101-120). So for RMSNorm on CUDA, the IR priority, not the CustomOp switch, picks the kernel.

CustomOpIR op
Unitan nn.Module classa function
Choice byenabled switch + platformpriority list + per-call argument check
Whenonce, at layer constructionat each call (or at Inductor lowering)
Out-of-tree hookregister_oot (whole class)register_impl (one provider); Platform.import_ir_kernels imports it (vllm/platforms/interface.py:258-264)
Configcompilation_config.custom_opskernel_config.ir_op_priority

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

Worked example: RMSNorm with the default and with --enforce-eager

Default vllm serve of a Llama model on an NVIDIA GPU (O2).

  1. Mode VLLM_COMPILE, backend inductor: custom_ops = ["none"] (vllm.py lines 1819-1824).
  2. The model builds RMSNorm in the worker. enabled() finds no +rms_norm and returns false. The op is disabled: _forward_method = forward_native, not compiled alone (compile_native is false).
  3. forward_native calls ir.ops.rms_norm, or ir.ops.fused_add_rms_norm when the layer gets a residual. The CUDA priority is ["native"] (cuda.py line 720). With ir_enable_torch_wrap on (vllm/config/vllm.py:1813-1817), the call goes through the torch op vllm_ir.rms_norm, and the model-level torch.compile sees it.
  4. The field docstring says what follows: "Inductor generates (fused) Triton kernels for disabled custom ops" (vllm/config/compilation.py:489-492).

The same server with --enforce-eager.

  1. Mode NONE: custom_ops = ["all"]. RMSNorm is enabled: _forward_method = forward_cuda.
  2. forward_cuda calls forward_native (no batch invariance), which calls ir.ops.rms_norm (or fused_add_rms_norm).
  3. The priority is ["vllm_c", "native"]. For a BF16 input with BF16 weight and no variance size, vllm_c accepts the arguments, and the C++ kernel runs (vllm/kernels/vllm_c.py:17-25).

(Derived by reading the lines cited on this page in order.)

Deep dive

When vLLM builds the op, and what it needs

  • A current config. dispatch_forward and enabled() read get_cached_compilation_config(), which needs a config set with set_current_vllm_config (vllm/config/vllm.py:3570-3586). initialize_model sets it around the model constructor (vllm/model_executor/model_loader/utils.py:100-101). A CustomOp built when no config is set, for example at module import time, raises AssertionError "Current vLLM config is not set".
  • The process. The model is built in each worker (with TP = 1, in the engine core process). So register_oot must run in the worker before the model is built. A general plugin does this; the worker loads them in init_worker (How vLLM finds and loads plugins).
  • After the build. When the context ends, custom_op_log_check logs the enabled and disabled ops and warns about a +name or -name that matched no op of the model (vllm/config/vllm.py:3544-3545, vllm/config/compilation.py:1333-1361).
Deep dive

The name of a registered OOT class

register_oot sets op_cls.name to the key (line 339), and the key is the in-tree class name by default. enabled() builds the switch from cls.name (line 282). So for the test class DummyRotaryEmbedding, the switch is +RotaryEmbedding, not +rotary_embedding, the name that the in-tree base class registered (vllm/model_executor/layers/rotary_embedding/base.py:14-15). (Derived from lines 282 and 337-339; no test covers it.)

The test platform turns on all ops with custom_ops = ["all"]. Then the name does not matter. Your platform can keep the default "none" base mode. Then your forward_oot does not run for a disabled op, because a disabled op uses forward_native (line 189).

Deep dive

maybe_compile and dynamic_arg_dims

Some layers call an op inside an opaque torch custom op (for example fused MoE), where the model-level torch.compile cannot see it. For them, compile_native=True compiles forward_native alone (vllm/model_executor/custom_op.py:207-266):

  1. No compile when enable is false, the mode is NONE, or the backend is "eager" (lines 218-228).
  2. The backend is current_platform.simple_compile_backend (line 233).
  3. With dynamic_arg_dims from register, vLLM compiles with dynamic=False and marks the listed dimensions as dynamic at each call (lines 235-258). A negative index counts from the end (line 254).
  4. Else it compiles with dynamic=True "to avoid recompilations" (lines 260-266).
Deep dive

Validation errors of custom_ops

InputResultWhere
"rms_norm" (no sign)ValueError "Invalid syntax"vllm/config/compilation.py:1013-1019
["all", "none"]ValueError "only one base mode"vllm/config/compilation.py:1021-1025
["+x", "-x"]ValueErrorvllm/config/compilation.py:1027-1034
+x for an op not in the modelWARNING "has no effect"vllm/config/compilation.py:1343-1361
duplicate register nameAssertionErrorvllm/model_executor/custom_op.py:317
duplicate register_oot keyAssertionErrorvllm/model_executor/custom_op.py:338
register_oot("X")TypeErrorvllm/model_executor/custom_op.py:354-356

What can go wrong#

  • The docs form of register_oot. register_oot("Name") raises TypeError (Step 5).
  • The registration runs too late. __new__ reads op_registry_oot when the layer is built. A class registered after the model build has no effect on existing layers (derived from lines 117-126).
  • Two plugins replace the same op. The second register_oot fails the assert at line 338 and stops the plugin function.
  • A switch with the wrong name. +RMSNorm instead of +rms_norm matches nothing; the only sign is the WARNING of custom_op_log_check.

Why it matters. CustomOp and IR ops decide which kernel runs for the small layers of every model. A hardware plugin uses them to bring its own kernels without a change to the model files.

Check yourself#

  1. 1
    When does a CustomOp choose its forward method?
    easy
    Show answer

    Once, in __init__, through dispatch_forward; forward() then calls the stored method (vllm/model_executor/custom_op.py:128-134).

  2. 2
    With default vllm serve on CUDA, is RMSNorm enabled? Why?
    medium
    Show answer

    No. The mode is VLLM_COMPILE and the backend is Inductor, so vLLM adds the base mode "none" (vllm/config/vllm.py:1819-1826). The CUDA platform adds no +rms_norm, and RMSNorm does not use enforce_enable. So enabled() returns false (vllm/model_executor/custom_op.py:290), and the op uses forward_native.

  3. 3
    Why does @CustomOp.register_oot("MMEncoderAttention") fail, and what works instead?
    medium
    Show answer

    The string lands in the positional parameter _decorated_op_cls. It is neither None nor a class, so the method raises TypeError (vllm/model_executor/custom_op.py:343-356). Use @MMEncoderAttention.register_oot or @CustomOp.register_oot(name="MMEncoderAttention").

  4. 4
    With --enforce-eager on CUDA, which code computes RMSNorm for a BF16 input, and which two settings decide it?
    hard
    Show answer

    The vllm_c provider of the IR op rms_norm (or fused_add_rms_norm with a residual). First, the base mode is "all", so the CustomOp is enabled and uses forward_cuda, which calls forward_native and the IR op (vllm/model_executor/layers/layernorm.py:101-120). Second, the CUDA IR priority without Inductor is ["vllm_c", "native"] (vllm/platforms/cuda.py:716-720), and vllm_c accepts these arguments (vllm/kernels/vllm_c.py:17-25).