Signed-off-by: GUOGUO <xwdong1998@163.com> Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
463 lines
22 KiB
Python
463 lines
22 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""P0 correctness matrix for the fused AdaLayerNorm CUDA path.
|
|
|
|
Covers: fp32/bf16/fp16, B>1 (shared and per-sample modulation), C in
|
|
{1536, 3072, 4096}, varying L, elementwise_affine False/True with
|
|
NON-DEFAULT weight/bias, eps variants, legal non-contiguous inputs,
|
|
broadcast scale/shift shapes ((C,), (1, C), (1, 1, C), (B, 1, C)),
|
|
fallback completeness (fp64, oversized hidden size, cross-device
|
|
parameters), large-offset/small-variance stability, and the frozen
|
|
B > 1 (B, C) native broadcast-error contract.
|
|
"""
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from vllm_omni.diffusion.layers.adalayernorm import (
|
|
AdaLayerNorm,
|
|
_adaln_fused_forward,
|
|
)
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cuda]
|
|
|
|
TOL_STRICT = {"bf16": (2e-2, 2e-2), "fp16": (1e-2, 1e-2), "fp32": (1e-3, 1e-3)}
|
|
TOL_LOOSE = {"bf16": (5e-2, 2e-2), "fp16": (2e-2, 1e-2), "fp32": (5e-3, 5e-3)}
|
|
DTYPES = [torch.bfloat16, torch.float16, torch.float32]
|
|
|
|
|
|
def tol_key(dtype):
|
|
if dtype == torch.bfloat16:
|
|
return "bf16"
|
|
if dtype == torch.float16:
|
|
return "fp16"
|
|
return "fp32"
|
|
|
|
|
|
def fp32_reference(x, scale, shift, eps, weight=None, bias=None):
|
|
xf = x.float()
|
|
w = weight.float() if weight is not None else None
|
|
b = bias.float() if bias is not None else None
|
|
xn = torch.nn.functional.layer_norm(xf, (x.shape[-1],), w, b, eps)
|
|
return xn * (1 + scale.float()[:, None]) + shift.float()[:, None]
|
|
|
|
|
|
def make_module(hidden, elementwise_affine, eps, device, dtype, nondefault_affine=False):
|
|
m = AdaLayerNorm(hidden, elementwise_affine=elementwise_affine, eps=eps)
|
|
if elementwise_affine:
|
|
m = m.to(dtype)
|
|
if nondefault_affine:
|
|
with torch.no_grad():
|
|
g = torch.Generator(device="cpu").manual_seed(42)
|
|
m.layernorm.weight.copy_(0.5 + torch.rand(hidden, generator=g))
|
|
m.layernorm.bias.copy_(0.2 * torch.randn(hidden, generator=g))
|
|
return m.to(device)
|
|
|
|
|
|
def make_inputs(bs, seq, hidden, dtype, device, seed=0, mod_shape=None):
|
|
g = torch.Generator(device=device).manual_seed(seed)
|
|
x = torch.randn(bs, seq, hidden, generator=g, device=device, dtype=dtype)
|
|
ms = mod_shape or (1, hidden)
|
|
scale = torch.randn(ms, generator=g, device=device, dtype=dtype)
|
|
shift = torch.randn(ms, generator=g, device=device, dtype=dtype)
|
|
return x, scale, shift
|
|
|
|
|
|
def assert_close(a, b, dtype, loose=False):
|
|
atol, rtol = (TOL_LOOSE if loose else TOL_STRICT)[tol_key(dtype)]
|
|
torch.testing.assert_close(a.float(), b.float(), atol=atol, rtol=rtol)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", DTYPES)
|
|
@pytest.mark.parametrize("chunks", [2, 6])
|
|
@pytest.mark.parametrize("bs,frames,spatial", [(1, 21, 880), (2, 3, 5), (2, 1, 5), (1, 3, 5)])
|
|
def test_matrix_framewise_modulation(dtype, chunks, bs, frames, spatial):
|
|
hidden = 2240
|
|
m = make_module(hidden, False, 1e-6, "cuda", dtype)
|
|
g = torch.Generator(device="cuda").manual_seed(61)
|
|
x = torch.randn(bs, frames, spatial, hidden, generator=g, device="cuda", dtype=dtype)
|
|
modulation = torch.randn(bs, frames, chunks, hidden, generator=g, device="cuda", dtype=dtype)
|
|
scale, shift = modulation.chunk(chunks, dim=2)[:2]
|
|
# Sana's block/final modulation uses chunk views spanning frames.
|
|
assert scale.shape == (bs, frames, 1, hidden)
|
|
assert scale.stride(1) == chunks * hidden
|
|
fused = _adaln_fused_forward(m, x, scale, shift)
|
|
assert fused is not None, "framewise chunk views must take the fused path"
|
|
assert fused.shape == x.shape and fused.dtype == dtype
|
|
assert_close(fused, m.forward_native(x, scale, shift), dtype)
|
|
ref = torch.nn.functional.layer_norm(x.float(), (hidden,), eps=m.eps)
|
|
ref = ref * (1 + scale.float()) + shift.float()
|
|
assert_close(fused, ref, dtype, loose=True)
|
|
|
|
|
|
@pytest.mark.parametrize("shared_scale,shared_shift", [(False, True), (True, False), (True, True)])
|
|
@torch.no_grad()
|
|
def test_matrix_framewise_shared_modulation(shared_scale, shared_shift):
|
|
dtype = torch.bfloat16
|
|
hidden = 2240
|
|
m = make_module(hidden, True, 1e-6, "cuda", dtype, nondefault_affine=True)
|
|
x = torch.randn(2, 3, 5, hidden, device="cuda", dtype=dtype)
|
|
scale = torch.randn((hidden,) if shared_scale else (2, 3, 1, hidden), device="cuda", dtype=dtype)
|
|
shift = torch.randn((1, 1, 1, hidden) if shared_shift else (2, 3, 1, hidden), device="cuda", dtype=dtype)
|
|
fused = _adaln_fused_forward(m, x, scale, shift)
|
|
assert fused is not None
|
|
assert_close(fused, m.forward_native(x, scale, shift), dtype)
|
|
|
|
|
|
def test_matrix_framewise_irregular_group_stride_falls_back():
|
|
dtype = torch.bfloat16
|
|
hidden = 2240
|
|
m = make_module(hidden, False, 1e-6, "cuda", dtype)
|
|
x = torch.randn(2, 3, 5, hidden, device="cuda", dtype=dtype)
|
|
modulation = torch.randn(3, 2, 2, hidden, device="cuda", dtype=dtype).transpose(0, 1)
|
|
scale, shift = modulation.chunk(2, dim=2)
|
|
assert scale.stride(0) != x.shape[1] * scale.stride(1)
|
|
assert _adaln_fused_forward(m, x, scale, shift) is None
|
|
assert_close(m.forward_cuda(x, scale, shift), m.forward_native(x, scale, shift), dtype)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", DTYPES)
|
|
def test_matrix_sana_patch_embed_reaches_framewise_fused_path(dtype):
|
|
from vllm_omni.diffusion.models.sana_wm.sana_wm_transformer import SanaWmPatchEmbedMS3D
|
|
|
|
hidden = 2240
|
|
patch = SanaWmPatchEmbedMS3D((1, 1, 1), 8, hidden).to(device="cuda", dtype=dtype)
|
|
latents = torch.randn(2, 8, 3, 2, 3, device="cuda", dtype=dtype)
|
|
with torch.no_grad():
|
|
projected = patch.proj(latents)
|
|
tokens, (frames, height, width) = patch.project_with_shape(latents)
|
|
torch.testing.assert_close(tokens, projected.flatten(2).transpose(1, 2), atol=0, rtol=0)
|
|
assert tokens.is_contiguous()
|
|
x = tokens.reshape(2, frames, height * width, hidden)
|
|
modulation = torch.randn(2, frames, 6, hidden, device="cuda", dtype=dtype)
|
|
scale, shift = modulation.chunk(6, dim=2)[:2]
|
|
m = make_module(hidden, False, 1e-6, "cuda", dtype)
|
|
fused = _adaln_fused_forward(m, x, scale, shift)
|
|
assert fused is not None, "real Sana patch features must reach the fused path"
|
|
assert_close(fused, m.forward_native(x, scale, shift), dtype)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", DTYPES)
|
|
@pytest.mark.parametrize("affine", [False, True])
|
|
@pytest.mark.parametrize(
|
|
"bs,seq,hidden", [(1, 4096, 3072), (2, 1024, 1536), (3, 128, 4096), (1, 1, 3072), (1, 8192, 1536)]
|
|
)
|
|
@torch.no_grad()
|
|
def test_matrix_main(dtype, affine, bs, seq, hidden):
|
|
device = "cuda"
|
|
m = make_module(hidden, affine, 1e-6, device, dtype)
|
|
x, scale, shift = make_inputs(bs, seq, hidden, dtype, device)
|
|
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
|
assert out_cuda is not None, "supported matrix inputs must take the fused path"
|
|
out_native = m.forward_native(x, scale, shift)
|
|
assert out_cuda.shape == x.shape and out_cuda.dtype == dtype and out_cuda.device == x.device
|
|
assert_close(out_cuda, out_native, dtype, loose=False)
|
|
w = m.layernorm.weight if affine else None
|
|
b = m.layernorm.bias if affine else None
|
|
assert_close(out_cuda, fp32_reference(x, scale, shift, 1e-6, w, b), dtype, loose=True)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", DTYPES)
|
|
@torch.no_grad()
|
|
def test_matrix_weight_bias_nondefault(dtype):
|
|
# weight/bias semantics with non-identity initialization:
|
|
# out = (w * ln(x) + b) * (1 + scale) + shift
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, True, 1e-6, device, dtype, nondefault_affine=True)
|
|
x, scale, shift = make_inputs(1, 1024, hidden, dtype, device, seed=13)
|
|
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
|
assert out_cuda is not None, "non-default affine parameters must take the fused path"
|
|
out_native = m.forward_native(x, scale, shift)
|
|
assert_close(out_cuda, out_native, dtype)
|
|
ref = fp32_reference(x, scale, shift, 1e-6, m.layernorm.weight, m.layernorm.bias)
|
|
assert_close(out_cuda, ref, dtype, loose=True)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
@pytest.mark.parametrize("eps", [1e-5, 1e-3, 1e-2])
|
|
def test_matrix_eps_variants(dtype, eps):
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, eps, device, dtype)
|
|
x, scale, shift = make_inputs(1, 512, hidden, dtype, device, seed=3)
|
|
fused = _adaln_fused_forward(m, x, scale, shift)
|
|
assert fused is not None, "supported eps variants must take the fused path"
|
|
assert_close(fused, m.forward_native(x, scale, shift), dtype)
|
|
assert_close(
|
|
fused,
|
|
fp32_reference(x, scale, shift, eps),
|
|
dtype,
|
|
loose=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
@pytest.mark.parametrize("mod_shape", ["C", "1x1xC", "1xC"])
|
|
def test_matrix_broadcast_scale_shift(dtype, mod_shape):
|
|
# (C,), (1, 1, C) and (1, C) are all legal per-channel broadcasts against x (B, L, C)
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, _, _ = make_inputs(2, 512, hidden, dtype, device, seed=5)
|
|
g = torch.Generator(device=device).manual_seed(6)
|
|
if mod_shape == "C":
|
|
scale = torch.randn(hidden, generator=g, device=device, dtype=dtype)
|
|
shift = torch.randn(hidden, generator=g, device=device, dtype=dtype)
|
|
elif mod_shape == "1x1xC":
|
|
scale = torch.randn(1, 1, hidden, generator=g, device=device, dtype=dtype)
|
|
shift = torch.randn(1, 1, hidden, generator=g, device=device, dtype=dtype)
|
|
else:
|
|
scale = torch.randn(1, hidden, generator=g, device=device, dtype=dtype)
|
|
shift = torch.randn(1, hidden, generator=g, device=device, dtype=dtype)
|
|
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
|
assert out_cuda is not None, f"shared modulation layout {mod_shape} must take the fused path"
|
|
out_native = m.forward_native(x, scale, shift)
|
|
assert_close(out_cuda, out_native, dtype)
|
|
s = scale.float().reshape(1, 1, hidden) if scale.ndim == 1 else scale.float()
|
|
sh = shift.float().reshape(1, 1, hidden) if shift.ndim == 1 else shift.float()
|
|
ref = fp32_reference(x, s.reshape(-1, hidden)[0:1], sh.reshape(-1, hidden)[0:1], 1e-6)
|
|
assert_close(out_cuda, ref, dtype, loose=True)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
def test_matrix_per_sample_modulation(dtype):
|
|
# Qwen-Image's _modulate produces (B, 1, C): one modulation row per sample.
|
|
# B > 1 must take the fused path (not fall back to native) and match native.
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, _, _ = make_inputs(4, 512, hidden, dtype, device, seed=7, mod_shape=(4, hidden))
|
|
g = torch.Generator(device=device).manual_seed(8)
|
|
scale = torch.randn(4, 1, hidden, generator=g, device=device, dtype=dtype)
|
|
shift = torch.randn(4, 1, hidden, generator=g, device=device, dtype=dtype)
|
|
fused = _adaln_fused_forward(m, x, scale, shift)
|
|
assert fused is not None, "(B, 1, C) per-sample modulation must take the fused path"
|
|
out_cuda = fused
|
|
out_native = m.forward_native(x, scale, shift)
|
|
assert_close(out_cuda, out_native, dtype)
|
|
# fp32_reference's [:, None] expects a 2D (B, C) modulation row; the
|
|
# (B, 1, C) tensor reshapes to it losslessly.
|
|
ref = fp32_reference(x, scale.reshape(x.shape[0], hidden), shift.reshape(x.shape[0], hidden), 1e-6)
|
|
assert_close(out_cuda, ref, dtype, loose=True)
|
|
# Per-sample: different rows must receive different modulation.
|
|
row_diff = (out_cuda[0] - out_cuda[1]).abs().max().item()
|
|
assert row_diff > 0
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
def test_matrix_chunk_view_modulation(dtype):
|
|
# Wan2.2-style producer: the modulation projection is (B, 6, C) and the
|
|
# consumer chunks it along dim=1. The resulting (B, 1, C) views are
|
|
# NON-CONTIGUOUS (row stride 6*C) - the fused path must consume them
|
|
# directly via the explicit row stride, without a contiguous copy.
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, _, _ = make_inputs(4, 512, hidden, dtype, device, seed=27)
|
|
g = torch.Generator(device=device).manual_seed(28)
|
|
source = torch.randn(4, 6, hidden, generator=g, device=device, dtype=dtype) * 0.1
|
|
chunks = source.chunk(6, dim=1)
|
|
scale, shift = chunks[1], chunks[2]
|
|
assert scale.shape == (4, 1, hidden) and shift.shape == (4, 1, hidden)
|
|
assert not scale.is_contiguous()
|
|
assert scale.stride(-1) == 1
|
|
fused = _adaln_fused_forward(m, x, scale, shift)
|
|
assert fused is not None, "chunk-view modulation must take the fused path"
|
|
out_native = m.forward_native(x, scale, shift)
|
|
assert_close(fused, out_native, dtype)
|
|
ref = fp32_reference(x, scale.reshape(4, hidden), shift.reshape(4, hidden), 1e-6)
|
|
assert_close(fused, ref, dtype, loose=True)
|
|
|
|
|
|
def test_matrix_3d_modulation_fallback():
|
|
# Wan2.2 TI2V-style per-token modulation: scale/shift (B, L, C) full 3D.
|
|
# This is intentionally outside the fused kernel contract - the fast path
|
|
# must be rejected (None) and forward_cuda must match native.
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, torch.bfloat16)
|
|
x, scale, shift = make_inputs(2, 128, hidden, torch.bfloat16, device, seed=29, mod_shape=(2, 128, hidden))
|
|
fused = _adaln_fused_forward(m, x, scale, shift)
|
|
assert fused is None, "(B, L, C) per-token modulation must be rejected"
|
|
out = m.forward_cuda(x, scale, shift)
|
|
native = m.forward_native(x, scale, shift)
|
|
torch.testing.assert_close(out.float(), native.float(), atol=2e-2, rtol=2e-2)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
|
def test_matrix_production_widths(dtype):
|
|
# Real consumer widths: Sana-WM 2240 (BLOCK_C 4096, 45.3% mask) and
|
|
# Wan2.2 A14B 5120 (BLOCK_C 8192, the max supported block, 16-warp
|
|
# compiled variant). BF16 + elementwise_affine=False.
|
|
device = "cuda"
|
|
for hidden in (2240, 5120):
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, scale, shift = make_inputs(1, 512, hidden, dtype, device, seed=31)
|
|
fused = _adaln_fused_forward(m, x, scale, shift)
|
|
assert fused is not None, f"hidden={hidden} must take the fused path"
|
|
out_native = m.forward_native(x, scale, shift)
|
|
assert_close(fused, out_native, dtype)
|
|
assert_close(fused, fp32_reference(x, scale, shift, 1e-6), dtype, loose=True)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
def test_matrix_noncontiguous_fallback(dtype):
|
|
# Non-contiguous inputs must fall back to native and stay correct
|
|
# (the fused kernel only serves the contiguous fast path).
|
|
device = "cuda"
|
|
hidden = 1536
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x_big, scale, shift = make_inputs(1, 1024, hidden, dtype, device, seed=7)
|
|
x = x_big[:, ::2, :]
|
|
assert not x.is_contiguous()
|
|
out_cuda = m.forward_cuda(x, scale, shift)
|
|
out_native = m.forward_native(x, scale, shift)
|
|
assert_close(out_cuda, out_native, dtype)
|
|
assert_close(out_cuda, fp32_reference(x.contiguous(), scale, shift, 1e-6), dtype, loose=True)
|
|
|
|
|
|
def test_matrix_fallback_fp64():
|
|
# fp64 is not in the kernel's supported dtype list -> must fall back to
|
|
# native and stay correct (fallback completeness).
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, torch.float64)
|
|
x, scale, shift = make_inputs(1, 256, hidden, torch.float64, device, seed=9)
|
|
out_cuda = m.forward_cuda(x, scale, shift)
|
|
out_native = m.forward_native(x, scale, shift)
|
|
torch.testing.assert_close(out_cuda, out_native)
|
|
|
|
|
|
def test_matrix_fallback_oversized_hidden():
|
|
# hidden sizes whose next_power_of_2 exceeds the kernel's supported block
|
|
# bound must fall back to native and stay correct.
|
|
device = "cuda"
|
|
hidden = 10000 # next_power_of_2 = 16384 > MAX_BLOCK_C
|
|
m = make_module(hidden, False, 1e-6, device, torch.bfloat16)
|
|
x, scale, shift = make_inputs(1, 128, hidden, torch.bfloat16, device, seed=10)
|
|
out_cuda = m.forward_cuda(x, scale, shift)
|
|
out_native = m.forward_native(x, scale, shift)
|
|
torch.testing.assert_close(out_cuda, out_native)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
def test_matrix_zero_scale_shift_identity(dtype):
|
|
# With scale = shift = 0 the output must equal a pure LayerNorm.
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, _, _ = make_inputs(1, 256, hidden, dtype, device, seed=19)
|
|
scale = torch.zeros(1, hidden, device=device, dtype=dtype)
|
|
shift = torch.zeros(1, hidden, device=device, dtype=dtype)
|
|
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
|
assert out_cuda is not None, "zero modulation must take the fused path"
|
|
assert_close(out_cuda, m.forward_native(x, scale, shift), dtype)
|
|
assert_close(out_cuda, m.layernorm(x), dtype)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
def test_matrix_determinism(dtype):
|
|
# Two calls on the same input must agree bitwise (a hard constraint for
|
|
# any cached/direct-launch scheme).
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, scale, shift = make_inputs(1, 4096, hidden, dtype, device, seed=11)
|
|
out1 = _adaln_fused_forward(m, x, scale, shift)
|
|
out2 = _adaln_fused_forward(m, x, scale, shift)
|
|
assert out1 is not None and out2 is not None, "determinism must exercise the fused path"
|
|
assert torch.equal(out1, out2)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
def test_matrix_multi_batch_shared_modulation(dtype):
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, scale, shift = make_inputs(4, 512, hidden, dtype, device, seed=15)
|
|
out = _adaln_fused_forward(m, x, scale, shift)
|
|
assert out is not None, "multi-batch shared modulation must take the fused path"
|
|
assert_close(out, m.forward_native(x, scale, shift), dtype)
|
|
for b in range(4):
|
|
single = _adaln_fused_forward(m, x[b : b + 1], scale, shift)
|
|
assert single is not None
|
|
assert_close(out[b : b + 1], single, dtype)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
def test_matrix_bc_modulation_preserves_native_error(dtype):
|
|
# Frozen native contract: (B, C) modulation with B > 1 raises
|
|
# RuntimeError (torch broadcasts B against L). The fused path preserves
|
|
# this behavior - it neither supports nor silently changes it.
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, scale, shift = make_inputs(4, 64, hidden, dtype, device, seed=17, mod_shape=(4, hidden))
|
|
with pytest.raises(RuntimeError):
|
|
m.forward_native(x, scale, shift)
|
|
with pytest.raises(RuntimeError):
|
|
m.forward_cuda(x, scale, shift)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
|
def test_matrix_cross_device_fallback(dtype):
|
|
# Raw pointers go to one Triton kernel, which does no cross-device
|
|
# checking: the fast-path guard must reject cross-device parameters so
|
|
# they fall back to native, which raises torch's own cross-device error
|
|
# instead of crashing in the kernel.
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, scale, shift = make_inputs(1, 512, hidden, dtype, device, seed=21)
|
|
cpu_scale = scale.to("cpu")
|
|
with pytest.raises(RuntimeError):
|
|
m.forward_cuda(x, cpu_scale, shift)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16, torch.float32])
|
|
def test_matrix_large_offset_small_variance(dtype):
|
|
"""A large constant offset with a small variance must stay stable.
|
|
|
|
Mirrors the fused_adaptive_group_norm_silu case: x ~ 10000 +/- 0.1 has
|
|
mean ~1e4 and variance ~1e-2, so a plain fp32 sum of large-offset values
|
|
rounds away the low-order bits that the variance is built from. The fused
|
|
kernel centers the row on its first element before the tree sum
|
|
(shift-invariant two-pass), keeping mean/variance accurate.
|
|
|
|
For fp32 the meaningful reference is an fp64 computation, NOT the native
|
|
eager chain: at mean/std = 1e5 the native chain itself carries a
|
|
~0.07 absolute deviation from the fp64 truth (its own fp32 dynamic-range
|
|
floor), while the fused kernel stays within ~2e-6. For fp16/bf16 inputs
|
|
the +/-0.1 perturbation quantizes away at magnitude 1e4, so the
|
|
meaningful assertion is that outputs stay finite and match native.
|
|
"""
|
|
device = "cuda"
|
|
hidden = 3072
|
|
m = make_module(hidden, False, 1e-6, device, dtype)
|
|
x, scale, shift = make_inputs(1, 4096, hidden, dtype, device, seed=23)
|
|
# Apply the large constant offset AFTER generation: x ~ 10000 +/- 0.1 is
|
|
# the cancellation case that motivates the shift-invariant two-pass.
|
|
x = x * 0.1 + 10000
|
|
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
|
assert out_cuda is not None, "large-offset stability must exercise the fused path"
|
|
out_native = m.forward_native(x, scale, shift)
|
|
assert torch.isfinite(out_cuda).all()
|
|
|
|
# fp64 reference computed from the SAME fp32 input the implementations see
|
|
# (the kernel cannot recover bits that fp32 quantization never stored).
|
|
x64 = x.double()
|
|
mean64 = x64.mean(dim=-1, keepdim=True)
|
|
var64 = x64.var(dim=-1, unbiased=False, keepdim=True)
|
|
ref64 = (x64 - mean64) * torch.rsqrt(var64 + 1e-6) * (1 + scale.double()[:, None, :]) + shift.double()[:, None, :]
|
|
|
|
if dtype is torch.float32:
|
|
# fp32 fused output vs the fp64 truth of the same input: within the
|
|
# fp32 representation floor (~2e-6 measured). The native eager chain
|
|
# deviates ~0.07 on the same input (its own dynamic-range floor) -
|
|
# documented, not asserted.
|
|
torch.testing.assert_close(out_cuda.double(), ref64, atol=1e-4, rtol=1e-4)
|
|
else:
|
|
# bf16/fp16 quantize the perturbation away: no NaN, matches native.
|
|
assert_close(out_cuda, out_native, dtype, loose=True)
|