Files
vllm-omni/tests/model_executor/models/omnivoice/test_triton_kernels.py
T
2026-05-22 13:12:47 +00:00

201 lines
6.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Correctness tests for OmniVoice Triton kernels.
Verifies that RMSNorm, SwiGLU, and fused-add+RMSNorm Triton kernels
produce results numerically equivalent to the PyTorch reference.
All computations follow the kernel's internal float32 promotion rules.
"""
from __future__ import annotations
import pytest
import torch
pytestmark = [
pytest.mark.core_model,
pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"),
]
DEVICE = torch.device("cuda:0")
EPS = 1e-6
try:
from vllm_omni.model_executor.models.omnivoice.omnivoice_generator import (
_TRITON_AVAILABLE,
triton_fused_add_rms_norm,
triton_rms_norm,
triton_swiglu,
)
except ImportError:
_TRITON_AVAILABLE = False
triton_available = pytest.mark.skipif(not _TRITON_AVAILABLE, reason="Triton not available on this platform")
# ---------------------------------------------------------------------------
# PyTorch reference implementations (mirror the kernel's internal arithmetic)
# ---------------------------------------------------------------------------
def _ref_rms_norm(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
# Kernel promotes x to float32, computes RMS, casts back to x.dtype, then mul weight
x_f32 = x.float()
ms = x_f32.pow(2).mean(-1, keepdim=True)
normed = (x_f32 * torch.rsqrt(ms + eps)).to(x.dtype)
return normed * weight.to(normed.dtype)
def _ref_swiglu(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor:
# Kernel: gate → float32, silu, cast to up.dtype, mul up
gate_f32 = gate.float()
silu = gate_f32 * torch.sigmoid(gate_f32)
return silu.to(up.dtype) * up
def _ref_fused_add_rms_norm(
x: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, eps: float
) -> tuple[torch.Tensor, torch.Tensor]:
# Kernel: s_f32 = x+r in float32, s stored as x.dtype; y = norm(s_f32)*weight
s_f32 = x.float() + residual.float()
s = s_f32.to(x.dtype)
ms = s_f32.pow(2).mean(-1, keepdim=True)
normed = (s_f32 * torch.rsqrt(ms + eps)).to(x.dtype)
y = normed * weight.to(normed.dtype)
return y, s
# ---------------------------------------------------------------------------
# RMSNorm
# ---------------------------------------------------------------------------
@triton_available
@pytest.mark.parametrize("rows,cols", [(1, 64), (4, 512), (16, 1024), (32, 3072)])
def test_rms_norm_float32(rows, cols):
torch.manual_seed(0)
x = torch.randn(rows, cols, device=DEVICE)
w = torch.ones(cols, device=DEVICE)
ref = _ref_rms_norm(x, w, EPS)
out = triton_rms_norm(x, w, EPS)
torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5)
@triton_available
@pytest.mark.parametrize("shape", [(2, 16, 64), (4, 32, 512)])
def test_rms_norm_3d_input(shape):
"""Triton wrapper must handle 3-D [B, S, H] inputs by flattening and restoring shape."""
torch.manual_seed(1)
hidden = shape[-1]
x = torch.randn(*shape, device=DEVICE)
w = torch.ones(hidden, device=DEVICE)
ref = _ref_rms_norm(x, w, EPS)
out = triton_rms_norm(x, w, EPS)
torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5)
assert out.shape == x.shape
@triton_available
def test_rms_norm_no_nan_on_zero_input():
"""RMSNorm of the zero vector must not produce NaN (eps prevents divide-by-zero)."""
x = torch.zeros(4, 256, device=DEVICE)
w = torch.ones(256, device=DEVICE)
out = triton_rms_norm(x, w, EPS)
assert not torch.isnan(out).any()
# ---------------------------------------------------------------------------
# SwiGLU
# ---------------------------------------------------------------------------
@triton_available
@pytest.mark.parametrize("rows,cols", [(1, 64), (4, 512), (16, 3072)])
def test_swiglu_float32(rows, cols):
torch.manual_seed(2)
gate = torch.randn(rows, cols, device=DEVICE)
up = torch.randn(rows, cols, device=DEVICE)
ref = _ref_swiglu(gate, up)
out = triton_swiglu(gate, up)
torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5)
@triton_available
def test_swiglu_negative_gate():
"""silu(x) = x*sigmoid(x) is defined for negative x; result must not be NaN."""
gate = torch.full((4, 64), -10.0, device=DEVICE)
up = torch.ones(4, 64, device=DEVICE)
ref = _ref_swiglu(gate, up)
out = triton_swiglu(gate, up)
torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5)
assert not torch.isnan(out).any()
@triton_available
def test_swiglu_output_shape_preserved():
gate = torch.randn(3, 8, 512, device=DEVICE)
up = torch.randn(3, 8, 512, device=DEVICE)
out = triton_swiglu(gate, up)
assert out.shape == gate.shape
# ---------------------------------------------------------------------------
# Fused Add + RMSNorm
# ---------------------------------------------------------------------------
@triton_available
@pytest.mark.parametrize("rows,cols", [(1, 64), (4, 512), (16, 1024)])
def test_fused_add_rms_norm_float32(rows, cols):
torch.manual_seed(3)
x = torch.randn(rows, cols, device=DEVICE)
residual = torch.randn(rows, cols, device=DEVICE)
weight = torch.ones(cols, device=DEVICE)
ref_y, ref_s = _ref_fused_add_rms_norm(x, residual, weight, EPS)
out_y, out_s = triton_fused_add_rms_norm(x, residual, weight, EPS)
torch.testing.assert_close(out_y, ref_y, atol=1e-5, rtol=1e-5)
# Sum output s = x + r must be bit-exact (no numerics involved, just addition)
torch.testing.assert_close(out_s, ref_s, atol=0, rtol=0)
@triton_available
def test_fused_add_rms_norm_sum_is_bitexact():
"""The residual sum s = x + r must be bit-exact between Triton and PyTorch."""
torch.manual_seed(4)
x = torch.randn(8, 512, device=DEVICE)
r = torch.randn(8, 512, device=DEVICE)
w = torch.ones(512, device=DEVICE)
_, ref_s = _ref_fused_add_rms_norm(x, r, w, EPS)
_, out_s = triton_fused_add_rms_norm(x, r, w, EPS)
torch.testing.assert_close(out_s, ref_s, atol=0, rtol=0)
@triton_available
def test_fused_add_rms_norm_returns_two_tensors():
"""Wrapper must return exactly two tensors: (normed_output, sum)."""
x = torch.randn(4, 256, device=DEVICE)
r = torch.randn(4, 256, device=DEVICE)
w = torch.ones(256, device=DEVICE)
result = triton_fused_add_rms_norm(x, r, w, EPS)
assert len(result) == 2
y, s = result
assert y.shape == x.shape
assert s.shape == x.shape