Files
vllm-omni/vllm_omni/model_executor/layers/cpu_ir_ops.py
T
tzhouamandClaude Opus 5 c4e61133fb [Edge][CPU] Fused norms off by default, and register the switches this branch added
Two corrections and the paperwork they imply.

The fused-RMSNorm registration defaulted to *on*, which contradicts the
measurement it was committed with: those kernels are 3.6x and 4.5x faster in
isolation but measured 72.7 tok/s against 74.0 end to end, because inductor
already fuses the native norm into the residual add and an opaque call breaks
that fusion. The implementations stay registered, so the provider remains
selectable through `kernel_config.ir_op_priority` on a machine where the trade
lands differently; only the default ranking is left alone now, and the test
asserts both states rather than just the opt-in one.

The four switches this branch added (VLLM_OMNI_AOT_CACHE_SHIM,
_CPU_FUSED_NORMS, _CPU_RELEASE_MEMORY, _SPARK_GATE_REDUCTION) were not in the
reviewed environment-variable inventory, so
`test_statically_resolvable_environment_accesses_are_classified` failed. They
are classified now, the reviewed count moves 28 -> 32 with a note saying why,
and each has a row in docs/configuration/environment_variables.md carrying the
measurement that justifies its default.

`VLLM_OMNI_CPU_FAST_SAMPLER` and `VLLM_OMNI_STEP_STATS_SYNC` remain
unclassified and that test still fails on them. They arrived in 6ce8de1b, in
files this branch does not touch, so they are left to whoever owns that work
rather than documented from a guess.

Under `-m "core_model and cpu"`: 2593 passed before these fixes with 9
failures, of which two were mine. The rest are CUDA-only tests in a CPU venv
("Torch not compiled with CUDA enabled") plus test_qnn_export.py, which
another session is actively editing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 14:55:24 +08:00

