CustomOp, out-of-tree ops and IR op providers
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#
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]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)- Line 131.
__init__picks the method once and keeps it in_forward_method. - Lines 133-134.
forward()only calls it. - Lines 136-145.
forward_nativeandforward_cudaraiseNotImplementedError. A subclass must give them. - Lines 147-149.
forward_hipcallsforward_cudaby default. - Lines 151-170.
forward_xpu,forward_cpu,forward_tpuandforward_ootcallforward_nativeby 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- Line 183. The op is enabled when
enforce_enable=Truewas passed, or whenenabled()says so. - 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). - Lines 189-192. Disabled:
forward_native.maybe_compilewraps it intorch.compileonly when the layer asked forcompile_native=True(line 218). - 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- Lines 273-280. A class without a
name(not registered) logs a WARNING once and follows the base mode. - Lines 282-283. The switches are the strings
"+" + nameand"-" + name. - Line 290. The result is (base mode on, or
+name) and not-name. - Lines 300-307. The list must hold exactly one of
"all"and"none", elseValueError.
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:
- a
+nameentry in the list (line 290); enforce_enable=Truefrom the layer's constructor (line 183). The vision encoders of some multimodal models buildApplyRotaryEmbthis way (vllm/model_executor/models/qwen2_5_vl.py:404);- a class that overrides
enabled().MMEncoderAttention.enabled()always returns true (vllm/model_executor/layers/attention/mm_encoder_attention.py:507-509).
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.")- Line 337. The key is
nameif given, else the name of the class on which you call the decorator:RotaryEmbedding.register_ootuses"RotaryEmbedding". - Lines 338-340. A second registration with the same key fails the
assert. The OOT class getsname = key. - Lines 343-353. Two forms work:
@X.register_ootand@X.register_oot()or@CustomOp.register_oot(name="X"). - Lines 354-356. A string as the first argument is neither
Nonenor a class, soregister_oot("X")raisesTypeErrorat 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"
)- Lines 327-335. No priority set: WARNING once, and the native body runs.
- Lines 337-344. Walk the list. The first provider whose
supports_argsaccepts the arguments wins. TheValueErrorfor 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). - Lines 356-359. No provider accepts:
RuntimeError. When vLLM installs a list, it addsnativeat 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.
| CustomOp | IR op | |
|---|---|---|
| Unit | an nn.Module class | a function |
| Choice by | enabled switch + platform | priority list + per-call argument check |
| When | once, at layer construction | at each call (or at Inductor lowering) |
| Out-of-tree hook | register_oot (whole class) | register_impl (one provider); Platform.import_ir_kernels imports it (vllm/platforms/interface.py:258-264) |
| Config | compilation_config.custom_ops | kernel_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 diveWorked example: RMSNorm with the default and with --enforce-eager
Default vllm serve of a Llama model on an NVIDIA GPU (O2).
- Mode
VLLM_COMPILE, backendinductor:custom_ops = ["none"](vllm.py lines 1819-1824). - The model builds
RMSNormin the worker.enabled()finds no+rms_normand returns false. The op is disabled:_forward_method = forward_native, not compiled alone (compile_nativeis false). forward_nativecallsir.ops.rms_norm, orir.ops.fused_add_rms_normwhen the layer gets a residual. The CUDA priority is["native"](cuda.py line 720). Withir_enable_torch_wrapon (vllm/config/vllm.py:1813-1817), the call goes through the torch opvllm_ir.rms_norm, and the model-leveltorch.compilesees it.- 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.
- Mode
NONE:custom_ops = ["all"].RMSNormis enabled:_forward_method = forward_cuda. forward_cudacallsforward_native(no batch invariance), which callsir.ops.rms_norm(orfused_add_rms_norm).- The priority is
["vllm_c", "native"]. For a BF16 input with BF16 weight and no variance size,vllm_caccepts 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 diveWhen vLLM builds the op, and what it needs
- A current config.
dispatch_forwardandenabled()readget_cached_compilation_config(), which needs a config set withset_current_vllm_config(vllm/config/vllm.py:3570-3586).initialize_modelsets 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, raisesAssertionError"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_ootmust run in the worker before the model is built. A general plugin does this; the worker loads them ininit_worker(How vLLM finds and loads plugins). - After the build. When the context ends,
custom_op_log_checklogs the enabled and disabled ops and warns about a+nameor-namethat matched no op of the model (vllm/config/vllm.py:3544-3545,vllm/config/compilation.py:1333-1361).
Deep diveThe 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 divemaybe_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):
- No compile when
enableis false, the mode isNONE, or the backend is"eager"(lines 218-228). - The backend is
current_platform.simple_compile_backend(line 233). - With
dynamic_arg_dimsfromregister, vLLM compiles withdynamic=Falseand marks the listed dimensions as dynamic at each call (lines 235-258). A negative index counts from the end (line 254). - Else it compiles with
dynamic=True"to avoid recompilations" (lines 260-266).
Deep diveValidation errors of custom_ops
| Input | Result | Where |
|---|---|---|
"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"] | ValueError | vllm/config/compilation.py:1027-1034 |
+x for an op not in the model | WARNING "has no effect" | vllm/config/compilation.py:1343-1361 |
duplicate register name | AssertionError | vllm/model_executor/custom_op.py:317 |
duplicate register_oot key | AssertionError | vllm/model_executor/custom_op.py:338 |
register_oot("X") | TypeError | vllm/model_executor/custom_op.py:354-356 |
What can go wrong#
- The docs form of
register_oot.register_oot("Name")raisesTypeError(Step 5). - The registration runs too late.
__new__readsop_registry_ootwhen 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_ootfails theassertat line 338 and stops the plugin function. - A switch with the wrong name.
+RMSNorminstead of+rms_normmatches nothing; the only sign is the WARNING ofcustom_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#
- 1When does a CustomOp choose its forward method?easy
Show answer
Once, in
__init__, throughdispatch_forward;forward()then calls the stored method (vllm/model_executor/custom_op.py:128-134). - 2With defaultmedium
vllm serveon CUDA, isRMSNormenabled? Why?Show answer
No. The mode is
VLLM_COMPILEand the backend is Inductor, so vLLM adds the base mode"none"(vllm/config/vllm.py:1819-1826). The CUDA platform adds no+rms_norm, andRMSNormdoes not useenforce_enable. Soenabled()returns false (vllm/model_executor/custom_op.py:290), and the op usesforward_native. - 3Why doesmedium
@CustomOp.register_oot("MMEncoderAttention")fail, and what works instead?Show answer
The string lands in the positional parameter
_decorated_op_cls. It is neitherNonenor a class, so the method raisesTypeError(vllm/model_executor/custom_op.py:343-356). Use@MMEncoderAttention.register_ootor@CustomOp.register_oot(name="MMEncoderAttention"). - 4Withhard
--enforce-eageron CUDA, which code computes RMSNorm for a BF16 input, and which two settings decide it?Show answer
The
vllm_cprovider of the IR oprms_norm(orfused_add_rms_normwith a residual). First, the base mode is"all", so the CustomOp is enabled and usesforward_cuda, which callsforward_nativeand 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), andvllm_caccepts these arguments (vllm/kernels/vllm_c.py:17-25).