main
py 553 lines 16.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 import torch
6 import torch.nn.functional as F
7
8 from vllm.platforms import current_platform
9 from vllm.third_party.flash_linear_attention.ops.layernorm_guard import (
10 LayerNormFwdKernel,
11 calc_rows_per_block,
12 layer_norm_fwd,
13 layernorm_fn,
14 rms_norm_ref,
15 )
16 from vllm.triton_utils import triton
17 from vllm.utils.torch_utils import set_random_seed
18
19 DEVICE = "xpu" if current_platform.is_xpu() else "cuda"
20
21
22 def layer_norm_ref(
23 x,
24 weight,
25 bias,
26 z=None,
27 eps=1e-6,
28 group_size=None,
29 norm_before_gate=True,
30 is_rms_norm=False,
31 ):
32 """Reference implementation for both layer norm and RMS norm."""
33 if is_rms_norm:
34 # Use the imported rms_norm_ref for RMS norm cases
35 return rms_norm_ref(
36 x,
37 weight,
38 bias,
39 z=z,
40 eps=eps,
41 group_size=group_size,
42 norm_before_gate=norm_before_gate,
43 upcast=True,
44 )
45
46 # Layer norm implementation
47 dtype = x.dtype
48 x = x.float()
49 weight = weight.float()
50 bias = bias.float() if bias is not None else None
51 z = z.float() if z is not None else None
52
53 if z is not None and not norm_before_gate:
54 x = x * F.silu(z)
55
56 if group_size is None:
57 # Layer norm: subtract mean
58 mean = x.mean(dim=-1, keepdim=True)
59 var = ((x - mean).square()).mean(dim=-1, keepdim=True)
60 rstd = 1 / torch.sqrt(var + eps)
61 out = (x - mean) * rstd * weight
62 if bias is not None:
63 out = out + bias
64 else:
65 # Group norm
66 from einops import rearrange
67
68 x_group = rearrange(x, "... (g d) -> ... g d", d=group_size)
69 mean = x_group.mean(dim=-1, keepdim=True)
70 var = ((x_group - mean).square()).mean(dim=-1, keepdim=True)
71 rstd = 1 / torch.sqrt(var + eps)
72 x_group = (x_group - mean) * rstd
73 out = rearrange(x_group, "... g d -> ... (g d)") * weight
74 if bias is not None:
75 out = out + bias
76
77 if z is not None and norm_before_gate:
78 out *= F.silu(z)
79
80 return out.to(dtype)
81
82
83 DTYPES = [torch.float16, torch.bfloat16, torch.float32]
84 # Test various M sizes to ensure rows_per_block logic works correctly
85 NUM_TOKENS = [
86 1,
87 7,
88 16,
89 63,
90 128,
91 256,
92 512,
93 1024,
94 2048,
95 4096,
96 5789,
97 8189,
98 8191,
99 16383,
100 32767,
101 ]
102 HIDDEN_SIZES = [64, 128, 256, 1024]
103 GROUP_SIZES = [None, 64, 128] # None means full hidden size
104 NORM_BEFORE_GATE = [True, False]
105 IS_RMS_NORM = [True, False]
106 SEEDS = [0, 42]
107
108
109 @pytest.mark.parametrize("rows_per_token", [1, 2, 4, 8, 16])
110 def test_layer_norm_fwd_warmup_keys_cover_qwen_gdn(
111 rows_per_token: int,
112 ) -> None:
113 device = torch.device(DEVICE)
114 group_size = 128
115 max_num_tokens = 512
116 kernel = LayerNormFwdKernel()
117 warmup_keys = set(
118 kernel.get_warmup_keys(
119 max_num_tokens=max_num_tokens,
120 rows_per_token=rows_per_token,
121 group_size=group_size,
122 x_dtype=torch.bfloat16,
123 weight_dtype=torch.bfloat16,
124 device=device,
125 norm_before_gate=True,
126 is_rms_norm=True,
127 activation="silu",
128 )
129 )
130
131 for num_tokens in range(1, max_num_tokens + 1):
132 num_rows = num_tokens * rows_per_token
133 runtime_key = kernel.dispatch(
134 x_dtype=torch.bfloat16,
135 y_dtype=torch.bfloat16,
136 weight_dtype=torch.bfloat16,
137 bias_dtype=None,
138 z_dtype=torch.bfloat16,
139 mean_dtype=None,
140 rstd_dtype=torch.float32,
141 x_aligned=True,
142 y_aligned=True,
143 weight_aligned=True,
144 bias_aligned=True,
145 z_aligned=True,
146 mean_aligned=True,
147 rstd_aligned=True,
148 stride_x_row=group_size,
149 stride_y_row=group_size,
150 stride_z_row=group_size,
151 M=num_rows,
152 N=group_size,
153 BLOCK_N=triton.next_power_of_2(group_size),
154 ROWS_PER_BLOCK=calc_rows_per_block(num_rows, device),
155 norm_before_gate=True,
156 is_rms_norm=True,
157 activation="silu",
158 )
159 assert runtime_key in warmup_keys
160
161
162 @pytest.mark.parametrize("num_tokens", NUM_TOKENS)
163 @pytest.mark.parametrize("hidden_size", HIDDEN_SIZES)
164 @pytest.mark.parametrize("dtype", DTYPES)
165 @pytest.mark.parametrize("seed", SEEDS)
166 @pytest.mark.parametrize("is_rms_norm", IS_RMS_NORM)
167 @torch.inference_mode()
168 def test_layer_norm_fwd_basic(
169 num_tokens: int,
170 hidden_size: int,
171 dtype: torch.dtype,
172 seed: int,
173 is_rms_norm: bool,
174 ) -> None:
175 """Test basic layer norm forward pass without z (gate) tensor."""
176 set_random_seed(seed)
177 device = torch.device(DEVICE)
178
179 # Create inputs
180 x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
181 weight = torch.randn(hidden_size, dtype=dtype, device=device)
182 bias = None if is_rms_norm else torch.randn(hidden_size, dtype=dtype, device=device)
183 eps = 1e-6
184
185 # Run the triton kernel
186 out, mean, rstd = layer_norm_fwd(
187 x, weight, bias, eps, z=None, is_rms_norm=is_rms_norm
188 )
189
190 # Run reference implementation
191 ref_out = layer_norm_ref(x, weight, bias, z=None, eps=eps, is_rms_norm=is_rms_norm)
192
193 # Check outputs
194 assert out.shape == x.shape
195 assert out.dtype == x.dtype
196 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
197
198 # Check mean and rstd shapes
199 if not is_rms_norm:
200 assert mean.shape == (num_tokens,)
201 assert rstd.shape == (num_tokens,)
202
203
204 @pytest.mark.parametrize("num_tokens", NUM_TOKENS)
205 @pytest.mark.parametrize("hidden_size", [128, 256, 1024])
206 @pytest.mark.parametrize("dtype", DTYPES)
207 @pytest.mark.parametrize("norm_before_gate", NORM_BEFORE_GATE)
208 @pytest.mark.parametrize("is_rms_norm", IS_RMS_NORM)
209 @torch.inference_mode()
210 def test_layer_norm_fwd_with_gate(
211 num_tokens: int,
212 hidden_size: int,
213 dtype: torch.dtype,
214 norm_before_gate: bool,
215 is_rms_norm: bool,
216 ) -> None:
217 """Test layer norm forward pass with z (gate) tensor."""
218 set_random_seed(42)
219 device = torch.device(DEVICE)
220
221 # Create inputs
222 x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
223 z = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
224 weight = torch.randn(hidden_size, dtype=dtype, device=device)
225 bias = None if is_rms_norm else torch.randn(hidden_size, dtype=dtype, device=device)
226 eps = 1e-6
227
228 # Run the triton kernel
229 out, mean, rstd = layer_norm_fwd(
230 x,
231 weight,
232 bias,
233 eps,
234 z=z,
235 norm_before_gate=norm_before_gate,
236 is_rms_norm=is_rms_norm,
237 )
238
239 # Run reference implementation
240 ref_out = layer_norm_ref(
241 x,
242 weight,
243 bias,
244 z=z,
245 eps=eps,
246 norm_before_gate=norm_before_gate,
247 is_rms_norm=is_rms_norm,
248 )
249
250 # Check outputs
251 assert out.shape == x.shape
252 assert out.dtype == x.dtype
253 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
254
255
256 @pytest.mark.parametrize("num_tokens", [128, 512])
257 @pytest.mark.parametrize("hidden_size", [512, 1024])
258 @pytest.mark.parametrize("group_size", [64, 128, 256])
259 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
260 @pytest.mark.parametrize("is_rms_norm", IS_RMS_NORM)
261 @torch.inference_mode()
262 def test_layer_norm_fwd_with_groups(
263 num_tokens: int,
264 hidden_size: int,
265 group_size: int,
266 dtype: torch.dtype,
267 is_rms_norm: bool,
268 ) -> None:
269 """Test layer norm forward pass with group normalization."""
270 if hidden_size % group_size != 0:
271 pytest.skip(
272 f"hidden_size {hidden_size} not divisible by group_size {group_size}"
273 )
274
275 set_random_seed(42)
276 device = torch.device(DEVICE)
277
278 # Create inputs
279 x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
280 weight = torch.randn(hidden_size, dtype=dtype, device=device)
281 bias = None if is_rms_norm else torch.randn(hidden_size, dtype=dtype, device=device)
282 eps = 1e-6
283
284 ngroups = hidden_size // group_size
285
286 # Run the triton kernel
287 out, mean, rstd = layer_norm_fwd(
288 x, weight, bias, eps, z=None, group_size=group_size, is_rms_norm=is_rms_norm
289 )
290
291 # Run reference implementation
292 ref_out = layer_norm_ref(
293 x, weight, bias, z=None, eps=eps, group_size=group_size, is_rms_norm=is_rms_norm
294 )
295
296 # Check outputs
297 assert out.shape == x.shape
298 assert out.dtype == x.dtype
299 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
300
301 # Check mean and rstd shapes for groups
302 if not is_rms_norm:
303 assert mean.shape == (ngroups * num_tokens,)
304 assert rstd.shape == (ngroups * num_tokens,)
305
306
307 @pytest.mark.parametrize("num_tokens", [7, 63, 128, 513, 1024, 2049])
308 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
309 @torch.inference_mode()
310 def test_layer_norm_rows_per_block(
311 num_tokens: int,
312 dtype: torch.dtype,
313 ) -> None:
314 """Test that rows_per_block logic works correctly for various M sizes."""
315 set_random_seed(42)
316 device = torch.device(DEVICE)
317 hidden_size = 1024
318
319 # Create inputs
320 x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
321 weight = torch.randn(hidden_size, dtype=dtype, device=device)
322 bias = torch.randn(hidden_size, dtype=dtype, device=device)
323 eps = 1e-6
324
325 # Run the triton kernel
326 out, mean, rstd = layer_norm_fwd(x, weight, bias, eps, z=None, is_rms_norm=False)
327
328 # Run reference implementation
329 ref_out = layer_norm_ref(x, weight, bias, z=None, eps=eps, is_rms_norm=False)
330
331 # Check outputs
332 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
333
334
335 @pytest.mark.parametrize("dtype", [torch.bfloat16])
336 @torch.inference_mode()
337 def test_strided_input(dtype: torch.dtype) -> None:
338 """Test that the kernel handles non-contiguous (strided)
339 inputs correctly."""
340 set_random_seed(42)
341 device = torch.device(DEVICE)
342 num_tokens = 128
343 hidden_size = 1024
344
345 # Create a larger tensor and take a strided slice
346 x_large = torch.randn(num_tokens, hidden_size * 2, dtype=dtype, device=device)
347 x = x_large[:, :hidden_size]
348
349 # Make it contiguous for the kernel
350 x_contiguous = x.contiguous()
351
352 weight = torch.randn(hidden_size, dtype=dtype, device=device)
353 bias = torch.randn(hidden_size, dtype=dtype, device=device)
354 eps = 1e-6
355
356 # Run the triton kernel with contiguous input
357 out, mean, rstd = layer_norm_fwd(
358 x_contiguous, weight, bias, eps, z=None, is_rms_norm=False
359 )
360
361 # Run reference implementation
362 ref_out = layer_norm_ref(
363 x_contiguous, weight, bias, z=None, eps=eps, is_rms_norm=False
364 )
365
366 # Check outputs
367 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
368
369
370 @pytest.mark.parametrize("num_tokens", [1, 128, 2048])
371 @pytest.mark.parametrize("hidden_size", [768, 4096])
372 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
373 @torch.inference_mode()
374 def test_output_buffer_provided(
375 num_tokens: int,
376 hidden_size: int,
377 dtype: torch.dtype,
378 ) -> None:
379 """Test that the kernel works when an output buffer is provided."""
380 set_random_seed(42)
381 device = torch.device(DEVICE)
382
383 # Create inputs
384 x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
385 weight = torch.randn(hidden_size, dtype=dtype, device=device)
386 bias = torch.randn(hidden_size, dtype=dtype, device=device)
387 eps = 1e-6
388
389 # Pre-allocate output buffer
390 out_buffer = torch.empty_like(x)
391
392 # Run the triton kernel with provided output
393 out, mean, rstd = layer_norm_fwd(
394 x, weight, bias, eps, z=None, out=out_buffer, is_rms_norm=False
395 )
396
397 # Check that the provided buffer was used
398 assert out.data_ptr() == out_buffer.data_ptr()
399
400 # Run reference implementation
401 ref_out = layer_norm_ref(x, weight, bias, z=None, eps=eps, is_rms_norm=False)
402
403 # Check outputs
404 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
405
406
407 @pytest.mark.parametrize(
408 "shape",
409 [
410 (4, 16, 1024), # 3D tensor
411 (2, 8, 512, 256), # 4D tensor
412 ],
413 )
414 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
415 @torch.inference_mode()
416 def test_multidimensional_input(
417 shape: tuple,
418 dtype: torch.dtype,
419 ) -> None:
420 """Test that the autograd function handles multidimensional inputs."""
421 set_random_seed(42)
422 device = torch.device(DEVICE)
423 hidden_size = shape[-1]
424
425 # Create inputs
426 x = torch.randn(*shape, dtype=dtype, device=device)
427 weight = torch.randn(hidden_size, dtype=dtype, device=device)
428 bias = torch.randn(hidden_size, dtype=dtype, device=device)
429 eps = 1e-6
430
431 # Run through autograd function
432 out = layernorm_fn(x, weight, bias, z=None, eps=eps)
433
434 # Run reference implementation
435 ref_out = layer_norm_ref(x, weight, bias, z=None, eps=eps, is_rms_norm=False)
436
437 # Check outputs
438 assert out.shape == x.shape
439 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
440
441
442 @pytest.mark.parametrize("num_tokens", [1, 128, 1024])
443 @pytest.mark.parametrize("hidden_size", [64, 256, 1024])
444 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
445 @pytest.mark.parametrize("has_gate", [True, False])
446 @pytest.mark.parametrize("group_size", [None, 64])
447 @pytest.mark.parametrize("norm_before_gate", [True, False])
448 @torch.inference_mode()
449 def test_rmsnorm_gated_forward_native_dtype(
450 default_vllm_config,
451 num_tokens: int,
452 hidden_size: int,
453 dtype: torch.dtype,
454 has_gate: bool,
455 group_size: int | None,
456 norm_before_gate: bool,
457 ):
458 """Test that RMSNormGated.forward_native preserves input dtype."""
459 if group_size is not None and hidden_size % group_size != 0:
460 pytest.skip(
461 f"hidden_size {hidden_size} not divisible by group_size {group_size}"
462 )
463
464 from vllm.model_executor.layers.layernorm import RMSNormGated
465
466 device = torch.device(DEVICE)
467 set_random_seed(42)
468
469 layer = RMSNormGated(
470 hidden_size,
471 eps=1e-5,
472 group_size=group_size,
473 norm_before_gate=norm_before_gate,
474 device=device,
475 dtype=dtype,
476 )
477
478 x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
479 z = (
480 torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
481 if has_gate
482 else None
483 )
484
485 out = layer.forward_native(x, z)
486
487 # Verify dtype preservation
488 assert out.dtype == dtype, f"Expected {dtype}, got {out.dtype}"
489
490 # Verify numerical correctness against reference
491 ref_out = rms_norm_ref(
492 x,
493 layer.weight,
494 layer.bias,
495 z=z,
496 eps=1e-5,
497 group_size=group_size,
498 norm_before_gate=norm_before_gate,
499 upcast=True,
500 )
501 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
502
503
504 @pytest.mark.parametrize("dtype", [torch.bfloat16])
505 @torch.inference_mode()
506 def test_rmsnorm_gated_3d_matches_flattened_2d(
507 default_vllm_config,
508 dtype: torch.dtype,
509 ) -> None:
510 """Qwen GDN now norms (N, H, D) in place of a flatten to (N*H, D).
511
512 Last-dim RMS + gate must match the flattened layout on both the native
513 and CUDA/HIP kernels, including a 3D z view that is not contiguous.
514 """
515 from vllm.model_executor.layers.layernorm import RMSNormGated
516
517 set_random_seed(42)
518 device = torch.device(DEVICE)
519 num_tokens, num_heads, head_dim = 5, 4, 64
520 layer = RMSNormGated(
521 head_dim,
522 eps=1e-5,
523 group_size=None,
524 norm_before_gate=True,
525 device=device,
526 dtype=dtype,
527 )
528
529 packed = torch.randn(num_tokens, num_heads, 2, head_dim, dtype=dtype, device=device)
530 x3 = packed[:, :, 0, :]
531 z3 = packed[:, :, 1, :]
532 assert not x3.is_contiguous()
533 assert not z3.is_contiguous()
534 x2 = x3.reshape(-1, head_dim).contiguous()
535 z2 = z3.reshape(-1, head_dim).contiguous()
536
537 native_3d = layer.forward_native(x3, z3)
538 native_2d = layer.forward_native(x2, z2).reshape(num_tokens, num_heads, head_dim)
539 torch.testing.assert_close(native_3d, native_2d, atol=0, rtol=0)
540
541 cuda_3d = layer.forward_cuda(x3, z3)
542 cuda_2d = layer.forward_cuda(x2, z2).reshape(num_tokens, num_heads, head_dim)
543 torch.testing.assert_close(cuda_3d, cuda_2d, atol=1e-3, rtol=1e-2)
544 torch.testing.assert_close(cuda_3d, native_3d, atol=1e-2, rtol=1e-2)
545 assert cuda_3d.shape == (num_tokens, num_heads, head_dim)
546
547
548 if __name__ == "__main__":
549 # Run a quick smoke test
550 test_layer_norm_fwd_basic(128, 1024, torch.float16, 42, False)
551 test_layer_norm_fwd_with_gate(128, 1024, torch.float16, True, False)
552 test_layer_norm_rows_per_block(513, torch.float16)
553 print("All smoke tests passed!")