@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / ccfb620c62
Fork 0

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 4, 11:15 UTC

Create tests/distributed/test_mnnvl_alltoall.py (#35241)

Signed-off-by: Rishi Puri <riship@nvidia.com> Signed-off-by: Claude <claude@anthropic.com> Signed-off-by: Stefano Castagnetta <scastagnetta@nvidia.com> Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Stefano Castagnetta <scastagnetta@nvidia.com>

Rishi Puri committed Apr 29, 2026 at 18:56 UTC ccfb620c62533c0dbfa8d5a0307fab9682b7c29f
2 files changed +779
.buildkite/test_areas/distributed.yaml
+5
@@ -164,6 +164,8 @@ steps:
164 - vllm/v1/engine/llm_engine.py
165 - vllm/v1/executor/uniproc_executor.py
166 - vllm/v1/worker/gpu_worker.py
167 + - tests/distributed/test_mnnvl_alltoall.py
168 +
169 commands:
170 # https://github.com/NVIDIA/nccl/issues/1838
171 - export NCCL_CUMEM_HOST_ENABLE=0
@@ -207,6 +209,9 @@ steps:
209 - pytest -v -s tests/distributed/test_context_parallel.py
210 - pytest -v -s tests/distributed/test_nccl_symm_mem_allreduce.py
211 - pytest -v -s tests/v1/distributed/test_dbo.py
212 + - pytest -v -s tests/distributed/test_mnnvl_alltoall.py
213 +
214 +
215
216 - label: 2 Node Test (4 GPUs)
217 timeout_in_minutes: 30
tests/distributed/test_mnnvl_alltoall.py new
+774
@@ -0,0 +1,774 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""
4 +Tests for MNNVL AllToAll operations.
5 +
6 +Requires: docker run ... --cap-add=SYS_PTRACE ...
7 +Run: pytest tests/distributed/test_mnnvl_alltoall.py -v
8 +"""
9 +
10 +import os
11 +import traceback
12 +
13 +import pytest
14 +import torch
15 +import torch.multiprocessing as mp
16 +
17 +from vllm.distributed import get_ep_group
18 +from vllm.utils.flashinfer import (
19 + has_flashinfer_nvlink_one_sided,
20 + has_flashinfer_nvlink_two_sided,
21 +)
22 +from vllm.utils.network_utils import get_open_port
23 +
24 +from ..utils import init_test_distributed_environment
25 +
26 +# ---------------------------------------------------------------------------
27 +# Helpers
28 +# ---------------------------------------------------------------------------
29 +
30 +
31 +def _has_sys_ptrace() -> bool:
32 + """Check for SYS_PTRACE capability (bit 19 in CapEff)."""
33 + try:
34 + with open("/proc/self/status") as f:
35 + for line in f:
36 + if line.startswith("CapEff:"):
37 + return bool(int(line.split()[1], 16) & (1 << 19))
38 + except Exception:
39 + pass
40 + return False
41 +
42 +
43 +def _spawn_workers(worker_fn, world_size, *, dp_size=None):
44 + """Spawn one process per GPU, run worker_fn, assert all succeed.
45 +
46 + Uses an mp.Queue to propagate worker tracebacks back to the parent
47 + so pytest shows the actual failure, not just an exit code.
48 + """
49 + if mp.get_start_method(allow_none=True) is None:
50 + mp.set_start_method("spawn")
51 +
52 + port = str(get_open_port())
53 + # Allocate a second port for DP master when dp_size is set, so the
54 + # distributed init port and DP port can't collide even under xdist.
55 + dp_port = str(get_open_port()) if dp_size is not None else None
56 + err_queue: mp.Queue = mp.Queue()
57 + procs = []
58 + for rank in range(world_size):
59 + p = mp.Process(
60 + target=_run_worker,
61 + args=(rank, world_size, port, worker_fn, dp_size, dp_port, err_queue),
62 + )
63 + p.start()
64 + procs.append(p)
65 + for p in procs:
66 + p.join()
67 +
68 + # Collect any errors from workers before asserting.
69 + errors = []
70 + while not err_queue.empty():
71 + errors.append(err_queue.get_nowait())
72 + err_queue.close()
73 + err_queue.join_thread()
74 + if errors:
75 + pytest.fail("Worker(s) failed:\n" + "\n---\n".join(errors))
76 +
77 +
78 +def _run_worker(rank, world_size, port, worker_fn, dp_size, dp_port, err_queue):
79 + """Per-process setup: device, distributed env, then call worker_fn.
80 +
81 + Args:
82 + dp_size: If set, initialize with tp=1 and data_parallel_size=dp_size.
83 + Otherwise use tp=world_size (default for EP-based tests).
84 + dp_port: Separate port for the DP master (only used when dp_size is set).
85 + err_queue: Queue for propagating tracebacks to the parent process.
86 + """
87 + try:
88 + os.environ.pop("CUDA_VISIBLE_DEVICES", None)
89 + torch.accelerator.set_device_index(rank)
90 + if dp_size is not None:
91 + _init_dp_environment(world_size, rank, port, dp_size, dp_port)
92 + else:
93 + init_test_distributed_environment(world_size, 1, rank, port)
94 + worker_fn(rank, world_size)
95 + torch.distributed.barrier()
96 + except Exception:
97 + err_queue.put(f"[Rank {rank}]\n{traceback.format_exc()}")
98 + # Don't re-raise: the parent reads errors from err_queue.
99 + # A non-zero exit from the re-raise would be redundant.
100 + import sys
101 +
102 + sys.exit(1)
103 +
104 +
105 +def _init_dp_environment(world_size, rank, port, dp_size, dp_port):
106 + """Initialize distributed env with data parallelism.
107 +
108 + Sets up tp=1, pp=1, dp=dp_size. Each process is one DP rank
109 + with local rank 0 within its (trivial) tp*pp group.
110 +
111 + Args:
112 + port: Port for torch.distributed init.
113 + dp_port: Separate port for the DP master group init.
114 + """
115 + from vllm.config import VllmConfig, set_current_vllm_config
116 + from vllm.config.parallel import ParallelConfig
117 + from vllm.distributed.parallel_state import (
118 + ensure_model_parallel_initialized,
119 + init_distributed_environment,
120 + )
121 +
122 + vllm_config = VllmConfig()
123 + vllm_config.parallel_config = ParallelConfig(
124 + data_parallel_size=dp_size,
125 + data_parallel_rank=rank,
126 + # Pre-populate port list so __post_init__ doesn't auto-generate
127 + # random ports. All DP ranks must agree on the same port.
128 + _data_parallel_master_port_list=[int(dp_port)],
129 + )
130 + with set_current_vllm_config(vllm_config):
131 + # rank=0 here because each DP rank has a single (tp=1,pp=1) process,
132 + # so the local rank within the tp*pp group is always 0.
133 + # init_distributed_environment will offset by data_parallel_rank.
134 + init_distributed_environment(
135 + world_size=1, # tp * pp = 1
136 + rank=0,
137 + distributed_init_method=f"tcp://localhost:{port}",
138 + local_rank=rank,
139 + )
140 + ensure_model_parallel_initialized(1, 1)
141 +
142 +
143 +def _make_forward_context(rank, world_size, num_tokens_per_rank):
144 + """Create a forward context with mock DP metadata for AgRs tests.
145 +
146 + Returns a context manager suitable for ``with`` statements.
147 + The real DPMetadata (with sp_local_sizes etc.) is created internally
148 + by set_forward_context from num_tokens_across_dp; the attn_metadata
149 + placeholder just satisfies the "attn_metadata is not None" guard.
150 + """
151 + from vllm.config.parallel import ParallelConfig
152 + from vllm.config.vllm import VllmConfig
153 + from vllm.forward_context import set_forward_context
154 +
155 + class _AttnMeta:
156 + """Minimal placeholder so set_forward_context's
157 + ``attn_metadata is not None`` guard (forward_context.py:334)
158 + is satisfied. The real DPMetadata is built from num_tokens_across_dp."""
159 +
160 + dp_metadata = None
161 +
162 + vllm_config = VllmConfig()
163 + vllm_config.parallel_config = ParallelConfig(
164 + data_parallel_size=world_size,
165 + is_moe_model=True,
166 + data_parallel_rank=rank,
167 + )
168 + return set_forward_context(
169 + _AttnMeta(),
170 + vllm_config,
171 + num_tokens=num_tokens_per_rank,
172 + num_tokens_across_dp=torch.tensor(
173 + [num_tokens_per_rank] * world_size, dtype=torch.int
174 + ),
175 + )
176 +
177 +
178 +# ---------------------------------------------------------------------------
179 +# Skip conditions
180 +# ---------------------------------------------------------------------------
181 +
182 +requires_multi_gpu = pytest.mark.skipif(
183 + torch.accelerator.device_count() < 2, reason="Need >= 2 GPUs"
184 +)
185 +requires_two_sided = pytest.mark.skipif(
186 + not has_flashinfer_nvlink_two_sided(),
187 + reason="FlashInfer NVLink two-sided not available",
188 +)
189 +requires_one_sided = pytest.mark.skipif(
190 + not has_flashinfer_nvlink_one_sided(),
191 + reason="FlashInfer NVLink one-sided not available",
192 +)
193 +requires_ptrace = pytest.mark.skipif(
194 + not _has_sys_ptrace(),
195 + reason="SYS_PTRACE required (docker run --cap-add=SYS_PTRACE)",
196 +)
197 +
198 +# NOTE: No module-level pytestmark here. The FlashInfer lifecycle tests have
199 +# their own @requires_two_sided / @requires_one_sided decorators, and
200 +# test_args_dispatch_combine uses only standard torch.distributed ops and
201 +# should run even when FlashInfer NVLink backends are not installed.
202 +
203 +
204 +# ---------------------------------------------------------------------------
205 +# Test 1: Two-sided manager lifecycle (init, cleanup, reinit, ensure_init)
206 +# ---------------------------------------------------------------------------
207 +#
208 +# Tests FlashInferNVLinkTwoSidedManager which wraps FlashInfer's MnnvlMoe.
209 +# initialize() allocates MNNVL shared workspaces via MnnvlMoe.get_moe_workspaces,
210 +# which uses pidfd_getfd() to share memory file descriptors across processes —
211 +# hence the SYS_PTRACE requirement.
212 +#
213 +# Uses EP group (get_ep_group) because the two-sided manager is constructed
214 +# with an EP-scoped communicator in production. With tp=world_size the EP
215 +# group spans all ranks, giving us a multi-rank group for testing.
216 +# ---------------------------------------------------------------------------
217 +
218 +
219 +def _two_sided_lifecycle_worker(rank, world_size):
220 + from vllm.distributed.device_communicators.all2all import (
221 + FlashInferNVLinkTwoSidedManager,
222 + )
223 +
224 + cpu_group = get_ep_group().cpu_group
225 + num_gpus = torch.accelerator.device_count()
226 + manager = FlashInferNVLinkTwoSidedManager(cpu_group)
227 +
228 + # Not initialized yet
229 + assert not manager.initialized
230 + assert manager.rank == rank
231 + assert manager.world_size == world_size
232 +
233 + # Initialize
234 + manager.initialize(world_size=world_size, rank=rank, gpus_per_node=num_gpus)
235 + assert manager.initialized
236 + assert manager.workspace_tensor is not None
237 + assert manager.prepare_workspace_tensor is not None
238 + assert manager.mapping is not None
239 +
240 + torch.distributed.barrier()
241 +
242 + # Cleanup
243 + manager.cleanup()
244 + assert not manager.initialized
245 + assert manager.workspace_tensor is None
246 + assert manager.prepare_workspace_tensor is None
247 +
248 + torch.distributed.barrier()
249 +
250 + # Reinitialize
251 + manager.initialize(world_size=world_size, rank=rank, gpus_per_node=num_gpus)
252 + assert manager.initialized
253 +
254 + torch.distributed.barrier()
255 +
256 + # ensure_alltoall_workspace_initialized is idempotent when already init'd
257 + assert manager.ensure_alltoall_workspace_initialized()
258 + assert manager.initialized
259 +
260 + manager.cleanup()
261 + assert not manager.initialized
262 +
263 +
264 +@requires_multi_gpu
265 +@requires_two_sided
266 +@requires_ptrace
267 +@pytest.mark.parametrize("world_size", [2])
268 +def test_two_sided_manager_lifecycle(world_size):
269 + """Test init, cleanup, reinit, and ensure_initialized idempotency."""
270 + _spawn_workers(_two_sided_lifecycle_worker, world_size)
271 +
272 +
273 +# ---------------------------------------------------------------------------
274 +# Test 2: One-sided manager lifecycle (init, cleanup, reinit)
275 +# ---------------------------------------------------------------------------
276 +#
277 +# Tests FlashInferNVLinkOneSidedManager which wraps FlashInfer's MoeAlltoAll.
278 +# initialize() creates MoeAlltoAll with an MnnvlConfig, which allocates MNNVL
279 +# shared workspaces — same cross-process memory sharing as two-sided, hence
280 +# the SYS_PTRACE requirement.
281 +#
282 +# Uses DP group (get_dp_group) because the one-sided manager's initialize()
283 +# internally calls get_dp_group() to set up the MnnvlConfig communicator.
284 +# We therefore need a real DP group with world_size > 1, which requires
285 +# dp_size=world_size via _init_dp_environment.
286 +# ---------------------------------------------------------------------------
287 +
288 +
289 +def _one_sided_lifecycle_worker(rank, world_size):
290 + from vllm.distributed.device_communicators.all2all import (
291 + FlashInferNVLinkOneSidedManager,
292 + )
293 + from vllm.distributed.parallel_state import get_dp_group
294 +
295 + cpu_group = get_dp_group().cpu_group
296 + manager = FlashInferNVLinkOneSidedManager(cpu_group)
297 +
298 + assert not manager.initialized
299 + assert manager.rank == rank
300 + assert manager.world_size == world_size
301 +
302 + init_kwargs = dict(
303 + max_num_tokens=1024,
304 + top_k=2,
305 + num_experts=world_size * 8,
306 + hidden_size=4096,
307 + )
308 +
309 + # Initialize
310 + manager.initialize(**init_kwargs)
311 + assert manager.initialized
312 + assert manager.moe_alltoall is not None
313 + assert manager.mapping is not None
314 +
315 + torch.distributed.barrier()
316 +
317 + # Cleanup
318 + manager.cleanup()
319 + assert not manager.initialized
320 + assert manager.moe_alltoall is None
321 +
322 + torch.distributed.barrier()
323 +
324 + # Reinitialize with different token count
325 + manager.initialize(**{**init_kwargs, "max_num_tokens": 2048})
326 + assert manager.initialized
327 +
328 + torch.distributed.barrier()
329 + manager.cleanup()
330 +
331 +
332 +@requires_multi_gpu
333 +@requires_one_sided
334 +@requires_ptrace
335 +@pytest.mark.parametrize("world_size", [2])
336 +def test_one_sided_manager_lifecycle(world_size):
337 + """Test init, cleanup, and reinit with different params."""
338 + _spawn_workers(
339 + _one_sided_lifecycle_worker,
340 + world_size,
341 + dp_size=world_size,
342 + )
343 +
344 +
345 +# ---------------------------------------------------------------------------
346 +# Test 3: AgRs dispatch/combine with value validation
347 +# ---------------------------------------------------------------------------
348 +#
349 +# Tests AgRsAll2AllManager which uses only standard torch.distributed
350 +# all_gatherv / reduce_scatterv — no FlashInfer or MNNVL dependency.
351 +# This test validates the reference all-to-all implementation that other
352 +# backends are compared against.
353 +# ---------------------------------------------------------------------------
354 +
355 +
356 +def _args_dispatch_combine_worker(rank, world_size):
357 + from vllm.distributed.device_communicators.all2all import AgRsAll2AllManager
358 + from vllm.forward_context import get_forward_context
359 +
360 + cpu_group = get_ep_group().cpu_group
361 + device = torch.device(f"cuda:{rank}")
362 +
363 + hidden_size = 64
364 + tokens_per_rank = 16
365 + experts_per_token = 2
366 + num_experts = world_size * 4
367 + total_tokens = world_size * tokens_per_rank
368 +
369 + # Deterministic per-rank data: rank r has value (r + 1)
370 + hidden = torch.full(
371 + (tokens_per_rank, hidden_size),
372 + float(rank + 1),
373 + device=device,
374 + dtype=torch.float32,
375 + )
376 + router = torch.full(
377 + (tokens_per_rank, num_experts),
378 + float(rank + 1) * 10,
379 + device=device,
380 + dtype=torch.float32,
381 + )
382 + weights = torch.full(
383 + (tokens_per_rank, experts_per_token),
384 + float(rank + 1) * 100,
385 + device=device,
386 + dtype=torch.float32,
387 + )
388 + ids = torch.full(
389 + (tokens_per_rank, experts_per_token),
390 + rank,
391 + device=device,
392 + dtype=torch.long,
393 + )
394 +
395 + with _make_forward_context(rank, world_size, tokens_per_rank):
396 + manager = AgRsAll2AllManager(cpu_group)
397 + dp_metadata = get_forward_context().dp_metadata
398 +
399 + with dp_metadata.sp_local_sizes(sequence_parallel_size=1):
400 + # -- dispatch_router_logits --
401 + d_hidden, d_router = manager.dispatch_router_logits(
402 + hidden.clone(),
403 + router.clone(),
404 + is_sequence_parallel=True,
405 + )
406 + assert d_hidden.shape == (total_tokens, hidden_size)
407 + assert d_router.shape == (total_tokens, num_experts)
408 +
409 + for r in range(world_size):
410 + s = r * tokens_per_rank
411 + e = (r + 1) * tokens_per_rank
412 + torch.testing.assert_close(
413 + d_hidden[s:e],
414 + torch.full_like(d_hidden[s:e], float(r + 1)),
415 + )
416 + torch.testing.assert_close(
417 + d_router[s:e],
418 + torch.full_like(d_router[s:e], float(r + 1) * 10),
419 + )
420 +
421 + # -- dispatch --
422 + d_hidden2, d_weights, d_ids = manager.dispatch(
423 + hidden.clone(),
424 + weights.clone(),
425 + ids.clone(),
426 + is_sequence_parallel=True,
427 + )
428 + assert d_hidden2.shape == (total_tokens, hidden_size)
429 + assert d_weights.shape == (total_tokens, experts_per_token)
430 + assert d_ids.shape == (total_tokens, experts_per_token)
431 +
432 + for r in range(world_size):
433 + s = r * tokens_per_rank
434 + e = (r + 1) * tokens_per_rank
435 + torch.testing.assert_close(
436 + d_weights[s:e],
437 + torch.full_like(d_weights[s:e], float(r + 1) * 100),
438 + )
439 + assert (d_ids[s:e] == r).all()
440 +
441 + # -- combine (reduce-scatter) --
442 + # Each token i has value i in all columns; after reduce-scatter
443 + # each rank gets its slice, summed across ranks.
444 + expert_out = (
445 + torch.arange(total_tokens, device=device, dtype=torch.float32)
446 + .unsqueeze(1)
447 + .expand(total_tokens, hidden_size)
448 + .contiguous()
449 + )
450 +
451 + combined = manager.combine(expert_out, is_sequence_parallel=True)
452 + assert combined.shape == (tokens_per_rank, hidden_size)
453 +
454 + for i in range(tokens_per_rank):
455 + expected_val = float(rank * tokens_per_rank + i) * world_size
456 + torch.testing.assert_close(
457 + combined[i],
458 + torch.full_like(combined[i], expected_val),
459 + )
460 +
461 + torch.distributed.barrier()
462 +
463 +
464 +@requires_multi_gpu
465 +@pytest.mark.parametrize("world_size", [2])
466 +def test_args_dispatch_combine(world_size):
467 + """Validate dispatch gathers all-rank data and combine reduces correctly."""
468 + _spawn_workers(_args_dispatch_combine_worker, world_size)
469 +
470 +
471 +# ---------------------------------------------------------------------------
472 +# Test 4: FlashInfer two-sided dispatch/combine data communication
473 +# ---------------------------------------------------------------------------
474 +#
475 +# Tests actual data flow through the FlashInfer NVLink two-sided backend
476 +# by calling flashinfer_alltoall_dispatch (with defer_input_quant=True to
477 +# skip quantization) and flashinfer_alltoall_combine, then verifying exact
478 +# round-trip values. Dispatch sends each token once per distinct expert
479 +# rank, and combine performs an unweighted sum, so:
480 +# dispatch(hidden) → identity → combine = hidden * num_distinct_ranks(i)
481 +# ---------------------------------------------------------------------------
482 +
483 +
484 +def _two_sided_data_worker(rank, world_size):
485 + from vllm.distributed.device_communicators.all2all import (
486 + FlashInferNVLinkTwoSidedManager,
487 + )
488 + from vllm.distributed.parallel_state import get_dp_group
489 + from vllm.forward_context import get_forward_context
490 + from vllm.model_executor.layers.fused_moe.config import (
491 + FusedMoEQuantConfig,
492 + FusedMoEQuantDesc,
493 + )
494 + from vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_two_sided import ( # noqa: E501
495 + flashinfer_alltoall_combine,
496 + flashinfer_alltoall_dispatch,
497 + )
498 +
499 + # Use DP group because MnnvlMoe workspace allocation calls get_dp_group()
500 + # internally and requires dp_size == ep_size.
501 + cpu_group = get_dp_group().cpu_group
502 + device = torch.device(f"cuda:{rank}")
503 + num_gpus = torch.accelerator.device_count()
504 +
505 + hidden_size = 128
506 + tokens_per_rank = 32
507 + experts_per_token = 2
508 + num_experts = world_size * 4
509 +
510 + # Initialize the FlashInfer two-sided manager
511 + manager = FlashInferNVLinkTwoSidedManager(cpu_group)
512 + manager.initialize(world_size=world_size, rank=rank, gpus_per_node=num_gpus)
513 + assert manager.initialized
514 +
515 + torch.distributed.barrier()
516 +
517 + # Create deterministic per-rank test data
518 + torch.manual_seed(rank + 42)
519 + hidden = torch.randn(
520 + tokens_per_rank,
521 + hidden_size,
522 + device=device,
523 + dtype=torch.bfloat16,
524 + )
525 + # Assign each token to experts spread across ranks so tokens move between GPUs
526 + topk_ids = torch.randint(
527 + 0,
528 + num_experts,
529 + (tokens_per_rank, experts_per_token),
530 + device=device,
531 + dtype=torch.int32,
532 + )
533 + topk_weights = torch.rand(
534 + tokens_per_rank,
535 + experts_per_token,
536 + device=device,
537 + dtype=torch.float32,
538 + )
539 +
540 + # Unquantized config: quant_dtype=None means moe_kernel_quantize_input is a no-op
541 + no_quant = FusedMoEQuantDesc()
542 + quant_config = FusedMoEQuantConfig(
543 + _a1=no_quant,
544 + _a2=no_quant,
545 + _w1=no_quant,
546 + _w2=no_quant,
547 + )
548 + assert quant_config.quant_dtype is None # sanity: no quantization
549 +
550 + with _make_forward_context(rank, world_size, tokens_per_rank):
551 + dp_metadata = get_forward_context().dp_metadata
552 +
553 + with dp_metadata.sp_local_sizes(sequence_parallel_size=1):
554 + local_sizes = dp_metadata.get_chunk_sizes_across_dp_rank()
555 +
556 + # --- FlashInfer two-sided dispatch ---
557 + alltoall_info, fi_topk_ids, fi_topk_weights, fi_hidden, fi_scale = (
558 + flashinfer_alltoall_dispatch(
559 + manager,
560 + local_sizes,
561 + hidden.clone(),
562 + None, # no global scale
563 + topk_ids.clone(),
564 + topk_weights.clone(),
565 + experts_per_token,
566 + num_experts,
567 + quant_config,
568 + defer_input_quant=True,
569 + )
570 + )
571 + assert fi_scale is None # deferred quant: no scale produced
572 + assert fi_hidden is not None
573 + assert fi_hidden.shape[1] == hidden_size
574 + assert fi_hidden.numel() > 0
575 +
576 + # --- Round-trip exact verification ---
577 + # The all-to-all sends each token once per *distinct* expert
578 + # rank. Combine performs an unweighted sum of the per-rank
579 + # contributions. With identity expert (feeding dispatched
580 + # hidden straight back):
581 + # result[i] = hidden[i] * num_distinct_expert_ranks(i)
582 + combined = flashinfer_alltoall_combine(
583 + manager,
584 + fi_hidden,
585 + top_k=experts_per_token,
586 + token_count=tokens_per_rank,
587 + alltoall_info=alltoall_info,
588 + )
589 + assert combined.shape == (tokens_per_rank, hidden_size)
590 +
591 + experts_per_rank = num_experts // world_size
592 + expert_ranks = topk_ids // experts_per_rank # (tokens, top_k)
593 + num_distinct = torch.tensor(
594 + [len(set(row.tolist())) for row in expert_ranks],
595 + device=device,
596 + dtype=torch.float32,
597 + ).unsqueeze(1) # (tokens, 1)
598 + expected = (hidden.float() * num_distinct).to(hidden.dtype)
599 + torch.testing.assert_close(combined, expected)
600 +
601 + # --- Linearity check with scaled expert output ---
602 + # Scaling the expert output by a constant should scale the
603 + # combined result by the same constant.
604 + scale = 3.0
605 + combined_scaled = flashinfer_alltoall_combine(
606 + manager,
607 + fi_hidden * scale,
608 + top_k=experts_per_token,
609 + token_count=tokens_per_rank,
610 + alltoall_info=alltoall_info,
611 + )
612 + expected_scaled = (hidden.float() * num_distinct * scale).to(hidden.dtype)
613 + torch.testing.assert_close(combined_scaled, expected_scaled)
614 +
615 + torch.distributed.barrier()
616 +
617 + manager.cleanup()
618 +
619 +
620 +@requires_multi_gpu
621 +@requires_two_sided
622 +@requires_ptrace
623 +@pytest.mark.parametrize("world_size", [2])
624 +def test_two_sided_dispatch_combine(world_size):
625 + """Test FlashInfer two-sided dispatch/combine with exact value verification."""
626 + _spawn_workers(_two_sided_data_worker, world_size, dp_size=world_size)
627 +
628 +
629 +# ---------------------------------------------------------------------------
630 +# Test 5: FlashInfer one-sided dispatch/combine data communication
631 +# ---------------------------------------------------------------------------
632 +#
633 +# Tests actual data flow through the FlashInfer NVLink one-sided backend
634 +# by calling MoeAlltoAll.dispatch() and MoeAlltoAll.combine() directly
635 +# with synthetic payloads, then verifying shapes and round-trip consistency.
636 +# ---------------------------------------------------------------------------
637 +
638 +
639 +def _one_sided_data_worker(rank, world_size):
640 + from vllm.distributed.device_communicators.all2all import (
641 + FlashInferNVLinkOneSidedManager,
642 + )
643 + from vllm.distributed.parallel_state import get_dp_group
644 + from vllm.forward_context import get_forward_context
645 +
646 + cpu_group = get_dp_group().cpu_group
647 + device = torch.device(f"cuda:{rank}")
648 +
649 + hidden_size = 256
650 + tokens_per_rank = 32
651 + experts_per_token = 2
652 + num_experts = world_size * 8
653 +
654 + # Initialize the one-sided manager
655 + manager = FlashInferNVLinkOneSidedManager(cpu_group)
656 + manager.initialize(
657 + max_num_tokens=tokens_per_rank,
658 + top_k=experts_per_token,
659 + num_experts=num_experts,
660 + hidden_size=hidden_size,
661 + )
662 + assert manager.initialized
663 + assert manager.moe_alltoall is not None
664 +
665 + with _make_forward_context(rank, world_size, tokens_per_rank):
666 + dp_metadata = get_forward_context().dp_metadata
667 +
668 + with dp_metadata.sp_local_sizes(sequence_parallel_size=1):
669 + local_sizes = dp_metadata.get_chunk_sizes_across_dp_rank()
670 + runtime_max_tokens = max(local_sizes)
671 +
672 + # Create test data with raw tensors matching the nvfp4 payload
673 + # sizes the workspace was allocated for:
674 + # a1q: (tokens, hidden_size // 2) — nvfp4 hidden states
675 + # a1q_scale: (tokens, hidden_size // 16) — fp8 scaling factors
676 + torch.manual_seed(rank + 42)
677 + a1q = torch.randint(
678 + 0,
679 + 256,
680 + (tokens_per_rank, hidden_size // 2),
681 + device=device,
682 + dtype=torch.uint8,
683 + )
684 + a1q_scale = torch.randint(
685 + 0,
686 + 256,
687 + (tokens_per_rank, hidden_size // 16),
688 + device=device,
689 + dtype=torch.uint8,
690 + )
691 + topk_ids = torch.randint(
692 + 0,
693 + num_experts,
694 + (tokens_per_rank, experts_per_token),
695 + device=device,
696 + dtype=torch.int32,
697 + )
698 + topk_weights = torch.rand(
699 + tokens_per_rank,
700 + experts_per_token,
701 + device=device,
702 + dtype=torch.float32,
703 + )
704 +
705 + # --- One-sided dispatch ---
706 + payloads = [a1q, a1q_scale, topk_ids, topk_weights]
707 + recv_payloads = manager.moe_alltoall.dispatch(
708 + token_selected_experts=topk_ids,
709 + input_payloads=payloads,
710 + runtime_max_tokens_per_rank=runtime_max_tokens,
711 + )
712 + assert len(recv_payloads) == 4
713 + recv_a1q, recv_scale, recv_ids, recv_weights = recv_payloads
714 + assert recv_a1q.numel() > 0
715 + assert recv_ids.numel() > 0
716 +
717 + # --- Round-trip exact verification ---
718 + # The dispatch routes each token once per *distinct* expert
719 + # rank. Combine performs an unweighted sum of per-rank
720 + # contributions. With constant expert output (all 1s):
721 + # result[i] = 1.0 * num_distinct_expert_ranks(i)
722 + expert_output = torch.ones(
723 + world_size,
724 + runtime_max_tokens,
725 + hidden_size,
726 + device=device,
727 + dtype=torch.bfloat16,
728 + )
729 + combined = manager.moe_alltoall.combine(
730 + payload=expert_output,
731 + runtime_max_tokens_per_rank=runtime_max_tokens,
732 + )
733 + assert combined.shape == (tokens_per_rank, hidden_size)
734 +
735 + experts_per_rank = num_experts // world_size
736 + expert_ranks = topk_ids // experts_per_rank # (tokens, top_k)
737 + num_distinct = torch.tensor(
738 + [len(set(row.tolist())) for row in expert_ranks],
739 + device=device,
740 + dtype=torch.bfloat16,
741 + ).unsqueeze(1) # (tokens, 1)
742 + expected = num_distinct.expand_as(combined)
743 + torch.testing.assert_close(combined, expected)
744 +
745 + # --- Linearity check with scaled expert output ---
746 + # Scaling the expert output by a constant should scale the
747 + # combined result by the same constant.
748 + # Re-dispatch to reset internal state (one-sided requires a
749 + # fresh dispatch before each combine).
750 + manager.moe_alltoall.dispatch(
751 + token_selected_experts=topk_ids,
752 + input_payloads=payloads,
753 + runtime_max_tokens_per_rank=runtime_max_tokens,
754 + )
755 + scale = 3.0
756 + combined_scaled = manager.moe_alltoall.combine(
757 + payload=expert_output * scale,
758 + runtime_max_tokens_per_rank=runtime_max_tokens,
759 + )
760 + expected_scaled = (expected * scale).to(torch.bfloat16)
761 + torch.testing.assert_close(combined_scaled, expected_scaled)
762 +
763 + torch.distributed.barrier()
764 +
765 + manager.cleanup()
766 +
767 +
768 +@requires_multi_gpu
769 +@requires_one_sided
770 +@requires_ptrace
771 +@pytest.mark.parametrize("world_size", [2])
772 +def test_one_sided_dispatch_combine(world_size):
773 + """Test FlashInfer one-sided dispatch/combine with actual data flow."""
774 + _spawn_workers(_one_sided_data_worker, world_size, dp_size=world_size)