387 lines
16 KiB
Python
387 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
import pytest
|
|
import torch
|
|
|
|
# This registers op implementations
|
|
import vllm.kernels # noqa: F401
|
|
from tests.ir.ir_test_utils import (
|
|
COMMON_HIDDEN_SIZES,
|
|
NUM_TOKENS,
|
|
assert_close,
|
|
clone_args,
|
|
supported_providers,
|
|
)
|
|
from tests.kernels.allclose_default import get_default_rtol
|
|
from vllm import ir
|
|
from vllm.platforms import current_platform
|
|
|
|
rms_norm_native = ir.ops.rms_norm.impls["native"].impl_fn
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_cuda_alike() and not current_platform.is_xpu(),
|
|
reason="Currently only kernels on CUDA, ROCm and XPU",
|
|
)
|
|
def test_rms_norm_registration():
|
|
expected = {
|
|
"native": True,
|
|
"vllm_c": current_platform.is_cuda_alike(),
|
|
"aiter": current_platform.is_rocm(),
|
|
"oink": current_platform.has_device_capability(100)
|
|
and hasattr(torch.ops, "oink")
|
|
and hasattr(torch.ops.oink, "rmsnorm"),
|
|
"xpu_kernels": current_platform.is_xpu(),
|
|
}
|
|
|
|
actual = {
|
|
provider: impl.supported for provider, impl in ir.ops.rms_norm.impls.items()
|
|
}
|
|
|
|
assert actual == expected
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
|
|
@pytest.mark.parametrize("n_tokens", NUM_TOKENS)
|
|
@pytest.mark.parametrize("hidden_size", COMMON_HIDDEN_SIZES)
|
|
@pytest.mark.parametrize("epsilon", [1e-6, 1e-5])
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_cuda_alike() and not current_platform.is_xpu(),
|
|
reason="Currently only kernels on CUDA, ROCm and XPU",
|
|
)
|
|
class TestRMSNorm:
|
|
@classmethod
|
|
def setup_class(cls, **kwargs):
|
|
torch.set_default_device(current_platform.device_type)
|
|
|
|
def test_native_semantics(self, dtype, n_tokens, hidden_size, epsilon):
|
|
x, weight, epsilon = ir.ops.rms_norm.generate_inputs(
|
|
num_tokens=4, hidden_size=8, dtype=dtype, epsilon=epsilon
|
|
)
|
|
out = rms_norm_native(x, weight, epsilon=epsilon)
|
|
|
|
# Check shape, dtype, device
|
|
assert out.shape == x.shape
|
|
assert out.dtype == x.dtype
|
|
assert out.device == x.device
|
|
|
|
# Check the scaling property of rms norm
|
|
out2 = rms_norm_native(x * 2.0, weight, epsilon=epsilon)
|
|
torch.testing.assert_close(out2, out, rtol=get_default_rtol(out), atol=1e-3)
|
|
|
|
# Mean square should be approximately 1 (ignoring epsilon and weight scaling)
|
|
combined_norm = out.float() / weight.float()
|
|
variance = combined_norm.pow(2).mean(dim=-1)
|
|
# After RMS normalization, variance should be close to 1
|
|
torch.testing.assert_close(
|
|
variance, torch.ones_like(variance), rtol=1e-2, atol=1e-2
|
|
)
|
|
|
|
# Check behavior with and without weight
|
|
weight1 = torch.ones_like(weight)
|
|
out3 = rms_norm_native(x, weight1, epsilon=epsilon)
|
|
out4 = rms_norm_native(x, None, epsilon=epsilon)
|
|
torch.testing.assert_close(out3, out4)
|
|
|
|
@pytest.mark.parametrize("provider", supported_providers(ir.ops.rms_norm))
|
|
def test_impls(self, dtype, n_tokens, hidden_size, epsilon, provider):
|
|
impl = ir.ops.rms_norm.impls[provider]
|
|
x, weight, eps = ir.ops.rms_norm.generate_inputs(
|
|
num_tokens=n_tokens, hidden_size=hidden_size, dtype=dtype, epsilon=epsilon
|
|
)
|
|
args = (x, weight, eps)
|
|
|
|
if not impl.supports_args(*args):
|
|
pytest.skip(f"{provider} does not support args")
|
|
|
|
ref_output = rms_norm_native(*clone_args(args))
|
|
output = impl.impl_fn(*clone_args(args))
|
|
assert_close(ir.ops.rms_norm, output, ref_output)
|
|
|
|
# check that dispatched call matches direct call
|
|
with ir.ops.rms_norm.set_priority([provider, "native"]):
|
|
out_dispatched = ir.ops.rms_norm(*args)
|
|
out_direct = impl.impl_fn(*args)
|
|
torch.testing.assert_close(out_dispatched, out_direct, rtol=0.0, atol=0.0)
|
|
|
|
# none of these support variance_size override
|
|
assert not impl.supports_args(x, weight, eps, 4)
|
|
assert not impl.supports_args(x, weight, eps, variance_size=4)
|
|
|
|
# test weight=None behavior
|
|
out_no_weight = impl.impl_fn(x, None, eps)
|
|
out_unit_weight = impl.impl_fn(x, torch.ones_like(weight), eps)
|
|
assert_close(ir.ops.rms_norm, out_no_weight, out_unit_weight)
|
|
|
|
@pytest.mark.parametrize("provider", ["vllm_c", "aiter", "xpu_kernels", "native"])
|
|
def test_torch_opcheck(self, dtype, n_tokens, hidden_size, epsilon, provider):
|
|
if not ir.ops.rms_norm.impls[provider].supported:
|
|
pytest.skip(f"{provider} impl not supported on this platform")
|
|
|
|
args = ir.ops.rms_norm.generate_inputs(
|
|
num_tokens=n_tokens, hidden_size=hidden_size, dtype=dtype, epsilon=epsilon
|
|
)
|
|
|
|
# When checking the torch op, we have to set priority and use dispatch
|
|
with ir.ops.rms_norm.set_priority([provider, "native"]):
|
|
torch.library.opcheck(torch.ops.vllm_ir.rms_norm, args)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_rocm(),
|
|
reason="aiter is only supported on ROCm",
|
|
)
|
|
def test_aiter_rejects_unsupported_dtypes():
|
|
torch.set_default_device(current_platform.device_type)
|
|
impl = ir.ops.rms_norm.impls["aiter"]
|
|
for dtype in [torch.float32, torch.float64]:
|
|
args = ir.ops.rms_norm.generate_inputs(
|
|
num_tokens=8, hidden_size=4096, dtype=dtype, epsilon=1e-5
|
|
)
|
|
assert not impl.supports_args(*args), f"aiter should reject dtype={dtype}"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_rocm(),
|
|
reason="ROCm vllm_c RMSNorm needs explicit ND input handling",
|
|
)
|
|
def test_vllm_c_rms_norm_accepts_nd_input():
|
|
torch.set_default_device(current_platform.device_type)
|
|
impl = ir.ops.rms_norm.impls["vllm_c"]
|
|
if not impl.supported:
|
|
pytest.skip("vllm_c impl not supported on this platform")
|
|
|
|
base = torch.randn(3, 8, 192, dtype=torch.float16)
|
|
x = base.split(64, dim=-1)[0].view(3, 8, 4, 16)
|
|
assert not x.is_contiguous()
|
|
weight = torch.randn(16, dtype=torch.float16)
|
|
epsilon = 1e-5
|
|
|
|
output = impl.impl_fn(x, weight, epsilon)
|
|
ref_output = rms_norm_native(x, weight, epsilon)
|
|
|
|
assert output.shape == x.shape
|
|
assert_close(ir.ops.rms_norm, output, ref_output)
|
|
|
|
|
|
fused_add_rms_norm_native = ir.ops.fused_add_rms_norm.impls["native"].impl_fn
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_cuda_alike() and not current_platform.is_xpu(),
|
|
reason="Currently only kernels on CUDA, ROCm and XPU",
|
|
)
|
|
def test_fused_add_rms_norm_registration():
|
|
expected = {
|
|
"native": True,
|
|
"vllm_c": current_platform.is_cuda_alike(),
|
|
"aiter": current_platform.is_rocm(),
|
|
"oink": current_platform.has_device_capability(100)
|
|
and hasattr(torch.ops, "oink")
|
|
and hasattr(torch.ops.oink, "fused_add_rms_norm"),
|
|
"xpu_kernels": current_platform.is_xpu(),
|
|
}
|
|
|
|
actual = {
|
|
provider: impl.supported
|
|
for provider, impl in ir.ops.fused_add_rms_norm.impls.items()
|
|
}
|
|
|
|
assert actual == expected
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_rocm(),
|
|
reason="ROCm vllm_c fused_add_rms_norm needs explicit ND input handling",
|
|
)
|
|
def test_vllm_c_fused_add_rms_norm_accepts_nd_input():
|
|
torch.set_default_device(current_platform.device_type)
|
|
impl = ir.ops.fused_add_rms_norm.impls["vllm_c"]
|
|
if not impl.supported:
|
|
pytest.skip("vllm_c impl not supported on this platform")
|
|
|
|
base = torch.randn(3, 8, 192, dtype=torch.float16)
|
|
residual_base = torch.randn(3, 8, 192, dtype=torch.float16)
|
|
x = base.split(64, dim=-1)[0].view(3, 8, 4, 16)
|
|
x_residual = residual_base.split(64, dim=-1)[0].view(3, 8, 4, 16)
|
|
assert not x.is_contiguous()
|
|
assert not x_residual.is_contiguous()
|
|
weight = torch.randn(16, dtype=torch.float16)
|
|
epsilon = 1e-5
|
|
|
|
output, residual = impl.impl_fn(x.clone(), x_residual.clone(), weight, epsilon)
|
|
ref_output, ref_residual = fused_add_rms_norm_native(x, x_residual, weight, epsilon)
|
|
|
|
assert output.shape == x.shape
|
|
assert residual.shape == x_residual.shape
|
|
assert_close(ir.ops.fused_add_rms_norm, output, ref_output)
|
|
assert_close(ir.ops.fused_add_rms_norm, residual, ref_residual)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
|
|
@pytest.mark.parametrize("n_tokens", NUM_TOKENS)
|
|
@pytest.mark.parametrize("hidden_size", COMMON_HIDDEN_SIZES)
|
|
@pytest.mark.parametrize("epsilon", [1e-6, 1e-5])
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_cuda_alike() and not current_platform.is_xpu(),
|
|
reason="Currently only kernels on CUDA, ROCm and XPU",
|
|
)
|
|
class TestFusedAddRMSNorm:
|
|
@classmethod
|
|
def setup_class(cls, **kwargs):
|
|
torch.set_default_device(current_platform.device_type)
|
|
|
|
def test_native_semantics(self, dtype, n_tokens, hidden_size, epsilon):
|
|
x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
|
|
num_tokens=4, hidden_size=8, dtype=dtype, epsilon=epsilon
|
|
)
|
|
out, residual_out = fused_add_rms_norm_native(x, x_residual, weight, eps)
|
|
|
|
# Check shape, dtype, device
|
|
assert out.shape == x.shape
|
|
assert out.dtype == x.dtype
|
|
assert out.device == x.device
|
|
assert residual_out.shape == x_residual.shape
|
|
assert residual_out.dtype == x_residual.dtype
|
|
assert residual_out.device == x_residual.device
|
|
|
|
# Check that residual_out = x + x_residual
|
|
expected_residual = (x.float() + x_residual.float()).to(dtype)
|
|
torch.testing.assert_close(
|
|
residual_out, expected_residual, rtol=1e-3, atol=1e-3
|
|
)
|
|
|
|
# Verify that the output is RMS normalized version of (x + x_residual)
|
|
expected_out = rms_norm_native(expected_residual, weight, epsilon)
|
|
assert_close(
|
|
ir.ops.fused_add_rms_norm,
|
|
(out, residual_out),
|
|
(expected_out, expected_residual),
|
|
)
|
|
|
|
# Check the scaling property of rms norm
|
|
out1, _ = fused_add_rms_norm_native(
|
|
x, torch.zeros_like(x), weight, epsilon=epsilon
|
|
)
|
|
out2, _ = fused_add_rms_norm_native(
|
|
x * 2.0, torch.zeros_like(x), weight, epsilon=epsilon
|
|
)
|
|
torch.testing.assert_close(out2, out1, rtol=get_default_rtol(out), atol=1e-3)
|
|
|
|
# Check behavior with and without weight
|
|
weight1 = torch.ones_like(weight)
|
|
out3, _ = fused_add_rms_norm_native(x, x_residual, weight1, eps)
|
|
out4, _ = fused_add_rms_norm_native(x, x_residual, None, eps)
|
|
torch.testing.assert_close(out3, out4)
|
|
|
|
@pytest.mark.parametrize("provider", supported_providers(ir.ops.fused_add_rms_norm))
|
|
def test_impls(self, dtype, n_tokens, hidden_size, epsilon, provider):
|
|
impl = ir.ops.fused_add_rms_norm.impls[provider]
|
|
x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
|
|
num_tokens=n_tokens, hidden_size=hidden_size, dtype=dtype, epsilon=epsilon
|
|
)
|
|
args = (x, x_residual, weight, eps, None)
|
|
|
|
if not impl.supports_args(*args):
|
|
pytest.skip(f"{provider} does not support args")
|
|
|
|
ref_output, ref_residual = fused_add_rms_norm_native(*clone_args(args))
|
|
output, residual = impl.impl_fn(*clone_args(args))
|
|
assert_close(ir.ops.fused_add_rms_norm, output, ref_output)
|
|
assert_close(ir.ops.fused_add_rms_norm, residual, ref_residual)
|
|
|
|
# check that dispatched call matches direct call
|
|
with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
|
|
out_dispatched, residual_dispatched = ir.ops.fused_add_rms_norm(*args[:4])
|
|
out_direct, residual_direct = impl.impl_fn(*clone_args(args))
|
|
torch.testing.assert_close(out_dispatched, out_direct, rtol=0.0, atol=0.0)
|
|
torch.testing.assert_close(
|
|
residual_dispatched, residual_direct, rtol=0.0, atol=0.0
|
|
)
|
|
|
|
# none of these support variance_size override
|
|
assert not impl.supports_args(x, x_residual, weight, epsilon, 4)
|
|
assert not impl.supports_args(x, x_residual, weight, epsilon, variance_size=4)
|
|
|
|
# test weight=None behavior
|
|
out_no_weight, residual_no_weight = impl.impl_fn(
|
|
x.clone(), x_residual.clone(), None, epsilon
|
|
)
|
|
out_unit_weight, residual_unit_weight = impl.impl_fn(
|
|
x.clone(), x_residual.clone(), torch.ones_like(weight), epsilon
|
|
)
|
|
assert_close(ir.ops.fused_add_rms_norm, out_no_weight, out_unit_weight)
|
|
assert_close(
|
|
ir.ops.fused_add_rms_norm, residual_no_weight, residual_unit_weight
|
|
)
|
|
|
|
@pytest.mark.parametrize("provider", ["vllm_c"])
|
|
def test_inplace_semantics(self, dtype, n_tokens, hidden_size, epsilon, provider):
|
|
"""Test that inplace implementations reuse inputs,
|
|
for maybe_inplace overload but not for default overload."""
|
|
impl = ir.ops.fused_add_rms_norm.impls[provider]
|
|
if not impl.supported:
|
|
pytest.skip(f"{provider} impl not supported on this platform")
|
|
|
|
x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
|
|
num_tokens=n_tokens, hidden_size=hidden_size, dtype=dtype, epsilon=epsilon
|
|
)
|
|
|
|
# Test default overload - should NOT modify inputs even with inplace impl
|
|
x_default = x.clone()
|
|
x_residual_default = x_residual.clone()
|
|
x_default_ptr = x_default.data_ptr()
|
|
x_residual_default_ptr = x_residual_default.data_ptr()
|
|
|
|
with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
|
|
out_default, residual_default = ir.ops.fused_add_rms_norm(
|
|
x_default, x_residual_default, weight, eps
|
|
)
|
|
|
|
# Default should NOT be inplace (even with inplace implementation)
|
|
assert out_default.data_ptr() != x_default_ptr
|
|
assert residual_default.data_ptr() != x_residual_default_ptr
|
|
torch.testing.assert_close(x, x_default, rtol=0.0, atol=0.0)
|
|
torch.testing.assert_close(x_residual, x_residual_default, rtol=0.0, atol=0.0)
|
|
|
|
# Test maybe_inplace overload - should modify inputs with inplace impl
|
|
x_inplace = x.clone()
|
|
x_residual_inplace = x_residual.clone()
|
|
x_inplace_ptr = x_inplace.data_ptr()
|
|
x_residual_inplace_ptr = x_residual_inplace.data_ptr()
|
|
|
|
with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
|
|
out_inplace, residual_inplace = ir.ops.fused_add_rms_norm.maybe_inplace(
|
|
x_inplace, x_residual_inplace, weight, eps
|
|
)
|
|
|
|
# maybe_inplace should be inplace
|
|
assert out_inplace.data_ptr() == x_inplace_ptr
|
|
assert residual_inplace.data_ptr() == x_residual_inplace_ptr
|
|
|
|
# Both should produce same results
|
|
torch.testing.assert_close(out_default, out_inplace, atol=0.0, rtol=0.0)
|
|
torch.testing.assert_close(
|
|
residual_default, residual_inplace, atol=0.0, rtol=0.0
|
|
)
|
|
|
|
@pytest.mark.parametrize("provider", supported_providers(ir.ops.fused_add_rms_norm))
|
|
def test_torch_opcheck(self, dtype, n_tokens, hidden_size, epsilon, provider):
|
|
args = ir.ops.fused_add_rms_norm.generate_inputs(
|
|
num_tokens=n_tokens, hidden_size=hidden_size, dtype=dtype, epsilon=epsilon
|
|
)
|
|
args = args + (None,) # Add variance_size parameter
|
|
|
|
# When checking the torch op, we have to set priority and use dispatch
|
|
with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
|
|
torch.library.opcheck(torch.ops.vllm_ir.fused_add_rms_norm.default, args)
|
|
|
|
# Only test maybe_inplace with non-inplace implementations
|
|
# Inplace implementations return aliases of inputs which is not allowed.
|
|
# We break this invariant, but we also convert maybe_inplace to the default
|
|
# overload during compilation, so maybe_inplace never reaches Inductor.
|
|
if not ir.ops.fused_add_rms_norm.impls[provider].inplace:
|
|
torch.library.opcheck(
|
|
torch.ops.vllm_ir.fused_add_rms_norm.maybe_inplace, args
|
|
)
|