main
py 305 lines 9.25 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Replay MoE weights after sleep/wake and verify captured graph replay.
4
5 This test exercises the failure mode directly at the MoE layer boundary:
6
7 1. Build a small ROCm unquantized `RoutedExperts` layer.
8 2. Load deterministic random expert weights and run post-load processing.
9 3. Capture a CUDA graph for `RoutedExperts.forward_modular`.
10 4. Put the CuMem weights pool through level-2 sleep/wake semantics.
11 5. Replay the same checkpoint-style weights through the layer weight loader.
12 6. Verify graph replay is deterministic across sleep/wake and weight replay.
13
14 The important invariant is that post-load processing must not replace parameter
15 storage after CUDA graph capture, because captured kernels hold device
16 addresses for the weight tensors.
17 """
18
19 import contextlib
20 import gc
21 import importlib.util
22 from collections.abc import Iterable
23
24 import pytest
25 import torch
26
27 from vllm.device_allocator.cumem import CuMemAllocator, cumem_available
28 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
29 from vllm.model_executor.layers.fused_moe.config import (
30 FusedMoEConfig,
31 FusedMoEParallelConfig,
32 RoutingMethodType,
33 )
34 from vllm.model_executor.layers.fused_moe.expert_map_manager import (
35 ExpertMapManager,
36 )
37 from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
38 from vllm.platforms import current_platform
39
40 aiter_available = importlib.util.find_spec("aiter") is not None
41
42 pytestmark = pytest.mark.skipif(
43 not (current_platform.is_rocm() and aiter_available),
44 reason="MoE replay test requires ROCm with AITER installed",
45 )
46
47 DEVICE = "cuda"
48 DTYPE = torch.bfloat16
49 NUM_EXPERTS = 4
50 TOPK = 2
51 NUM_TOKENS = 16
52 HIDDEN_SIZE = 256
53 INTERMEDIATE_SIZE = 256
54
55
56 def _moe_topk_softmax_available() -> bool:
57 try:
58 importlib.import_module("vllm._moe_C_stable_libtorch")
59 except ImportError:
60 return False
61 return hasattr(torch.ops, "_moe_C") and hasattr(torch.ops._moe_C, "topk_softmax")
62
63
64 def _set_backend_env(monkeypatch: pytest.MonkeyPatch, backend: str) -> None:
65 monkeypatch.setenv("VLLM_ROCM_MOE_PADDING", "1")
66 if backend == "aiter":
67 monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
68 monkeypatch.setenv("VLLM_ROCM_USE_AITER_MOE", "1")
69 elif backend == "triton":
70 monkeypatch.delenv("VLLM_ROCM_USE_AITER", raising=False)
71 monkeypatch.setenv("VLLM_ROCM_USE_AITER_MOE", "0")
72 else:
73 raise AssertionError(f"unknown backend {backend}")
74
75 from vllm._aiter_ops import rocm_aiter_ops
76
77 rocm_aiter_ops.refresh_env_variables()
78
79
80 def _make_moe_config() -> FusedMoEConfig:
81 return FusedMoEConfig(
82 num_experts=NUM_EXPERTS,
83 experts_per_token=TOPK,
84 hidden_dim=HIDDEN_SIZE,
85 intermediate_size=INTERMEDIATE_SIZE,
86 num_local_experts=NUM_EXPERTS,
87 num_logical_experts=NUM_EXPERTS,
88 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
89 activation=MoEActivation.SILU,
90 in_dtype=DTYPE,
91 device=DEVICE,
92 routing_method=RoutingMethodType.TopK,
93 max_num_tokens=NUM_TOKENS,
94 )
95
96
97 def _make_expert_map_manager(moe_config: FusedMoEConfig) -> ExpertMapManager:
98 return ExpertMapManager(
99 max_num_batched_tokens=NUM_TOKENS,
100 top_k=TOPK,
101 global_num_experts=NUM_EXPERTS,
102 num_redundant_experts=0,
103 num_expert_group=None,
104 moe_parallel_config=moe_config.moe_parallel_config,
105 placement_strategy="linear",
106 enable_eplb=False,
107 )
108
109
110 def _make_routed_experts() -> RoutedExperts:
111 moe_config = _make_moe_config()
112 return RoutedExperts(
113 "experts",
114 DTYPE,
115 moe_config,
116 quant_config=None,
117 expert_map_manager=_make_expert_map_manager(moe_config),
118 )
119
120
121 def _expert_weight_iterator(seed: int) -> Iterable[tuple[str, torch.Tensor]]:
122 generator = torch.Generator(device=DEVICE)
123 generator.manual_seed(seed)
124 for expert_id in range(NUM_EXPERTS):
125 yield (
126 f"{expert_id}.gate_proj.weight",
127 torch.randn(
128 INTERMEDIATE_SIZE,
129 HIDDEN_SIZE,
130 generator=generator,
131 device=DEVICE,
132 dtype=DTYPE,
133 ),
134 )
135 yield (
136 f"{expert_id}.up_proj.weight",
137 torch.randn(
138 INTERMEDIATE_SIZE,
139 HIDDEN_SIZE,
140 generator=generator,
141 device=DEVICE,
142 dtype=DTYPE,
143 ),
144 )
145 yield (
146 f"{expert_id}.down_proj.weight",
147 torch.randn(
148 HIDDEN_SIZE,
149 INTERMEDIATE_SIZE,
150 generator=generator,
151 device=DEVICE,
152 dtype=DTYPE,
153 ),
154 )
155
156
157 def _load_expert_weights(layer: RoutedExperts, seed: int) -> set[str]:
158 loaded = set(layer.load_weights(_expert_weight_iterator(seed)))
159 assert {"w13_weight", "w2_weight"} <= loaded
160 return loaded
161
162
163 def _process_weights_after_loading(layer: RoutedExperts) -> None:
164 layer.quant_method.process_weights_after_loading(layer)
165 torch.accelerator.synchronize()
166
167
168 def _make_static_inputs() -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
169 generator = torch.Generator(device=DEVICE)
170 generator.manual_seed(2026)
171 x = torch.randn(
172 NUM_TOKENS,
173 HIDDEN_SIZE,
174 generator=generator,
175 device=DEVICE,
176 dtype=DTYPE,
177 )
178 topk_ids = torch.arange(NUM_TOKENS, device=DEVICE, dtype=torch.int64).unsqueeze(1)
179 topk_ids = torch.cat(
180 [topk_ids % NUM_EXPERTS, (topk_ids + 1) % NUM_EXPERTS],
181 dim=1,
182 )
183 topk_weights = torch.full(
184 (NUM_TOKENS, TOPK),
185 1.0 / TOPK,
186 device=DEVICE,
187 dtype=DTYPE,
188 )
189 return x, topk_weights, topk_ids
190
191
192 def _forward(
193 layer: RoutedExperts,
194 x: torch.Tensor,
195 topk_weights: torch.Tensor,
196 topk_ids: torch.Tensor,
197 ) -> torch.Tensor:
198 return layer.forward_modular(
199 x,
200 topk_weights,
201 topk_ids,
202 shared_experts=None,
203 shared_experts_input=None,
204 )
205
206
207 def _capture_forward_graph(
208 layer: RoutedExperts,
209 x: torch.Tensor,
210 topk_weights: torch.Tensor,
211 topk_ids: torch.Tensor,
212 ) -> tuple[torch.cuda.CUDAGraph, torch.Tensor]:
213 # Warm up kernel dispatch/JIT before capture so the captured region is only
214 # the steady-state forward path.
215 for _ in range(3):
216 _forward(layer, x, topk_weights, topk_ids)
217 torch.accelerator.synchronize()
218
219 graph = torch.cuda.CUDAGraph()
220 stream = torch.cuda.Stream()
221 with torch.cuda.graph(graph, stream=stream):
222 graph_output = _forward(layer, x, topk_weights, topk_ids)
223 torch.accelerator.synchronize()
224 return graph, graph_output
225
226
227 def _weight_pointers(layer: RoutedExperts) -> dict[str, int]:
228 return {
229 "w13_weight": layer.w13_weight.data_ptr(),
230 "w2_weight": layer.w2_weight.data_ptr(),
231 }
232
233
234 def _assert_backend(layer: RoutedExperts, backend: str) -> None:
235 selected = str(
236 getattr(
237 layer.quant_method.unquantized_backend,
238 "value",
239 layer.quant_method.unquantized_backend,
240 )
241 )
242 if backend == "aiter":
243 assert selected == "ROCm AITER"
244 else:
245 assert selected == "TRITON"
246
247
248 @pytest.mark.parametrize("backend", ["triton", "aiter"])
249 @torch.inference_mode()
250 def test_moe_cuda_graph_replay_after_sleep_wake_weight_update(
251 monkeypatch: pytest.MonkeyPatch,
252 default_vllm_config,
253 workspace_init,
254 backend: str,
255 ) -> None:
256 assert default_vllm_config is not None
257 assert workspace_init is None
258 if not cumem_available:
259 pytest.skip("sleep mode replay requires the cumem allocator extension")
260 if not _moe_topk_softmax_available():
261 pytest.skip("MoE C extension with topk_softmax is required")
262
263 _set_backend_env(monkeypatch, backend)
264
265 allocator = CuMemAllocator.get_instance()
266 layer = graph = graph_output = None
267 try:
268 with allocator.use_memory_pool("weights"), torch.device(DEVICE):
269 layer = _make_routed_experts()
270 _load_expert_weights(layer, seed=1)
271 _process_weights_after_loading(layer)
272 _assert_backend(layer, backend)
273
274 x, topk_weights, topk_ids = _make_static_inputs()
275 graph, graph_output = _capture_forward_graph(layer, x, topk_weights, topk_ids)
276 captured_pointers = _weight_pointers(layer)
277
278 graph.replay()
279 torch.accelerator.synchronize()
280 first_graph_output = graph_output.clone()
281
282 # Match vLLM level-2 sleep semantics: weights are discarded, then the
283 # same virtual addresses are remapped on wake and refilled by replay.
284 allocator.sleep(offload_tags=tuple())
285 allocator.wake_up()
286 _load_expert_weights(layer, seed=1)
287 _process_weights_after_loading(layer)
288
289 assert _weight_pointers(layer) == captured_pointers
290
291 graph_output.zero_()
292 graph.replay()
293 torch.accelerator.synchronize()
294
295 torch.testing.assert_close(
296 graph_output, first_graph_output, rtol=2e-2, atol=2e-2
297 )
298 finally:
299 del graph_output
300 del graph
301 del layer
302 gc.collect()
303 with contextlib.suppress(Exception):
304 allocator.release_pools()
305 torch.accelerator.empty_cache()