main
py 302 lines 9.21 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import importlib
5
6 import pytest
7 import torch
8 import torch.nn.functional as F
9
10 from vllm.models.kimi_k3.common.mtp import fused_mtp_input
11 from vllm.models.kimi_k3.nvidia.ops import attn_res
12 from vllm.platforms import current_platform
13
14 HIDDEN_SIZE = 7168
15 MAX_BLOCKS = 8
16 EPS = 1e-5
17 attn_res_module = importlib.import_module("vllm.models.kimi_k3.nvidia.ops.attn_res")
18
19
20 def _on_rocm_below_10() -> bool:
21 if not current_platform.is_rocm():
22 return False
23 from vllm.platforms.rocm import get_rocm_version
24
25 return (get_rocm_version() or (0,)) < (10,)
26
27
28 # The Triton bundled with ROCm < 10 (3.7.x) crashes in the AMD
29 # CanonicalizePointers pass on the kernel's tl.where over pointer tensors. Only
30 # the num_blocks > 0 loop contains it. ROCm serves AttnRes from
31 # vllm/models/kimi_k3/amd/ops/attn_res.py instead, so nothing real is lost.
32 _OLD_ROCM = _on_rocm_below_10()
33
34
35 def _skip_on_old_rocm(num_blocks: int) -> None:
36 if _OLD_ROCM and num_blocks > 0:
37 pytest.skip("Triton on ROCm < 10 cannot compile this kernel's pointer select")
38
39
40 def _randn_with_row_padding(*shape: int, padding: int = 0) -> torch.Tensor:
41 storage = torch.randn(
42 *shape[:-1],
43 shape[-1] + padding,
44 device="cuda",
45 dtype=torch.bfloat16,
46 )
47 return storage[..., : shape[-1]]
48
49
50 def _reference(
51 prefix: torch.Tensor,
52 delta: torch.Tensor | None,
53 blocks: torch.Tensor,
54 norm_weight: torch.Tensor,
55 qk_weight: torch.Tensor,
56 output_norm_weight: torch.Tensor | None,
57 num_blocks: int,
58 ) -> tuple[torch.Tensor, torch.Tensor]:
59 if delta is not None:
60 prefix = prefix + delta
61 values = torch.cat((blocks[:, :num_blocks], prefix.unsqueeze(1)), dim=1)
62 keys = F.rms_norm(values, (HIDDEN_SIZE,), norm_weight, EPS)
63 probs = (keys @ qk_weight).softmax(dim=-1)
64 output = torch.matmul(probs.unsqueeze(1), values).squeeze(1)
65 if output_norm_weight is not None:
66 output = F.rms_norm(output, (HIDDEN_SIZE,), output_norm_weight, EPS)
67 return output, prefix
68
69
70 @pytest.mark.parametrize(
71 (
72 "num_tokens",
73 "num_blocks",
74 "row_padding",
75 "write_block",
76 "has_delta",
77 "backend",
78 ),
79 [
80 pytest.param(1, 0, 0, True, False, "triton", id="triton-empty"),
81 pytest.param(1, 0, 0, True, True, "triton", id="triton-empty-add"),
82 pytest.param(17, 5, 7, True, False, "triton", id="triton-write"),
83 pytest.param(17, 5, 7, True, True, "triton", id="triton-write-add"),
84 pytest.param(3, 8, 0, False, False, "triton", id="triton-full"),
85 pytest.param(3, 8, 0, False, True, "triton", id="triton-full-add"),
86 pytest.param(1, 4, 0, False, True, "nvidia", id="nvidia-single-token"),
87 pytest.param(320, 1, 0, False, True, "nvidia", id="nvidia-1"),
88 pytest.param(320, 4, 0, False, True, "nvidia", id="nvidia-4"),
89 pytest.param(320, 8, 0, False, True, "nvidia", id="nvidia-8"),
90 ],
91 )
92 def test_attn_res(
93 num_tokens: int,
94 num_blocks: int,
95 row_padding: int,
96 write_block: bool,
97 has_delta: bool,
98 backend: str,
99 ):
100 if backend == "nvidia" and not current_platform.is_device_capability_family(100):
101 pytest.skip("NVIDIA AttnRes requires the SM100 family")
102 _skip_on_old_rocm(num_blocks)
103
104 prefix = _randn_with_row_padding(num_tokens, HIDDEN_SIZE, padding=row_padding)
105 delta = (
106 _randn_with_row_padding(num_tokens, HIDDEN_SIZE, padding=row_padding)
107 if has_delta
108 else None
109 )
110 blocks = _randn_with_row_padding(
111 num_tokens, MAX_BLOCKS, HIDDEN_SIZE, padding=row_padding
112 )
113 norm_weight = 1 + 0.1 * torch.randn(
114 HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
115 )
116 qk_weight = (
117 torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) / HIDDEN_SIZE**0.5
118 )
119 output_norm_weight = 1 + 0.1 * torch.randn(
120 HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
121 )
122 original_blocks = blocks.clone()
123 expected, expected_prefix = _reference(
124 prefix.clone(),
125 delta,
126 blocks,
127 norm_weight,
128 qk_weight,
129 output_norm_weight,
130 num_blocks,
131 )
132 block_write_idx = num_blocks if write_block else -1
133
134 actual = attn_res(
135 prefix,
136 delta,
137 blocks,
138 norm_weight,
139 qk_weight,
140 output_norm_weight,
141 num_blocks,
142 block_write_idx,
143 EPS,
144 EPS,
145 )
146
147 torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
148 torch.testing.assert_close(prefix, expected_prefix, atol=0, rtol=0)
149 if write_block:
150 original_blocks[:, block_write_idx].copy_(expected_prefix)
151 torch.testing.assert_close(blocks, original_blocks, atol=0, rtol=0)
152 assert actual.is_contiguous()
153
154
155 @pytest.mark.parametrize("num_blocks", range(MAX_BLOCKS + 1))
156 def test_attn_res_block_counts(num_blocks: int):
157 _skip_on_old_rocm(num_blocks)
158 prefix = torch.randn(1, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
159 blocks = torch.randn(
160 1, MAX_BLOCKS, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
161 )
162 norm_weight = torch.ones(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
163 qk_weight = (
164 torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) / HIDDEN_SIZE**0.5
165 )
166 output_norm_weight = torch.ones_like(norm_weight)
167 expected, _ = _reference(
168 prefix.clone(),
169 None,
170 blocks,
171 norm_weight,
172 qk_weight,
173 output_norm_weight,
174 num_blocks,
175 )
176
177 actual = attn_res(
178 prefix,
179 None,
180 blocks,
181 norm_weight,
182 qk_weight,
183 output_norm_weight,
184 num_blocks,
185 -1,
186 EPS,
187 EPS,
188 )
189
190 torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
191
192
193 def test_attn_res_without_output_norm():
194 _skip_on_old_rocm(MAX_BLOCKS)
195 prefix = torch.randn(7, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
196 delta = torch.randn_like(prefix)
197 blocks = torch.randn(
198 7, MAX_BLOCKS, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
199 )
200 norm_weight = torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
201 qk_weight = (
202 torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) / HIDDEN_SIZE**0.5
203 )
204 expected, _ = _reference(
205 prefix.clone(), delta, blocks, norm_weight, qk_weight, None, MAX_BLOCKS
206 )
207
208 actual = attn_res(
209 prefix,
210 delta,
211 blocks,
212 norm_weight,
213 qk_weight,
214 None,
215 MAX_BLOCKS,
216 -1,
217 EPS,
218 0.0,
219 )
220
221 torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
222
223
224 @pytest.mark.parametrize(
225 ("num_blocks", "has_delta", "has_output_norm", "write_block"),
226 [
227 (0, False, True, True),
228 (4, True, True, True),
229 (4, False, True, False),
230 (8, True, False, False),
231 ],
232 )
233 def test_sm100_variants_do_not_fall_back_to_triton(
234 monkeypatch,
235 num_blocks: int,
236 has_delta: bool,
237 has_output_norm: bool,
238 write_block: bool,
239 ):
240 if not current_platform.is_device_capability_family(100) or not hasattr(
241 torch.ops._C, "kimi_k3_attn_res"
242 ):
243 pytest.skip("native AttnRes requires an SM100 build")
244
245 class FailingKernel:
246 def __getitem__(self, grid):
247 raise AssertionError(f"unexpected Triton launch with grid {grid}")
248
249 monkeypatch.setattr(attn_res_module, "_attn_res_kernel", FailingKernel())
250 prefix = torch.randn(1, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
251 delta = torch.zeros_like(prefix) if has_delta else None
252 blocks = torch.randn(
253 1, MAX_BLOCKS, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
254 )
255 norm_weight = torch.ones(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
256 qk_weight = torch.randn_like(norm_weight) / HIDDEN_SIZE**0.5
257 output_norm_weight = torch.ones_like(norm_weight) if has_output_norm else None
258
259 attn_res(
260 prefix,
261 delta,
262 blocks,
263 norm_weight,
264 qk_weight,
265 output_norm_weight,
266 num_blocks,
267 num_blocks if write_block else -1,
268 EPS,
269 EPS,
270 )
271
272
273 @pytest.mark.parametrize("num_tokens", [0, 1, 17])
274 def test_fused_mtp_input(num_tokens: int):
275 positions = torch.arange(num_tokens, device="cuda")
276 inputs_embeds = _randn_with_row_padding(num_tokens, HIDDEN_SIZE, padding=7)
277 previous_hidden_states = _randn_with_row_padding(
278 num_tokens, HIDDEN_SIZE, padding=11
279 )
280 enorm_weight = torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
281 hnorm_weight = torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
282
283 masked_inputs_embeds = torch.where(positions.unsqueeze(-1) == 0, 0, inputs_embeds)
284 expected = torch.cat(
285 (
286 F.rms_norm(masked_inputs_embeds, (HIDDEN_SIZE,), enorm_weight, EPS),
287 F.rms_norm(previous_hidden_states, (HIDDEN_SIZE,), hnorm_weight, EPS),
288 ),
289 dim=-1,
290 )
291 actual = fused_mtp_input(
292 positions,
293 inputs_embeds,
294 previous_hidden_states,
295 enorm_weight,
296 hnorm_weight,
297 EPS,
298 )
299
300 torch.testing.assert_close(actual, expected, atol=2e-2, rtol=2e-2)
301 assert actual.shape == (num_tokens, 2 * HIDDEN_SIZE)
302 assert actual.is_contiguous()