151 lines
5.7 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Give vLLM's CPU backend the fused RMSNorm kernels it already ships.
vLLM's CPU build registers `_C::rms_norm` and `_C::fused_add_rms_norm`, and
`vllm/kernels/vllm_c.py` already wraps both as IR-op implementations -- but it
gates them on ``GPGPU_DEVICE = is_cuda_alike() or is_xpu()``, so on CPU the IR
layer marks them unsupported and every norm falls through to the PyTorch
implementation. Nothing then fuses it: inductor emits an elementwise loop and
spreads a 4 KB reduction across every core, which at one token is almost
entirely thread barrier.
Measured on a Xeon 8480C at Spark-X2.5's decode shape (1 token, hidden 2048),
24 threads:
rms_norm native 20.99 us fused 5.76 us 3.6x
fused_add_rms_norm native 41.14 us fused 9.24 us 4.5x
Two norms per layer over 28 layers is ~1.8 ms of an ~12 ms decode step.
The same file's `gelu_and_mul` is deliberately *not* re-registered here: the
fused CPU kernel measured 64.11 us against the native path's 23.36 us at
Spark's GEGLU shape ([1, 13312] -> [1, 6656]), i.e. 2.7x slower, so the native
form is the right choice there and the asymmetry is the point -- "use the
fused kernel" is not a rule, it is a per-op measurement.
Registering under our own provider name rather than editing `vllm_c.py` keeps
this working across vLLM wheel upgrades; it is the mechanism vLLM documents for
out-of-tree kernels (see `vllm/kernels/oink_ops.py`). The equivalent upstream
change is two lines: add `is_cpu()` to `GPGPU_DEVICE` and give `CpuPlatform` a
`get_default_ir_op_priority`.
"""
import os
import torch
from torch import Tensor
from vllm import ir
from vllm.logger import init_logger
from vllm.platforms import current_platform
logger = init_logger(__name__)
PROVIDER = "vllm_c_cpu"
_FUSED_NORM_OPS = ("rms_norm", "fused_add_rms_norm")
def _cpu_kernels_available() -> bool:
if not current_platform.is_cpu():
return False
try:
current_platform.import_kernels()
return all(hasattr(torch.ops._C, name) for name in _FUSED_NORM_OPS)
except Exception as exc: # pragma: no cover - build without the extension
logger.debug("CPU fused norm kernels unavailable: %s", exc)
return False
CPU_FUSED_NORMS = _cpu_kernels_available()
def _supports(x: Tensor, weight, epsilon, variance_size=None) -> bool:
# The kernel normalizes over the last dim of a contiguous 2-D buffer and
# has no variance_size override; anything else stays on the native path.
return (
variance_size is None
and (weight is None or weight.dtype == x.dtype)
and x.is_contiguous()
)
def _supports_add(x: Tensor, x_residual: Tensor, weight, epsilon, variance_size=None):
return _supports(x, weight, epsilon, variance_size) and x_residual.is_contiguous()
@ir.ops.rms_norm.register_impl(
PROVIDER, supports_args=_supports, supported=CPU_FUSED_NORMS
)
def rms_norm(
x: Tensor, weight: Tensor | None, epsilon: float, variance_size: int | None = None
) -> Tensor:
assert variance_size is None
shape = x.shape
x2d = x if x.dim() == 2 else x.reshape(-1, shape[-1])
out = torch.empty_like(x2d)
torch.ops._C.rms_norm(out, x2d, weight, epsilon)
return out if x.dim() == 2 else out.reshape(shape)
@ir.ops.fused_add_rms_norm.register_impl(
PROVIDER, supports_args=_supports_add, supported=CPU_FUSED_NORMS, inplace=True
)
def fused_add_rms_norm(
x: Tensor,
x_residual: Tensor,
weight: Tensor | None,
epsilon: float,
variance_size: int | None = None,
) -> tuple[Tensor, Tensor]:
assert variance_size is None
if x.dim() == 2:
torch.ops._C.fused_add_rms_norm(x, x_residual, weight, epsilon)
return x, x_residual
shape = x.shape
x2d, r2d = x.reshape(-1, shape[-1]), x_residual.reshape(-1, shape[-1])
torch.ops._C.fused_add_rms_norm(x2d, r2d, weight, epsilon)
return x2d.reshape(shape), r2d.reshape(shape)
def prefer_cpu_fused_norms() -> None:
"""Rank these kernels ahead of the native path -- opt-in only.
The implementations are always registered (so they can be selected through
``kernel_config.ir_op_priority`` like any other provider); this function
only changes the *default* ranking, and it does nothing unless
``VLLM_OMNI_CPU_FUSED_NORMS=1``. The platform hook fills in ops the user
left unset, so an explicit priority still wins either way.
"""
if not CPU_FUSED_NORMS:
return
# Off by default, and the default is the measurement: in isolation these
# kernels are 3.6x and 4.5x faster than the native path, but in the
# compiled model they measured 72.7 tok/s against 74.0, because inductor
# was already fusing the native norm into the residual add and an opaque
# call breaks that fusion. Set VLLM_OMNI_CPU_FUSED_NORMS=1 to try them on
# a machine where that trade may land differently.
if os.environ.get("VLLM_OMNI_CPU_FUSED_NORMS", "0") != "1":
return
from vllm.platforms.cpu import CpuPlatform
if getattr(CpuPlatform, "_omni_fused_norm_priority", False):
return
base = CpuPlatform.get_default_ir_op_priority.__func__
def get_default_ir_op_priority(cls, vllm_config):
priority = base(cls, vllm_config)
for op_name in _FUSED_NORM_OPS:
current = list(getattr(priority, op_name))
if PROVIDER not in current:
setattr(priority, op_name, [PROVIDER] + current)
return priority
CpuPlatform.get_default_ir_op_priority = classmethod(get_default_ir_op_priority)
CpuPlatform._omni_fused_norm_priority = True
logger.info(
"CPU fused RMSNorm kernels enabled (provider %s) ahead of the native path.",
PROVIDER,
)