Files
vllm-omni/tests/diffusion/models/sensenova_u1/test_sensenova_triton_kernel.py
T

233 lines
7.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Correctness tests for Sensenova-U1 Triton kernels.
Verifies that fused-RMSNorm+3D Rope Triton kernels
produce results numerically equivalent to the PyTorch reference.
All computations follow the kernel's internal float32 promotion rules.
"""
from __future__ import annotations
from dataclasses import dataclass
import pytest
import torch
pytestmark = [
pytest.mark.core_model,
pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"),
]
SEED = 42
B = 1
H_Q = 32
H_K = 8
HEAD_DIM = 128
T_DIM = 64
HW_DIM = 64
HW_ROPE_DIM = 32
QKV_DIM = (H_Q + H_K + H_K) * HEAD_DIM
EPS = 1e-6
DEVICE = torch.device("cuda:0")
@dataclass(frozen=True)
class KernelInput:
seq_len: int
q: torch.Tensor
k: torch.Tensor
q_norm_weight: torch.Tensor
k_norm_weight: torch.Tensor
q_norm_hw_weight: torch.Tensor
k_norm_hw_weight: torch.Tensor
cos_t: torch.Tensor
sin_t: torch.Tensor
cos_h: torch.Tensor
sin_h: torch.Tensor
cos_w: torch.Tensor
sin_w: torch.Tensor
def args(self) -> tuple[torch.Tensor, ...]:
return (
self.q,
self.k,
self.q_norm_weight,
self.k_norm_weight,
self.q_norm_hw_weight,
self.k_norm_hw_weight,
self.cos_t,
self.sin_t,
self.cos_h,
self.sin_h,
self.cos_w,
self.sin_w,
)
try:
from vllm_omni.diffusion.models.sensenova_u1.fused_rmsnorm_rope import triton_qk_norm_rope
_TRITON_AVAILABLE = True
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 rotate_half(x: torch.Tensor) -> torch.Tensor:
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def apply_rope(
q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
cos = cos.unsqueeze(1)
sin = sin.unsqueeze(1)
return (q * cos) + (rotate_half(q) * sin), (k * cos) + (rotate_half(k) * sin)
def rmsnorm(x: torch.Tensor, weight: torch.Tensor, eps: float = EPS) -> torch.Tensor:
input_dtype = x.dtype
xf = x.float()
var = xf.pow(2).mean(dim=-1, keepdim=True)
xf = xf * torch.rsqrt(var + eps)
return xf.to(input_dtype) * weight
def assert_close_with_error_stats(
actual: torch.Tensor,
expected: torch.Tensor,
*,
name: str,
atol: float,
rtol: float,
) -> None:
try:
torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
except AssertionError as exc:
abs_diff = (actual.float() - expected.float()).abs().flatten()
tolerance = atol + rtol * expected.float().abs().flatten()
mismatch = abs_diff > tolerance
mismatch_count = int(mismatch.sum().item())
total = abs_diff.numel()
p99 = torch.quantile(abs_diff, 0.99).item() if total else 0.0
stats = (
f"{name} error stats: "
f"max={abs_diff.max().item() if total else 0.0:.6g}, "
f"p99={p99:.6g}, "
f"mean={abs_diff.mean().item() if total else 0.0:.6g}, "
f"mismatch={mismatch_count}/{total} ({mismatch_count / total:.4%}), "
f"atol={atol}, rtol={rtol}"
)
raise AssertionError(f"{stats}\n{exc}") from exc
def reference_kernel(
q: torch.Tensor,
k: torch.Tensor,
q_norm_weight: torch.Tensor,
k_norm_weight: torch.Tensor,
q_norm_hw_weight: torch.Tensor,
k_norm_hw_weight: torch.Tensor,
cos_t: torch.Tensor,
sin_t: torch.Tensor,
cos_h: torch.Tensor,
sin_h: torch.Tensor,
cos_w: torch.Tensor,
sin_w: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""PyTorch reference matching the model's current math."""
q_t, q_hw = q.chunk(2, dim=-1)
k_t, k_hw = k.chunk(2, dim=-1)
q_t = rmsnorm(q_t, q_norm_weight).transpose(1, 2)
k_t = rmsnorm(k_t, k_norm_weight).transpose(1, 2)
q_hw = rmsnorm(q_hw, q_norm_hw_weight).transpose(1, 2)
k_hw = rmsnorm(k_hw, k_norm_hw_weight).transpose(1, 2)
q_h, q_w = q_hw.chunk(2, dim=-1)
k_h, k_w = k_hw.chunk(2, dim=-1)
q_t, k_t = apply_rope(q_t, k_t, cos_t, sin_t)
q_h, k_h = apply_rope(q_h, k_h, cos_h, sin_h)
q_w, k_w = apply_rope(q_w, k_w, cos_w, sin_w)
query = torch.cat([q_t, q_h, q_w], dim=-1)
key = torch.cat([k_t, k_h, k_w], dim=-1)
return query, key
def make_input(seq_len: int, dtype: torch.dtype, device: torch.device, seed: int) -> KernelInput:
gen = torch.Generator(device=device)
gen.manual_seed(seed + seq_len)
# Match real model layout: q/k/v are views split from fused qkv.
qkv = torch.randn(B, seq_len, QKV_DIM, device=device, dtype=dtype, generator=gen)
q, k, _v = qkv.split([H_Q * HEAD_DIM, H_K * HEAD_DIM, H_K * HEAD_DIM], dim=-1)
q = q.view(B, seq_len, H_Q, HEAD_DIM)
k = k.view(B, seq_len, H_K, HEAD_DIM)
q_norm_weight = torch.randn(T_DIM, device=device, dtype=dtype, generator=gen)
k_norm_weight = torch.randn(T_DIM, device=device, dtype=dtype, generator=gen)
q_norm_hw_weight = torch.randn(HW_DIM, device=device, dtype=dtype, generator=gen)
k_norm_hw_weight = torch.randn(HW_DIM, device=device, dtype=dtype, generator=gen)
# Match Qwen3RotaryEmbedding: emb = torch.cat((freqs, freqs), dim=-1).
def make_rope_pair(dim: int) -> tuple[torch.Tensor, torch.Tensor]:
cos_half = torch.randn(B, seq_len, dim // 2, device=device, dtype=dtype, generator=gen)
sin_half = torch.randn(B, seq_len, dim // 2, device=device, dtype=dtype, generator=gen)
return torch.cat((cos_half, cos_half), dim=-1), torch.cat((sin_half, sin_half), dim=-1)
cos_t, sin_t = make_rope_pair(T_DIM)
cos_h, sin_h = make_rope_pair(HW_ROPE_DIM)
cos_w, sin_w = make_rope_pair(HW_ROPE_DIM)
return KernelInput(
seq_len=seq_len,
q=q,
k=k,
q_norm_weight=q_norm_weight,
k_norm_weight=k_norm_weight,
q_norm_hw_weight=q_norm_hw_weight,
k_norm_hw_weight=k_norm_hw_weight,
cos_t=cos_t,
sin_t=sin_t,
cos_h=cos_h,
sin_h=sin_h,
cos_w=cos_w,
sin_w=sin_w,
)
# ---------------------------------------------------------------------------
# Fused RMSNorm + 3D Rope
# ---------------------------------------------------------------------------
@triton_available
@pytest.mark.parametrize("seq_len", [1, 2, 4, 8, 9, 16, 32, 64, 128, 256, 260, 271, 512, 1024, 2048, 4096])
@pytest.mark.parametrize(
("dtype", "atol", "rtol"),
[
(torch.float32, 1e-5, 1e-5),
(torch.bfloat16, 3e-2, 3e-2),
],
)
def test_fused_qk_norm_rope_matches_reference(seq_len: int, dtype: torch.dtype, atol: float, rtol: float):
data = make_input(seq_len, dtype=dtype, device=DEVICE, seed=SEED)
call_args = data.args()
ref_q, ref_k = reference_kernel(*call_args)
out_q, out_k = triton_qk_norm_rope(*call_args, EPS)
assert_close_with_error_stats(out_q, ref_q, name="query", atol=atol, rtol=rtol)
assert_close_with_error_stats(out_k, ref_k, name="key", atol=atol, rtol=rtol)