A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 3, 06:45 UTC
[Bugfix][MoE] FlashInfer one-sided: workspace union across heterogeneous layers (#42976)
Signed-off-by: Tomer Asida <57313761+tomeras91@users.noreply.github.com>
tomeras91 committed
May 19, 2026 at 21:43 UTC
f54721bcc3e072d71b0e09c0b0bd6d692eb06161
2 files changed
+142
-27
tests/distributed/test_mnnvl_alltoall.py
+84
@@ -342,6 +342,90 @@ def test_one_sided_manager_lifecycle(world_size):
342
)
343
344
345
+# ---------------------------------------------------------------------------
346
+# Test 2b: One-sided manager grows workspace across heterogeneous MoE layers
347
+# ---------------------------------------------------------------------------
348
+#
349
+# Models with heterogeneous MoE quantization — most notably a quantized base
350
+# MoE combined with an unquantized MTP head — can call initialize() multiple
351
+# times with different per-token dispatch payload sizes. The shared workspace
352
+# must grow to the union and the MoeAlltoAll must be rebuilt; otherwise a
353
+# later layer's combine call overruns the workspace sized for the first
354
+# layer's smaller payload and trips FlashInfer's combinePayloadOffset assert.
355
+# ---------------------------------------------------------------------------
356
+
357
+
358
+def _one_sided_workspace_grow_worker(rank, world_size):
359
+ from vllm.distributed.device_communicators.all2all import (
360
+ FlashInferNVLinkOneSidedManager,
361
+ )
362
+ from vllm.distributed.parallel_state import get_dp_group
363
+
364
+ cpu_group = get_dp_group().cpu_group
365
+ manager = FlashInferNVLinkOneSidedManager(cpu_group)
366
+
367
+ base_kwargs = dict(
368
+ max_num_tokens=1024,
369
+ top_k=2,
370
+ num_experts=world_size * 8,
371
+ hidden_size=4096,
372
+ )
373
+ nvfp4_kwargs = dict(
374
+ dispatch_dtype_bytes_per_elem=0,
375
+ dispatch_scale_bytes_per_token=base_kwargs["hidden_size"] // 16,
376
+ )
377
+ bf16_kwargs = dict(
378
+ dispatch_dtype_bytes_per_elem=2,
379
+ dispatch_scale_bytes_per_token=0,
380
+ )
381
+
382
+ # First init: NVFP4-like (hidden_bytes = hidden // 2 + hidden // 16).
383
+ manager.initialize(**base_kwargs, **nvfp4_kwargs)
384
+ assert manager.initialized
385
+ nvfp4_workspace_size = manager.workspace_size
386
+ nvfp4_moe_alltoall = manager.moe_alltoall
387
+
388
+ torch.distributed.barrier()
389
+
390
+ # Second init: bf16-like (hidden_bytes = hidden * 2). Models the case of
391
+ # a quantized base MoE followed by an unquantized MoE layer (e.g. an MTP
392
+ # head). Per-token dispatch payload is ~4x larger, so the union workspace
393
+ # must grow and MoeAlltoAll must be rebuilt.
394
+ manager.initialize(**base_kwargs, **bf16_kwargs)
395
+ assert manager.initialized
396
+ assert manager.workspace_size > nvfp4_workspace_size
397
+ assert manager.moe_alltoall is not nvfp4_moe_alltoall
398
+ bf16_workspace_size = manager.workspace_size
399
+ bf16_moe_alltoall = manager.moe_alltoall
400
+
401
+ torch.distributed.barrier()
402
+
403
+ # Third init: back to NVFP4-like shape. Existing workspace already covers
404
+ # it, so initialize() must no-op — no shrink, no rebuild.
405
+ manager.initialize(**base_kwargs, **nvfp4_kwargs)
406
+ assert manager.initialized
407
+ assert manager.workspace_size == bf16_workspace_size
408
+ assert manager.moe_alltoall is bf16_moe_alltoall
409
+
410
+ torch.distributed.barrier()
411
+ manager.cleanup()
412
+
413
+
414
+@requires_multi_gpu
415
+@requires_one_sided
416
+@requires_ptrace
417
+@pytest.mark.parametrize("world_size", [2])
418
+def test_one_sided_manager_workspace_grow(world_size):
419
+ """A later initialize() with a larger per-token payload must grow the
420
+ workspace and rebuild MoeAlltoAll; a later initialize() with a smaller
421
+ payload must no-op."""
422
+ _spawn_workers(
423
+ _one_sided_workspace_grow_worker,
424
+ world_size,
425
+ dp_size=world_size,
426
+ )
427
+
428
+
429
# ---------------------------------------------------------------------------
430
# Test 3: AgRs dispatch/combine with value validation
431
# ---------------------------------------------------------------------------
vllm/distributed/device_communicators/all2all.py
+58
-27
@@ -571,6 +571,10 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
571
self.initialized = False
572
self.moe_alltoall: MoeAlltoAll | None = None
573
self.mapping = None
574
+ self.workspace_size = 0
575
+ self.max_num_tokens = 0
576
+ self.top_k = 0
577
+ self.num_experts = 0
578
579
def initialize(
580
self,
@@ -581,9 +585,54 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
585
dispatch_dtype_bytes_per_elem: int = 0,
586
dispatch_scale_bytes_per_token: int = 0,
587
):
584
- """Initialize the MoeAlltoAll workspace."""
588
+ """Initialize (or grow) the MoeAlltoAll workspace."""
589
+ if dispatch_dtype_bytes_per_elem == 0:
590
+ hidden_bytes = hidden_size // 2
591
+ else:
592
+ hidden_bytes = hidden_size * dispatch_dtype_bytes_per_elem
593
+ total_dispatch_payload_size_per_token = (
594
+ hidden_bytes
595
+ + dispatch_scale_bytes_per_token
596
+ + top_k * 4 # int32 topks ids
597
+ + top_k * 4 # float32 topk weights
598
+ )
599
+ combine_payload_size_per_token = hidden_size * 2 # bf16 hidden states
600
+ needed_workspace_size = moe_a2a_get_workspace_size_per_rank(
601
+ ep_size=self.world_size,
602
+ max_num_tokens=max_num_tokens,
603
+ total_dispatch_payload_size_per_token=total_dispatch_payload_size_per_token,
604
+ combine_payload_size_per_token=combine_payload_size_per_token,
605
+ )
606
+ # workspace_size and max_num_tokens are kernel-side max-bounds, so
607
+ # heterogeneous MoE layers (e.g. NVFP4 base + bf16 MTP head) only
608
+ # need the shared workspace grown to the union. top_k and num_experts
609
+ # must match across layers: top_k is a strict-equality assert at
610
+ # dispatch (FlashInfer csrc/trtllm_moe_alltoall.cu), and num_experts
611
+ # feeds the expert-to-rank routing math, so any mismatch would crash
612
+ # or silently corrupt routing. All ranks see the same MoE layers in
613
+ # the same order with identical shapes, so the skip / rebuild
614
+ # branches are taken consistently across ranks.
615
if self.initialized:
586
- return
616
+ assert top_k == self.top_k, (
617
+ "FlashInfer one-sided MoeAlltoAll does not support "
618
+ f"heterogeneous top_k across MoE layers (got {top_k}, "
619
+ f"was built with {self.top_k})"
620
+ )
621
+ assert num_experts == self.num_experts, (
622
+ "FlashInfer one-sided MoeAlltoAll does not support "
623
+ f"heterogeneous num_experts across MoE layers (got "
624
+ f"{num_experts}, was built with {self.num_experts})"
625
+ )
626
+ if (
627
+ needed_workspace_size <= self.workspace_size
628
+ and max_num_tokens <= self.max_num_tokens
629
+ ):
630
+ return
631
+
632
+ self.workspace_size = max(self.workspace_size, needed_workspace_size)
633
+ self.max_num_tokens = max(self.max_num_tokens, max_num_tokens)
634
+ self.top_k = top_k
635
+ self.num_experts = num_experts
636
637
self.cleanup()
638
gpus_per_node = torch.accelerator.device_count()
@@ -610,38 +659,17 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
659
ep_config = MnnvlConfig(
660
comm_backend=CustomCommunicator(self.cpu_group),
661
)
613
- if dispatch_dtype_bytes_per_elem == 0:
614
- hidden_bytes = hidden_size // 2
615
- else:
616
- hidden_bytes = hidden_size * dispatch_dtype_bytes_per_elem
617
- total_dispatch_payload_size_per_token = (
618
- hidden_bytes
619
- + dispatch_scale_bytes_per_token
620
- + top_k * 4 # int32 topks ids
621
- + top_k * 4 # float32 topk weights
622
- )
623
- combine_payload_size_per_token = hidden_size * 2 # bf16 hidden states
624
- self.workspace_size = moe_a2a_get_workspace_size_per_rank(
625
- ep_size=self.world_size,
626
- max_num_tokens=max_num_tokens,
627
- total_dispatch_payload_size_per_token=total_dispatch_payload_size_per_token,
628
- combine_payload_size_per_token=combine_payload_size_per_token,
629
- )
662
663
self.moe_alltoall = MoeAlltoAll(
664
mapping=self.mapping,
633
- max_num_tokens=max_num_tokens,
634
- top_k=top_k,
635
- num_experts=num_experts,
665
+ max_num_tokens=self.max_num_tokens,
666
+ top_k=self.top_k,
667
+ num_experts=self.num_experts,
668
workspace_size_per_rank=self.workspace_size,
669
mnnvl_config=ep_config,
670
)
671
672
self.gpus_per_node = gpus_per_node
641
- self.max_num_tokens = max_num_tokens
642
- self.top_k = top_k
643
- self.num_experts = num_experts
644
- self.hidden_size = hidden_size
673
self.initialized = True
674
675
logger.info(
@@ -649,7 +677,10 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
677
self.rank,
678
self.world_size,
679
)
652
- dist.barrier()
680
+ # Scope barrier to the EP group: with PP, different EP groups can
681
+ # rebuild a different number of times if their MoE layers have
682
+ # different shape sequences, so a world-level barrier would deadlock.
683
+ dist.barrier(group=self.cpu_group)
684
685
def get_handle(self, kwargs):
686
return self