main
py 138 lines 5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Correctness tests for the fused Inkling vision/audio tower kernels.
4
5 Each kernel is checked against a pure-PyTorch reference implementing the
6 towers' original op sequence (native ``rms_norm`` semantics: fp32 variance,
7 bf16-rounded weight multiply; exact-erf GELU; fp32-accumulated embedding sum).
8 The fused kernels keep the same accumulation dtype and rounding points, so
9 outputs differ from the reference only by reduction order — a few bf16 ulps.
10 """
11
12 import pytest
13 import torch
14 import torch.nn.functional as F
15
16 from vllm.platforms import current_platform
17
18 if not current_platform.is_cuda():
19 pytest.skip("requires CUDA", allow_module_level=True)
20
21 from vllm.models.inkling.common.towers import fold_timespace_to_depth
22 from vllm.models.inkling.nvidia.ops.mm_towers import dmel_embed_sum_norm, rmsnorm_gelu
23
24 DTYPE = torch.bfloat16
25
26
27 def _bf16_spacing(x: torch.Tensor) -> torch.Tensor:
28 return torch.exp2(torch.floor(torch.log2(x.float().abs().clamp(min=1e-30)))) * (
29 2**-7
30 )
31
32
33 def _assert_close_ulps(
34 got: torch.Tensor,
35 ref: torch.Tensor,
36 max_ulps: float = 4.0,
37 atol: float = 1e-3,
38 pre_act: torch.Tensor | None = None,
39 ) -> None:
40 """Fused vs reference differ only by fp32 reduction order, which shows up
41 as a few bf16 ulps at any magnitude — a fixed rtol misrepresents that, so
42 compare against the reference's local ulp spacing. ``pre_act`` (the
43 reference pre-activation) adds derivative-propagated slack: near
44 ``gelu(x) ~ 0`` an ulp-level input flip legitimately moves the output by
45 many of ITS (tiny) ulps, bounded by |gelu'| <= 1.13 times the input ulps."""
46 g, r = got.float(), ref.float()
47 tol = torch.clamp(max_ulps * _bf16_spacing(ref), min=atol)
48 if pre_act is not None:
49 tol = tol + 2.5 * _bf16_spacing(pre_act)
50 bad = (g - r).abs() > tol
51 assert not bad.any(), (
52 f"{int(bad.sum())}/{ref.numel()} elements beyond tolerance; "
53 f"max abs diff {(g - r).abs().max().item():.3e}"
54 )
55
56
57 def _ref_rms_norm(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
58 # Matches ir.ops.rms_norm: fp32 normalize, bf16 weight multiply.
59 x32 = x.float()
60 var = x32.pow(2).mean(dim=-1, keepdim=True)
61 xn = x32 * torch.rsqrt(var + eps)
62 return (xn.to(weight.dtype) * weight).to(x.dtype)
63
64
65 @pytest.mark.parametrize("rows", [1, 7, 1200, 38400])
66 @pytest.mark.parametrize("dim", [128, 320, 4800, 6144])
67 @pytest.mark.parametrize("gelu", [True, False])
68 def test_rmsnorm_gelu(rows: int, dim: int, gelu: bool) -> None:
69 torch.manual_seed(rows * dim)
70 x = torch.randn(rows, dim, device="cuda", dtype=DTYPE)
71 w = torch.randn(dim, device="cuda", dtype=DTYPE)
72
73 h = _ref_rms_norm(x, w, 1e-5)
74 ref = F.gelu(h) if gelu else h
75 got = rmsnorm_gelu(x, w, 1e-5, gelu=gelu)
76
77 _assert_close_ulps(got, ref, pre_act=h if gelu else None)
78
79
80 @pytest.mark.parametrize("n", [1, 5, 64])
81 @pytest.mark.parametrize(
82 "shape,fold",
83 [
84 ((2, 8, 8, 128), (1, 2)), # the real L0 -> L1 vision transition
85 ((2, 4, 4, 320), (1, 2)),
86 ((2, 8, 8, 128), (2, 2)), # temporal + spatial fold
87 ],
88 )
89 def test_rmsnorm_gelu_folded_store(
90 n: int, shape: tuple[int, ...], fold: tuple[int, int]
91 ) -> None:
92 torch.manual_seed(n)
93 x = torch.randn(n, *shape, device="cuda", dtype=DTYPE)
94 w = torch.randn(shape[-1], device="cuda", dtype=DTYPE)
95
96 plain = rmsnorm_gelu(x, w, 1e-5, gelu=True)
97 ref = fold_timespace_to_depth(plain, *fold)
98 got = rmsnorm_gelu(x, w, 1e-5, gelu=True, fold=fold)
99
100 # The folded store is a pure permutation of the unfolded output.
101 assert got.shape == ref.shape
102 torch.testing.assert_close(got, ref, rtol=0, atol=0)
103
104
105 @pytest.mark.parametrize("num_frames", [1, 3, 100, 4097])
106 @pytest.mark.parametrize("with_norm", [True, False])
107 def test_dmel_embed_sum_norm(num_frames: int, with_norm: bool) -> None:
108 torch.manual_seed(num_frames)
109 n_bins, vocab, dim = 80, 16, 6144
110 idx = torch.randint(
111 0, vocab, (num_frames, n_bins), device="cuda", dtype=torch.int32
112 )
113 table = torch.randn(n_bins * vocab, dim, device="cuda", dtype=DTYPE)
114 norm_w = torch.randn(dim, device="cuda", dtype=DTYPE)
115
116 flat = (torch.arange(n_bins, device="cuda", dtype=torch.int32) * vocab).unsqueeze(
117 0
118 ) + idx
119 ref = (
120 F.embedding(flat.reshape(-1).long(), table)
121 .reshape(num_frames, n_bins, dim)
122 .sum(dim=1)
123 )
124 if with_norm:
125 ref = _ref_rms_norm(ref, norm_w, 1e-6)
126
127 got = dmel_embed_sum_norm(idx, table, norm_w if with_norm else None, 1e-6)
128 _assert_close_ulps(got, ref)
129
130
131 def test_empty_inputs() -> None:
132 x = torch.empty(0, 320, device="cuda", dtype=DTYPE)
133 w = torch.randn(320, device="cuda", dtype=DTYPE)
134 assert rmsnorm_gelu(x, w, 1e-5).shape == (0, 320)
135
136 idx = torch.empty(0, 80, device="cuda", dtype=torch.int32)
137 table = torch.randn(1280, 6144, device="cuda", dtype=DTYPE)
138 assert dmel_embed_sum_norm(idx, table, None, 0.0).shape == (0, 6144)