main
py 878 lines 28.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """End-to-end tests for routed-expert capture on the monolithic MoE path.
4
5 These tests exercise the wiring that lets ``RoutedExpertsCapturer`` see the
6 expert IDs picked by FlashInfer's fused router-and-experts kernels (the
7 "monolithic" path). When ``set_capture_fn`` is installed on
8 a ``FusedMoEExpertsMonolithic`` subclass that supports it, the kernel call
9 should:
10
11 * allocate an int16 ``(num_tokens, top_k)`` buffer,
12 * pass it to FlashInfer as ``routing_replay_out``,
13 * invoke the callback after the kernel returns.
14 """
15
16 from __future__ import annotations
17
18 from types import SimpleNamespace
19 from unittest.mock import patch
20
21 import pytest
22 import torch
23
24 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
25 from vllm.model_executor.layers.fused_moe.config import (
26 FusedMoEConfig,
27 FusedMoEParallelConfig,
28 FusedMoEQuantConfig,
29 RoutingMethodType,
30 fp8_w8a8_moe_quant_config,
31 )
32 from vllm.model_executor.layers.fused_moe.experts.trtllm_bf16_moe import (
33 TrtLlmBf16ExpertsMonolithic,
34 )
35 from vllm.model_executor.layers.fused_moe.experts.trtllm_fp8_moe import (
36 TrtLlmFp8ExpertsMonolithic,
37 )
38 from vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe import (
39 TrtLlmNvFp4ExpertsMonolithic,
40 )
41 from vllm.platforms import current_platform
42
43 try:
44 from vllm.utils.flashinfer import has_flashinfer_trtllm_fused_moe
45 except ImportError:
46 pytest.skip("flashinfer not available", allow_module_level=True)
47
48 if not has_flashinfer_trtllm_fused_moe() or not current_platform.is_cuda():
49 pytest.skip(
50 "Requires FlashInfer TRT-LLM fused MoE on CUDA",
51 allow_module_level=True,
52 )
53
54 if not current_platform.is_device_capability_family(100):
55 pytest.skip(
56 "TRT-LLM fused MoE kernels require SM100+",
57 allow_module_level=True,
58 )
59
60
61 def _shuffle_bf16_weights_block_major_k(
62 w: torch.Tensor, epilogue_tile_m: int = 64, block_k: int = 128
63 ) -> torch.Tensor:
64 """Reshape ``w`` (E, M, K) into the ``BlockMajorK`` layout expected by
65 ``trtllm_bf16_moe``: ``(E, K/block_k, M, block_k)`` after a per-expert
66 row shuffle.
67 """
68 from flashinfer import shuffle_matrix_a
69 from flashinfer.fused_moe import convert_to_block_layout
70
71 num_experts = w.shape[0]
72 shuffled = []
73 for i in range(num_experts):
74 t = shuffle_matrix_a(w[i].view(torch.uint8), epilogue_tile_m)
75 shuffled.append(convert_to_block_layout(t, block_k))
76 return torch.stack(shuffled).view(torch.bfloat16)
77
78
79 def _make_bf16_monolithic_experts(
80 num_experts: int,
81 top_k: int,
82 hidden_size: int,
83 intermediate_size: int,
84 routing_method: RoutingMethodType,
85 device: torch.device,
86 ) -> tuple[TrtLlmBf16ExpertsMonolithic, torch.Tensor, torch.Tensor]:
87 """Construct the monolithic BF16 experts plus the BlockMajorK weights
88 expected by ``trtllm_bf16_moe``.
89 """
90 parallel_cfg = FusedMoEParallelConfig.make_no_parallel()
91 moe_config = FusedMoEConfig(
92 num_experts=num_experts,
93 experts_per_token=top_k,
94 hidden_dim=hidden_size,
95 intermediate_size=intermediate_size,
96 num_local_experts=num_experts,
97 num_logical_experts=num_experts,
98 moe_parallel_config=parallel_cfg,
99 in_dtype=torch.bfloat16,
100 activation=MoEActivation.SILU,
101 device=device,
102 routing_method=routing_method,
103 max_num_tokens=16,
104 )
105 quant_config = FusedMoEQuantConfig.make(
106 quant_dtype=None,
107 per_act_token_quant=False,
108 per_out_ch_quant=False,
109 block_shape=None,
110 )
111
112 experts = TrtLlmBf16ExpertsMonolithic(
113 moe_config=moe_config, quant_config=quant_config
114 )
115
116 gemm1 = (
117 torch.randn(
118 num_experts,
119 2 * intermediate_size,
120 hidden_size,
121 device=device,
122 dtype=torch.bfloat16,
123 )
124 * 0.1
125 )
126 gemm2 = (
127 torch.randn(
128 num_experts,
129 hidden_size,
130 intermediate_size,
131 device=device,
132 dtype=torch.bfloat16,
133 )
134 * 0.1
135 )
136 w13 = _shuffle_bf16_weights_block_major_k(gemm1)
137 w2 = _shuffle_bf16_weights_block_major_k(gemm2)
138 return experts, w13, w2
139
140
141 def _run_bf16_monolithic(
142 experts: TrtLlmBf16ExpertsMonolithic,
143 hidden_states: torch.Tensor,
144 w13: torch.Tensor,
145 w2: torch.Tensor,
146 router_logits: torch.Tensor,
147 num_experts: int,
148 *,
149 n_group: int | None = None,
150 topk_group: int | None = None,
151 routed_scaling_factor: float | None = None,
152 e_score_correction_bias: torch.Tensor | None = None,
153 ) -> torch.Tensor:
154 return experts.apply(
155 hidden_states=hidden_states,
156 w1=w13,
157 w2=w2,
158 router_logits=router_logits,
159 activation=MoEActivation.SILU,
160 global_num_experts=num_experts,
161 expert_map=None,
162 a1q_scale=None,
163 apply_router_weight_on_input=False,
164 num_expert_group=n_group,
165 topk_group=topk_group,
166 e_score_correction_bias=e_score_correction_bias,
167 routed_scaling_factor=routed_scaling_factor,
168 )
169
170
171 _DSV3_NUM_EXPERTS = 32
172 _DSV3_N_GROUP = 4
173 _DSV3_TOPK_GROUP = 2
174
175
176 def _make_dsv3_routing_bias(num_experts: int, device: torch.device) -> torch.Tensor:
177 return torch.randn(num_experts, device=device, dtype=torch.bfloat16)
178
179
180 @pytest.mark.parametrize("num_tokens", [2, 7, 16])
181 @pytest.mark.parametrize("top_k", [2, 4])
182 def test_trtllm_bf16_monolithic_routing_replay_records_valid_experts(
183 num_tokens: int,
184 top_k: int,
185 ) -> None:
186 """The capture callback should receive the int16 routed-expert IDs the
187 kernel actually used, the values should be valid expert indices, and
188 each token should pick ``top_k`` distinct experts."""
189 if top_k > _DSV3_N_GROUP * _DSV3_TOPK_GROUP:
190 pytest.skip(
191 f"DSV3 requires top_k <= n_group * topk_group "
192 f"({_DSV3_N_GROUP * _DSV3_TOPK_GROUP})"
193 )
194 torch.manual_seed(0)
195 device = torch.device("cuda:0")
196
197 num_experts = _DSV3_NUM_EXPERTS
198 hidden_size = 1024
199 intermediate_size = 1024
200
201 experts, w13, w2 = _make_bf16_monolithic_experts(
202 num_experts=num_experts,
203 top_k=top_k,
204 hidden_size=hidden_size,
205 intermediate_size=intermediate_size,
206 routing_method=RoutingMethodType.DeepSeekV3,
207 device=device,
208 )
209
210 captured: list[torch.Tensor] = []
211
212 def capture_fn(replay_out: torch.Tensor) -> None:
213 captured.append(replay_out.clone())
214
215 assert experts.supports_routing_replay_capture()
216 experts.set_capture_fn(capture_fn)
217
218 hidden_states = (
219 torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
220 )
221 router_logits = torch.rand(
222 num_tokens, num_experts, device=device, dtype=torch.float32
223 )
224 routing_bias = _make_dsv3_routing_bias(num_experts, device)
225
226 _ = _run_bf16_monolithic(
227 experts,
228 hidden_states=hidden_states,
229 w13=w13,
230 w2=w2,
231 router_logits=router_logits,
232 num_experts=num_experts,
233 n_group=_DSV3_N_GROUP,
234 topk_group=_DSV3_TOPK_GROUP,
235 routed_scaling_factor=1.0,
236 e_score_correction_bias=routing_bias,
237 )
238
239 assert len(captured) == 1
240 replay = captured[0]
241 assert replay.dtype == torch.int16
242 assert replay.shape == (num_tokens, top_k)
243 assert (replay >= 0).all(), f"got out-of-range values: {replay}"
244 assert (replay < num_experts).all(), f"got out-of-range values: {replay}"
245
246 for t in range(num_tokens):
247 unique = replay[t].unique()
248 assert unique.numel() == top_k, (
249 f"token {t}: expected {top_k} distinct experts, "
250 f"got {unique.numel()} ({replay[t].tolist()})"
251 )
252
253
254 @pytest.mark.parametrize("num_tokens", [2, 7, 16])
255 @pytest.mark.parametrize(
256 "routing_method",
257 [
258 RoutingMethodType.Renormalize,
259 RoutingMethodType.RenormalizeNaive,
260 ],
261 )
262 def test_trtllm_bf16_monolithic_routing_replay_non_dsv3(
263 num_tokens: int,
264 routing_method: RoutingMethodType,
265 ) -> None:
266 """Routing replay works for non-DeepSeekV3 routing methods too.
267 FlashInfer's ``routing_replay_out`` is routing-method-agnostic."""
268 torch.manual_seed(0)
269 device = torch.device("cuda:0")
270
271 num_experts = 8
272 top_k = 2
273 hidden_size = 1024
274 intermediate_size = 1024
275
276 experts, w13, w2 = _make_bf16_monolithic_experts(
277 num_experts=num_experts,
278 top_k=top_k,
279 hidden_size=hidden_size,
280 intermediate_size=intermediate_size,
281 routing_method=routing_method,
282 device=device,
283 )
284
285 captured: list[torch.Tensor] = []
286 experts.set_capture_fn(lambda r: captured.append(r.clone()))
287
288 hidden_states = (
289 torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
290 )
291 router_logits = torch.rand(
292 num_tokens, num_experts, device=device, dtype=torch.float32
293 )
294
295 _ = _run_bf16_monolithic(
296 experts,
297 hidden_states=hidden_states,
298 w13=w13,
299 w2=w2,
300 router_logits=router_logits,
301 num_experts=num_experts,
302 )
303
304 assert len(captured) == 1
305 replay = captured[0]
306 assert replay.dtype == torch.int16
307 assert replay.shape == (num_tokens, top_k)
308 assert (replay >= 0).all(), f"got out-of-range values: {replay}"
309 assert (replay < num_experts).all(), f"got out-of-range values: {replay}"
310 for t in range(num_tokens):
311 unique = replay[t].unique()
312 assert unique.numel() == top_k, (
313 f"token {t}: expected {top_k} distinct experts, "
314 f"got {unique.numel()} ({replay[t].tolist()})"
315 )
316
317
318 def test_trtllm_bf16_monolithic_capture_disabled_skips_buffer_alloc() -> None:
319 """With no callback installed the kernel should not see a
320 ``routing_replay_out`` tensor — verify the helper short-circuits."""
321 torch.manual_seed(0)
322 device = torch.device("cuda:0")
323 experts, _, _ = _make_bf16_monolithic_experts(
324 num_experts=_DSV3_NUM_EXPERTS,
325 top_k=2,
326 hidden_size=1024,
327 intermediate_size=1024,
328 routing_method=RoutingMethodType.DeepSeekV3,
329 device=device,
330 )
331 # No callback installed.
332 buf = experts._maybe_make_routing_replay_buffer(num_tokens=4, device=device)
333 assert buf is None
334
335 # Dispatch is also a no-op.
336 experts._maybe_dispatch_routing_replay(buf, num_tokens=4)
337
338
339 def test_trtllm_bf16_monolithic_supports_capture_for_all_routing() -> None:
340 """FlashInfer's ``routing_replay_out`` is supported by all routing
341 methods, so ``supports_routing_replay_capture`` should be True
342 regardless of routing method."""
343 device = torch.device("cuda:0")
344 for routing_method in (
345 RoutingMethodType.DeepSeekV3,
346 RoutingMethodType.Renormalize,
347 RoutingMethodType.RenormalizeNaive,
348 ):
349 experts, _, _ = _make_bf16_monolithic_experts(
350 num_experts=_DSV3_NUM_EXPERTS,
351 top_k=2,
352 hidden_size=1024,
353 intermediate_size=1024,
354 routing_method=routing_method,
355 device=device,
356 )
357 assert experts.supports_routing_replay_capture() is True, (
358 f"{routing_method!r} should support routing replay capture"
359 )
360
361
362 def test_trtllm_bf16_monolithic_capture_buffer_shape_and_dtype() -> None:
363 """When capture is installed, the allocated buffer is int16 and shaped
364 ``(num_tokens, experts_per_token)``."""
365 device = torch.device("cuda:0")
366 experts, _, _ = _make_bf16_monolithic_experts(
367 num_experts=_DSV3_NUM_EXPERTS,
368 top_k=4,
369 hidden_size=1024,
370 intermediate_size=1024,
371 routing_method=RoutingMethodType.DeepSeekV3,
372 device=device,
373 )
374 experts.set_capture_fn(lambda r: None)
375 buf = experts._maybe_make_routing_replay_buffer(num_tokens=11, device=device)
376 assert buf is not None
377 assert buf.dtype == torch.int16
378 assert buf.shape[0] >= 11
379 assert buf.shape[1] == 4
380 assert buf.device.type == "cuda"
381
382
383 def test_routed_experts_capturer_e2e_via_monolithic_experts() -> None:
384 """End-to-end: bind ``RoutedExpertsCapturer.capture`` as the callback
385 on the monolithic experts and verify the captured rows land in the
386 capturer's device buffer at the correct layer slot.
387
388 Mirrors the monolithic path in ``bind_routed_experts_capturer``: a single
389 closure is installed on ``fused_experts`` and routes per-layer based on
390 the closed-over ``layer_id``.
391 """
392 from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
393 RoutedExpertsCapturer,
394 )
395
396 torch.manual_seed(7)
397 device = torch.device("cuda:0")
398 num_tokens = 4
399 top_k = 2
400 num_experts = _DSV3_NUM_EXPERTS
401 hidden_size = 1024
402 intermediate_size = 1024
403
404 experts, w13, w2 = _make_bf16_monolithic_experts(
405 num_experts=num_experts,
406 top_k=top_k,
407 hidden_size=hidden_size,
408 intermediate_size=intermediate_size,
409 routing_method=RoutingMethodType.DeepSeekV3,
410 device=device,
411 )
412
413 num_layers = 3
414 layer_id = 1
415 capturer = RoutedExpertsCapturer.__new__(RoutedExpertsCapturer)
416 capturer.dp_rank = 0
417 capturer.tp_size = 1
418 capturer.device_buffer = torch.full(
419 (num_tokens + 4, num_layers, top_k),
420 -1,
421 dtype=torch.int32,
422 device=device,
423 )
424
425 def capture_fn(replay_out: torch.Tensor) -> None:
426 capturer.capture(layer_id, replay_out)
427
428 experts.set_capture_fn(capture_fn)
429
430 hidden_states = (
431 torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
432 )
433 router_logits = torch.rand(
434 num_tokens, num_experts, device=device, dtype=torch.float32
435 )
436 routing_bias = _make_dsv3_routing_bias(num_experts, device)
437
438 # Patch get_forward_context to return a dp_metadata=None context so the
439 # capturer takes the single-DP branch.
440 import vllm.model_executor.layers.fused_moe.routed_experts_capturer as rec
441
442 with patch.object(
443 rec,
444 "get_forward_context",
445 return_value=SimpleNamespace(dp_metadata=None),
446 ):
447 _ = _run_bf16_monolithic(
448 experts,
449 hidden_states=hidden_states,
450 w13=w13,
451 w2=w2,
452 router_logits=router_logits,
453 num_experts=num_experts,
454 n_group=_DSV3_N_GROUP,
455 topk_group=_DSV3_TOPK_GROUP,
456 routed_scaling_factor=1.0,
457 e_score_correction_bias=routing_bias,
458 )
459
460 captured = capturer.device_buffer[:num_tokens, layer_id, :].cpu()
461 # Valid expert IDs at this layer.
462 assert (captured >= 0).all()
463 assert (captured < num_experts).all()
464 for t in range(num_tokens):
465 unique = captured[t].unique()
466 assert unique.numel() == top_k, (
467 f"token {t}: expected {top_k} distinct experts at layer "
468 f"{layer_id}, got {unique.numel()}"
469 )
470
471 # Other layers / trailing token rows untouched.
472 for other_layer in range(num_layers):
473 if other_layer == layer_id:
474 continue
475 assert (capturer.device_buffer[:, other_layer, :].cpu() == -1).all(), (
476 f"layer {other_layer} should be untouched, got writes"
477 )
478 assert (capturer.device_buffer[num_tokens:, layer_id, :].cpu() == -1).all(), (
479 "tail rows beyond num_tokens should remain sentinel"
480 )
481
482
483 # ----------------------------------------------------------------------------
484 # FP8 block-scale (DeepSeekFp8) — vLLM's ``TrtLlmFp8ExpertsMonolithic``
485 # ----------------------------------------------------------------------------
486
487
488 def _make_fp8_block_scale_monolithic_experts(
489 num_experts: int,
490 top_k: int,
491 hidden_size: int,
492 intermediate_size: int,
493 device: torch.device,
494 ) -> tuple[TrtLlmFp8ExpertsMonolithic, torch.Tensor, torch.Tensor]:
495 """Set up ``TrtLlmFp8ExpertsMonolithic`` for the DeepSeekFp8 block-scale
496 code path with DSV3 routing.
497
498 Weights are shuffled into the BlockMajorK layout the kernel expects
499 (same helper the vLLM weight loader uses for DeepSeek-FP8 models).
500 """
501 from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
502 _shuffle_deepseek_fp8_moe_weights,
503 )
504
505 block_k = 128
506 parallel_cfg = FusedMoEParallelConfig.make_no_parallel()
507 moe_config = FusedMoEConfig(
508 num_experts=num_experts,
509 experts_per_token=top_k,
510 hidden_dim=hidden_size,
511 intermediate_size=intermediate_size,
512 num_local_experts=num_experts,
513 num_logical_experts=num_experts,
514 moe_parallel_config=parallel_cfg,
515 in_dtype=torch.bfloat16,
516 activation=MoEActivation.SILU,
517 device=device,
518 routing_method=RoutingMethodType.DeepSeekV3,
519 max_num_tokens=16,
520 )
521
522 # Random fp8 weights + ones-block scales (the kernel decoder only cares
523 # that the per-block scales are present and finite for routing/replay).
524 gemm1 = torch.randn(
525 num_experts, 2 * intermediate_size, hidden_size, device=device
526 ).to(torch.float8_e4m3fn)
527 gemm2 = torch.randn(num_experts, hidden_size, intermediate_size, device=device).to(
528 torch.float8_e4m3fn
529 )
530 w13_shuffled, w2_shuffled = _shuffle_deepseek_fp8_moe_weights(gemm1, gemm2)
531
532 w1_scale = torch.ones(
533 num_experts,
534 2 * intermediate_size // block_k,
535 hidden_size // block_k,
536 device=device,
537 dtype=torch.float32,
538 )
539 w2_scale = torch.ones(
540 num_experts,
541 hidden_size // block_k,
542 intermediate_size // block_k,
543 device=device,
544 dtype=torch.float32,
545 )
546 quant_config = fp8_w8a8_moe_quant_config(
547 w1_scale=w1_scale,
548 w2_scale=w2_scale,
549 block_shape=[block_k, block_k],
550 per_act_token_quant=False,
551 )
552
553 experts = TrtLlmFp8ExpertsMonolithic(
554 moe_config=moe_config, quant_config=quant_config
555 )
556 return experts, w13_shuffled, w2_shuffled
557
558
559 def _run_fp8_block_scale_monolithic(
560 experts: TrtLlmFp8ExpertsMonolithic,
561 hidden_states_fp8: torch.Tensor,
562 hidden_states_scale: torch.Tensor,
563 w13: torch.Tensor,
564 w2: torch.Tensor,
565 router_logits: torch.Tensor,
566 num_experts: int,
567 routing_bias: torch.Tensor,
568 ) -> torch.Tensor:
569 return experts.apply(
570 hidden_states=hidden_states_fp8,
571 w1=w13,
572 w2=w2,
573 router_logits=router_logits,
574 activation=MoEActivation.SILU,
575 global_num_experts=num_experts,
576 expert_map=None,
577 # The block-scale apply path reads ``a1q_scale`` and transposes it
578 # to ``(hidden_size/128, num_tokens)`` for the kernel call.
579 a1q_scale=hidden_states_scale,
580 apply_router_weight_on_input=False,
581 num_expert_group=_DSV3_N_GROUP,
582 topk_group=_DSV3_TOPK_GROUP,
583 e_score_correction_bias=routing_bias,
584 routed_scaling_factor=1.0,
585 )
586
587
588 @pytest.mark.parametrize("num_tokens", [2, 7, 16])
589 @pytest.mark.parametrize("top_k", [2, 4])
590 def test_trtllm_fp8_block_scale_monolithic_routing_replay_records_valid_experts(
591 num_tokens: int,
592 top_k: int,
593 ) -> None:
594 """End-to-end: ``TrtLlmFp8ExpertsMonolithic`` (DeepSeekFp8 block-scale
595 path, DSV3 routing) captures valid expert IDs."""
596 if top_k > _DSV3_N_GROUP * _DSV3_TOPK_GROUP:
597 pytest.skip(
598 f"DSV3 requires top_k <= n_group * topk_group "
599 f"({_DSV3_N_GROUP * _DSV3_TOPK_GROUP})"
600 )
601 torch.manual_seed(0)
602 device = torch.device("cuda:0")
603
604 num_experts = _DSV3_NUM_EXPERTS
605 hidden_size = 1024
606 intermediate_size = 1024
607
608 experts, w13, w2 = _make_fp8_block_scale_monolithic_experts(
609 num_experts=num_experts,
610 top_k=top_k,
611 hidden_size=hidden_size,
612 intermediate_size=intermediate_size,
613 device=device,
614 )
615 assert experts.supports_routing_replay_capture()
616
617 captured: list[torch.Tensor] = []
618 experts.set_capture_fn(lambda r: captured.append(r.clone()))
619
620 # Per-token / per-block hidden scales (ones is fine for the routing
621 # path; the GEMM output isn't being asserted on).
622 hidden_states = (
623 torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
624 ).to(torch.float8_e4m3fn)
625 hidden_states_scale = torch.ones(
626 num_tokens, hidden_size // 128, device=device, dtype=torch.float32
627 )
628 router_logits = torch.rand(
629 num_tokens, num_experts, device=device, dtype=torch.float32
630 )
631 routing_bias = _make_dsv3_routing_bias(num_experts, device)
632
633 _ = _run_fp8_block_scale_monolithic(
634 experts,
635 hidden_states_fp8=hidden_states,
636 hidden_states_scale=hidden_states_scale,
637 w13=w13,
638 w2=w2,
639 router_logits=router_logits,
640 num_experts=num_experts,
641 routing_bias=routing_bias,
642 )
643
644 assert len(captured) == 1
645 replay = captured[0]
646 assert replay.dtype == torch.int16
647 assert replay.shape == (num_tokens, top_k)
648 assert (replay >= 0).all(), f"got out-of-range values: {replay}"
649 assert (replay < num_experts).all(), f"got out-of-range values: {replay}"
650 for t in range(num_tokens):
651 unique = replay[t].unique()
652 assert unique.numel() == top_k, (
653 f"token {t}: expected {top_k} distinct experts, "
654 f"got {unique.numel()} ({replay[t].tolist()})"
655 )
656
657
658 # ----------------------------------------------------------------------------
659 # NVFP4 — vLLM's ``TrtLlmNvFp4ExpertsMonolithic``
660 # ----------------------------------------------------------------------------
661
662
663 def _make_nvfp4_monolithic_experts(
664 num_experts: int,
665 top_k: int,
666 hidden_size: int,
667 intermediate_size: int,
668 device: torch.device,
669 ) -> tuple[
670 TrtLlmNvFp4ExpertsMonolithic,
671 torch.Tensor, # w13 (packed nvfp4 uint8)
672 torch.Tensor, # w13 block-scale (fp8)
673 torch.Tensor, # w2 (packed nvfp4 uint8)
674 torch.Tensor, # w2 block-scale (fp8)
675 torch.Tensor, # input global scale (per-tensor float32)
676 ]:
677 """Set up ``TrtLlmNvFp4ExpertsMonolithic`` with NVFP4-quantized weights
678 and DSV3 routing.
679
680 NVFP4 = per-block-of-16 fp4 with an fp8 scale, plus a per-tensor
681 "global" scale. We follow the layout in ``test_ocp_mx_moe.py`` /
682 ``flashinfer/tests/moe/test_trtllm_gen_routed_fused_moe.py``:
683 * weights: uint8 (packed fp4) ``(E, M, K//2)``
684 * weight scales: fp8 ``(E, M, K//16)``
685 * hidden states: uint8 (packed fp4) ``(N, K//2)``
686 * hidden state scales: fp8 ``(N, K//16)``
687 """
688 from flashinfer import fp4_quantize
689
690 block_size = 16
691 parallel_cfg = FusedMoEParallelConfig.make_no_parallel()
692 moe_config = FusedMoEConfig(
693 num_experts=num_experts,
694 experts_per_token=top_k,
695 hidden_dim=hidden_size,
696 intermediate_size=intermediate_size,
697 num_local_experts=num_experts,
698 num_logical_experts=num_experts,
699 moe_parallel_config=parallel_cfg,
700 in_dtype=torch.bfloat16,
701 activation=MoEActivation.SILU,
702 device=device,
703 routing_method=RoutingMethodType.DeepSeekV3,
704 max_num_tokens=16,
705 )
706
707 gemm1 = torch.randn(
708 num_experts,
709 2 * intermediate_size,
710 hidden_size,
711 device=device,
712 dtype=torch.bfloat16,
713 )
714 gemm2 = torch.randn(
715 num_experts,
716 hidden_size,
717 intermediate_size,
718 device=device,
719 dtype=torch.bfloat16,
720 )
721 # Per-tensor weight scaling factor (used to build ``g1_alphas`` /
722 # ``g2_alphas`` below).
723 w_global_scale = torch.tensor(1.0, device=device)
724 # Per-tensor input scaling factor.
725 a_global_scale = torch.tensor(1.0, device=device)
726
727 w13_q, w13_scale = fp4_quantize(
728 gemm1,
729 w_global_scale,
730 block_size,
731 sf_use_ue8m0=False,
732 is_sf_swizzled_layout=False,
733 )
734 w13_scale = w13_scale.view(torch.float8_e4m3fn).reshape(
735 num_experts, 2 * intermediate_size, hidden_size // block_size
736 )
737 w2_q, w2_scale = fp4_quantize(
738 gemm2,
739 w_global_scale,
740 block_size,
741 sf_use_ue8m0=False,
742 is_sf_swizzled_layout=False,
743 )
744 w2_scale = w2_scale.view(torch.float8_e4m3fn).reshape(
745 num_experts, hidden_size, intermediate_size // block_size
746 )
747
748 # NVFP4 dq scale chain: g1_alphas = w1_scale_2 * a1_scale_2,
749 # g2_alphas = w2_scale_2 * a2_scale_2. The kernel multiplies by these.
750 g_alphas = torch.full((num_experts,), 1.0, device=device, dtype=torch.float32)
751 a2_gscale = torch.full((num_experts,), 1.0, device=device, dtype=torch.float32)
752
753 quant_config = FusedMoEQuantConfig.make(
754 quant_dtype="nvfp4",
755 per_act_token_quant=False,
756 per_out_ch_quant=False,
757 block_shape=None,
758 w1_scale=w13_scale,
759 w2_scale=w2_scale,
760 g1_alphas=g_alphas,
761 g2_alphas=g_alphas,
762 a1_gscale=a_global_scale,
763 a2_gscale=a2_gscale,
764 )
765
766 experts = TrtLlmNvFp4ExpertsMonolithic(
767 moe_config=moe_config, quant_config=quant_config
768 )
769 return experts, w13_q, w13_scale, w2_q, w2_scale, a_global_scale
770
771
772 def _run_nvfp4_monolithic(
773 experts: TrtLlmNvFp4ExpertsMonolithic,
774 hidden_states_q: torch.Tensor,
775 hidden_states_scale: torch.Tensor,
776 router_logits: torch.Tensor,
777 num_experts: int,
778 routing_bias: torch.Tensor,
779 ) -> torch.Tensor:
780 """The monolithic NVFP4 apply expects packed fp4 hidden states + the
781 matching fp8 per-block scale stored in the ``a1q_scale`` slot."""
782 # Stash the weight tensors on the experts in the locations the apply()
783 # implementation reads from (it pulls them from quant_config / scales
784 # already; w1/w2 come in as args).
785 return experts.apply(
786 hidden_states=hidden_states_q,
787 w1=experts._w13_packed,
788 w2=experts._w2_packed,
789 router_logits=router_logits,
790 activation=MoEActivation.SILU,
791 global_num_experts=num_experts,
792 expert_map=None,
793 a1q_scale=hidden_states_scale,
794 apply_router_weight_on_input=False,
795 num_expert_group=_DSV3_N_GROUP,
796 topk_group=_DSV3_TOPK_GROUP,
797 e_score_correction_bias=routing_bias,
798 routed_scaling_factor=1.0,
799 )
800
801
802 @pytest.mark.parametrize("num_tokens", [2, 7, 16])
803 @pytest.mark.parametrize("top_k", [2, 4])
804 def test_trtllm_nvfp4_monolithic_routing_replay_records_valid_experts(
805 num_tokens: int,
806 top_k: int,
807 ) -> None:
808 """End-to-end: ``TrtLlmNvFp4ExpertsMonolithic`` captures valid expert IDs
809 on the DSV3 routing path."""
810 if top_k > _DSV3_N_GROUP * _DSV3_TOPK_GROUP:
811 pytest.skip(
812 f"DSV3 requires top_k <= n_group * topk_group "
813 f"({_DSV3_N_GROUP * _DSV3_TOPK_GROUP})"
814 )
815 from flashinfer import fp4_quantize
816
817 torch.manual_seed(0)
818 device = torch.device("cuda:0")
819
820 num_experts = _DSV3_NUM_EXPERTS
821 hidden_size = 1024
822 intermediate_size = 1024
823 block_size = 16
824
825 experts, w13_q, _w13_s, w2_q, _w2_s, a_gs = _make_nvfp4_monolithic_experts(
826 num_experts=num_experts,
827 top_k=top_k,
828 hidden_size=hidden_size,
829 intermediate_size=intermediate_size,
830 device=device,
831 )
832 # The apply() reads w1/w2 from its args, but we keep them on the experts
833 # for convenience of the helper.
834 experts._w13_packed = w13_q
835 experts._w2_packed = w2_q
836
837 assert experts.supports_routing_replay_capture()
838 captured: list[torch.Tensor] = []
839 experts.set_capture_fn(lambda r: captured.append(r.clone()))
840
841 hidden_states = (
842 torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
843 )
844 hidden_states_q, hidden_states_scale = fp4_quantize(
845 hidden_states,
846 a_gs,
847 block_size,
848 sf_use_ue8m0=False,
849 is_sf_swizzled_layout=False,
850 )
851 # The vLLM apply() does the .view(fp8_e4m3fn).reshape itself, so leave
852 # ``hidden_states_scale`` in its native (uint8 packed) form.
853 router_logits = torch.rand(
854 num_tokens, num_experts, device=device, dtype=torch.float32
855 )
856 routing_bias = _make_dsv3_routing_bias(num_experts, device)
857
858 _ = _run_nvfp4_monolithic(
859 experts,
860 hidden_states_q=hidden_states_q,
861 hidden_states_scale=hidden_states_scale,
862 router_logits=router_logits,
863 num_experts=num_experts,
864 routing_bias=routing_bias,
865 )
866
867 assert len(captured) == 1
868 replay = captured[0]
869 assert replay.dtype == torch.int16
870 assert replay.shape == (num_tokens, top_k)
871 assert (replay >= 0).all(), f"got out-of-range values: {replay}"
872 assert (replay < num_experts).all(), f"got out-of-range values: {replay}"
873 for t in range(num_tokens):
874 unique = replay[t].unique()
875 assert unique.numel() == top_k, (
876 f"token {t}: expected {top_k} distinct experts, "
877 f"got {unique.numel()} ({replay[t].tolist()})"
878 )