main
py 320 lines 10.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for MiniMax QK RMS-norm fused all-reduce kernels and fallbacks."""
4
5 from unittest.mock import patch
6
7 import pytest
8 import torch
9 import torch.nn as nn
10 from torch.multiprocessing import spawn
11
12 from tests.kernels.utils import opcheck
13 from tests.utils import ensure_current_vllm_config, init_test_distributed_environment
14 from vllm._aiter_ops import is_aiter_found_and_supported, rocm_aiter_ops
15 from vllm.distributed import cleanup_dist_env_and_memory
16 from vllm.model_executor.layers.minimax_rms_norm import (
17 MiniMaxText01RMSNormTP,
18 rms_norm_tp,
19 )
20 from vllm.platforms import current_platform
21 from vllm.triton_utils import HAS_TRITON
22 from vllm.utils.network_utils import get_open_port
23 from vllm.utils.torch_utils import set_random_seed
24
25
26 @ensure_current_vllm_config()
27 def _worker_forward_qk(
28 local_rank,
29 world_size,
30 port,
31 num_tokens,
32 hidden_q_full,
33 hidden_k_full,
34 dtype,
35 seed,
36 eps,
37 ):
38 """Per-rank worker: compare NCCL allreduce path vs Lamport fused kernel."""
39 if not hasattr(torch.ops._C, "minimax_allreduce_rms_qk"):
40 cleanup_dist_env_and_memory()
41 return
42 device = torch.device(f"cuda:{local_rank}")
43 torch.accelerator.set_device_index(device)
44 init_test_distributed_environment(
45 world_size, 1, local_rank, port, local_rank=local_rank
46 )
47
48 hq = hidden_q_full // world_size
49 hk = hidden_k_full // world_size
50
51 q_norm = MiniMaxText01RMSNormTP(hidden_q_full, eps=eps).cuda()
52 k_norm = MiniMaxText01RMSNormTP(hidden_k_full, eps=eps).cuda()
53
54 set_random_seed(seed)
55 qw = torch.randn(hidden_q_full, dtype=dtype, device="cuda")
56 kw = torch.randn(hidden_k_full, dtype=dtype, device="cuda")
57 q_norm.weight = nn.Parameter(qw[local_rank * hq : (local_rank + 1) * hq])
58 k_norm.weight = nn.Parameter(kw[local_rank * hk : (local_rank + 1) * hk])
59
60 torch.manual_seed(seed + 1000 + local_rank)
61 qkv = torch.randn(num_tokens, hq + hk + hk, dtype=dtype, device="cuda")
62
63 # Reference: eager all-reduce path. ``forward_qk`` no longer all-reduces
64 # the variance (it is the tp==1 / already-reduced building block), so the
65 # multi-rank reference must use the eager path that performs the global
66 # variance all-reduce, matching the fused kernel below.
67 ref_q, ref_k = rms_norm_tp._minimax_qk_norm_tp_eager(
68 qkv.clone(),
69 q_norm.weight,
70 k_norm.weight,
71 hq,
72 hk,
73 world_size,
74 eps,
75 )
76
77 # Set up Lamport workspace.
78 from vllm.distributed.parallel_state import get_tp_group
79 from vllm.model_executor.layers.minimax_rms_norm.lamport_workspace import (
80 get_allreduce_workspace,
81 )
82
83 workspace = get_allreduce_workspace(
84 rank=local_rank,
85 world_size=world_size,
86 max_tokens=num_tokens,
87 process_group=get_tp_group().cpu_group,
88 )
89
90 opcheck(
91 torch.ops._C.minimax_allreduce_rms_qk,
92 (
93 qkv.clone(),
94 q_norm.weight,
95 k_norm.weight,
96 workspace,
97 hq,
98 hk,
99 local_rank,
100 world_size,
101 eps,
102 ),
103 )
104 fused_q, fused_k = torch.ops._C.minimax_allreduce_rms_qk(
105 qkv.clone(),
106 q_norm.weight,
107 k_norm.weight,
108 workspace,
109 hq,
110 hk,
111 local_rank,
112 world_size,
113 eps,
114 )
115 _, _, fused_v = qkv.split([hq, hk, hk], dim=-1)
116 torch.accelerator.synchronize()
117
118 torch.testing.assert_close(
119 fused_q,
120 ref_q,
121 atol=3e-2,
122 rtol=3e-2,
123 )
124 torch.testing.assert_close(fused_k, ref_k, atol=3e-2, rtol=3e-2)
125
126 cleanup_dist_env_and_memory()
127
128
129 @ensure_current_vllm_config()
130 def _worker_forward_qk_rocm_aiter(
131 local_rank: int,
132 world_size: int,
133 port: str,
134 hidden_q_full: int,
135 hidden_k_full: int,
136 seed: int,
137 eps: float,
138 ) -> None:
139 """Compare the ROCm AITER fusion with the eager TP implementation."""
140 device = torch.device(f"cuda:{local_rank}")
141 torch.accelerator.set_device_index(device)
142 init_test_distributed_environment(
143 world_size, 1, local_rank, port, local_rank=local_rank
144 )
145
146 try:
147 aiter_ar = rocm_aiter_ops.get_aiter_allreduce()
148 assert aiter_ar is not None, "AITER custom all-reduce was not initialized"
149 assert not aiter_ar.disabled, "AITER custom all-reduce is disabled"
150 assert hasattr(aiter_ar.aiter_ca, "custom_fused_qknorm_ar")
151
152 hq = hidden_q_full // world_size
153 hk = hidden_k_full // world_size
154 test_cases = (
155 (1, torch.bfloat16),
156 (128, torch.bfloat16),
157 (333, torch.bfloat16),
158 (1, torch.float16),
159 (128, torch.float16),
160 (333, torch.float16),
161 )
162
163 for num_tokens, dtype in test_cases:
164 q_norm = MiniMaxText01RMSNormTP(hidden_q_full, eps=eps).cuda()
165 k_norm = MiniMaxText01RMSNormTP(hidden_k_full, eps=eps).cuda()
166
167 set_random_seed(seed)
168 q_weight = torch.randn(hidden_q_full, dtype=dtype, device="cuda")
169 k_weight = torch.randn(hidden_k_full, dtype=dtype, device="cuda")
170 q_norm.weight = nn.Parameter(
171 q_weight[local_rank * hq : (local_rank + 1) * hq]
172 )
173 k_norm.weight = nn.Parameter(
174 k_weight[local_rank * hk : (local_rank + 1) * hk]
175 )
176
177 torch.manual_seed(seed + num_tokens + local_rank)
178 qkv = torch.randn(num_tokens, hq + hk + hk, dtype=dtype, device="cuda")
179
180 ref_q, ref_k = rms_norm_tp._minimax_qk_norm_tp_eager(
181 qkv.clone(),
182 q_norm.weight,
183 k_norm.weight,
184 hq,
185 hk,
186 world_size,
187 eps,
188 )
189
190 with (
191 patch.object(rms_norm_tp, "_MINIMAX_FUSED_AR_RMS_QK", None),
192 patch.object(
193 rms_norm_tp,
194 "_minimax_qk_norm_tp_fallback",
195 side_effect=AssertionError(
196 "ROCm AITER test unexpectedly used the fallback"
197 ),
198 ),
199 ):
200 fused_q, fused_k, fused_v = MiniMaxText01RMSNormTP.forward_qkv(
201 q_norm, k_norm, qkv.clone(), hq, hk
202 )
203
204 torch.accelerator.synchronize()
205 torch.testing.assert_close(fused_q, ref_q, atol=3e-2, rtol=3e-2)
206 torch.testing.assert_close(fused_k, ref_k, atol=3e-2, rtol=3e-2)
207 assert torch.equal(fused_v, qkv[:, hq + hk :])
208 finally:
209 cleanup_dist_env_and_memory()
210
211
212 @pytest.mark.distributed(num_gpus=2)
213 @pytest.mark.skipif(
214 not current_platform.is_rocm(),
215 reason="ROCm required",
216 )
217 def test_minimax_reduce_rms_qk_rocm_aiter(
218 monkeypatch: pytest.MonkeyPatch,
219 ) -> None:
220 """The MiniMax TP QK path dispatches to AITER and matches eager RMSNorm."""
221 assert is_aiter_found_and_supported(), "AITER requires ROCm CDNA3 or newer"
222
223 monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
224 monkeypatch.setenv("VLLM_ROCM_USE_AITER_CUSTOM_AR", "1")
225 monkeypatch.setenv("VLLM_ROCM_QUICK_REDUCE_QUANTIZATION", "NONE")
226
227 world_size = 2
228 if current_platform.device_count() < world_size:
229 pytest.skip("Need at least two GPUs")
230
231 spawn(
232 _worker_forward_qk_rocm_aiter,
233 args=(world_size, str(get_open_port()), 6144, 1024, 42, 1e-6),
234 nprocs=world_size,
235 join=True,
236 )
237
238
239 @pytest.mark.skipif(
240 not current_platform.is_cuda(),
241 reason="CUDA required",
242 )
243 @pytest.mark.parametrize("world_size", [2, 4, 8])
244 @pytest.mark.parametrize("num_tokens", [1, 128, 333])
245 @pytest.mark.parametrize(
246 "hidden_dims",
247 [(6144, 1024)],
248 )
249 @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
250 @pytest.mark.parametrize("eps", [1e-6])
251 @pytest.mark.parametrize("seed", [42])
252 def test_minimax_reduce_rms_qk(
253 world_size,
254 num_tokens,
255 hidden_dims,
256 dtype,
257 eps,
258 seed,
259 ):
260 num_gpus = current_platform.device_count()
261 if num_gpus < world_size:
262 pytest.skip(f"Need >= {world_size} GPUs, have {num_gpus}")
263 hidden_q_full, hidden_k_full = hidden_dims
264 port = str(get_open_port())
265 spawn(
266 _worker_forward_qk,
267 args=(
268 world_size,
269 port,
270 num_tokens,
271 hidden_q_full,
272 hidden_k_full,
273 dtype,
274 seed,
275 eps,
276 ),
277 nprocs=world_size,
278 join=True,
279 )
280
281
282 @pytest.mark.skipif(
283 not current_platform.is_cuda() or not HAS_TRITON,
284 reason="CUDA and Triton required",
285 )
286 @pytest.mark.parametrize("num_tokens", [1, 7, 128, 333, 2049])
287 @pytest.mark.parametrize("hidden_dims", [(3072, 512), (768, 256), (3000, 500)])
288 @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
289 @pytest.mark.parametrize("tp_world", [1, 4, 8])
290 @pytest.mark.parametrize("eps", [1e-6])
291 @pytest.mark.parametrize("seed", [42])
292 def test_minimax_qk_norm_triton_fallback(
293 monkeypatch, num_tokens, hidden_dims, dtype, tp_world, eps, seed
294 ):
295 """Single-GPU check: Triton fallback kernels vs the pure-torch reference.
296
297 The all-reduce is a TP communication barrier, so it is monkeypatched to
298 identity here; both the Triton path and the reference see the same
299 (patched) reduction. This validates the kernel math and the folded
300 ``/ tp_world`` scaling without needing multiple ranks -- ``hidden_dims``
301 are the per-rank q/k segment widths.
302 """
303 monkeypatch.setattr(rms_norm_tp, "_all_reduce_variance", lambda v: v)
304
305 q_size, kv_size = hidden_dims
306 device = "cuda"
307 torch.manual_seed(seed)
308 qkv = torch.randn(num_tokens, q_size + 2 * kv_size, dtype=dtype, device=device)
309 q_weight = torch.randn(q_size, dtype=dtype, device=device)
310 k_weight = torch.randn(kv_size, dtype=dtype, device=device)
311
312 q_triton, k_triton = rms_norm_tp._minimax_qk_norm_tp_fallback(
313 qkv, q_weight, k_weight, q_size, kv_size, 0, tp_world, eps
314 )
315 q_ref, k_ref = rms_norm_tp._minimax_qk_norm_tp_eager(
316 qkv, q_weight, k_weight, q_size, kv_size, tp_world, eps
317 )
318
319 torch.testing.assert_close(q_triton, q_ref, atol=3e-2, rtol=3e-2)
320 torch.testing.assert_close(k_triton, k_ref, atol=3e-2, rtol=3e-2)