Skip to content

Commit 66cb43b

Browse files
GirasoleYcodex
andcommitted
[Perf] Tune FlashInfer all-reduce selection on SM103 TP8
Prefer standalone FlashInfer all-reduce ahead of NCCL symmetric memory when the existing FlashInfer gate is enabled. Use a topology-keyed strict 64 MiB cutoff for two-node SM103 TP8 MNNVL and size the shared workspace accordingly. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Summer Yang <girasoleyang@gmail.com>
1 parent a556f3f commit 66cb43b

4 files changed

Lines changed: 155 additions & 21 deletions

File tree

tests/distributed/test_comm_ops.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,83 @@ def test_cuda_communicator_checkpoints_flashinfer_workspaces(
318318
workspace.checkpoint_restore.assert_called_once_with(group)
319319

320320

321+
@pytest.mark.parametrize(
322+
("backend", "capability", "nodes", "expected"),
323+
[
324+
("mnnvl", 103, 2, 64 * flashinfer_all_reduce.MiB - 1),
325+
("mnnvl", 103, 1, None),
326+
("trtllm", 103, 2, None),
327+
("mnnvl", 90, 2, None),
328+
],
329+
)
330+
def test_flashinfer_standalone_size_tuning(
331+
monkeypatch: pytest.MonkeyPatch,
332+
backend: str,
333+
capability: int,
334+
nodes: int,
335+
expected: int | None,
336+
) -> None:
337+
monkeypatch.setattr(
338+
flashinfer_all_reduce,
339+
"current_platform",
340+
Mock(get_device_capability=lambda: Mock(to_int=lambda: capability)),
341+
)
342+
monkeypatch.setattr(flashinfer_all_reduce, "_node_count", lambda _: nodes)
343+
344+
assert (
345+
flashinfer_all_reduce._get_tuned_standalone_max_size(8, backend, Mock())
346+
== expected
347+
)
348+
349+
350+
@pytest.mark.parametrize(("enabled", "expected"), [(True, 4681), (False, 128)])
351+
def test_flashinfer_standalone_workspace_size(
352+
monkeypatch: pytest.MonkeyPatch, enabled: bool, expected: int
353+
) -> None:
354+
create_workspace = Mock(return_value=Mock(backend="mnnvl"))
355+
monkeypatch.setattr(
356+
flashinfer_all_reduce.envs, "VLLM_ALLREDUCE_USE_FLASHINFER", enabled
357+
)
358+
monkeypatch.setattr(flashinfer_all_reduce, "_fi_ar_workspace", None)
359+
monkeypatch.setattr(flashinfer_all_reduce, "_fi_ar_quant_workspace", None)
360+
monkeypatch.setattr(
361+
flashinfer_all_reduce,
362+
"_resolve_fi_ar_backend",
363+
Mock(return_value=("mnnvl", False)),
364+
)
365+
monkeypatch.setattr(flashinfer_all_reduce, "get_node_count", lambda: 2)
366+
monkeypatch.setattr(
367+
flashinfer_all_reduce,
368+
"_get_tuned_standalone_max_size",
369+
Mock(return_value=64 * flashinfer_all_reduce.MiB - 1),
370+
)
371+
monkeypatch.setattr(flashinfer_all_reduce, "_create_workspace", create_workspace)
372+
373+
flashinfer_all_reduce.get_fi_ar_workspace(8, 0, 128, 7168, torch.bfloat16, Mock())
374+
375+
assert create_workspace.call_args.args[3] == expected
376+
377+
378+
def test_flashinfer_all_reduce_precedes_nccl(monkeypatch: pytest.MonkeyPatch) -> None:
379+
output = torch.empty(2)
380+
fi_ar_comm = Mock(disabled=False)
381+
fi_ar_comm.should_use_fi_ar.return_value = True
382+
fi_ar_comm.all_reduce.return_value = output
383+
communicator = CudaCommunicator.__new__(CudaCommunicator)
384+
communicator.fi_ar_comm = fi_ar_comm
385+
communicator.pynccl_comm = Mock(world_size=8)
386+
communicator.qr_comm = None
387+
nccl_selector = Mock(return_value=True)
388+
monkeypatch.setattr(
389+
"vllm.distributed.device_communicators.cuda_communicator."
390+
"should_nccl_symm_mem_allreduce",
391+
nccl_selector,
392+
)
393+
394+
assert communicator.all_reduce(torch.empty(1)) is output
395+
nccl_selector.assert_not_called()
396+
397+
321398
def test_async_intermediate_tensors_lazy_wait() -> None:
322399
work = _DummyWork()
323400
post_calls = {"n": 0}

vllm/distributed/device_communicators/all_reduce_utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@
8282
},
8383
}
8484

85+
# Per-rank input limit for standalone FlashInfer MNNVL all-reduce.
86+
# The key is (compute capability, world size, node count).
87+
FI_MNNVL_ALLREDUCE_MAX_SIZE_MB: dict[tuple[int, int, int], float] = {
88+
(103, 8, 2): 64,
89+
}
90+
8591
# NCCL symmetric memory allreduce configuration based on H100 and GB200 benchmarks.
8692
# PyNCCL-symm outperforms custom_AR for small and large tensor sizes,
8793
# while custom_AR wins for mid-range sizes.

vllm/distributed/device_communicators/cuda_communicator.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -220,15 +220,17 @@ def _log_all_reduce_backend_selection(self) -> None:
220220
depends on the input tensor.
221221
"""
222222
all_potential_ar_backends = [
223+
"FLASHINFER",
223224
"NCCL_SYMM_MEM",
224225
"QUICK_REDUCE",
225-
"FLASHINFER",
226226
"AITER_CUSTOM",
227227
"CUSTOM",
228228
"SYMM_MEM",
229229
"PYNCCL",
230230
]
231231
enabled_ar_backends: list[str] = []
232+
if self.fi_ar_comm is not None and not self.fi_ar_comm.disabled:
233+
enabled_ar_backends.append("FLASHINFER")
232234
# Mirror the static preconditions of `should_nccl_symm_mem_allreduce`:
233235
# VLLM_BATCH_INVARIANT off, NCCL symm mem enabled, world_size meets
234236
# min_world_size, and world_size either has a tuned entry in
@@ -255,8 +257,6 @@ def _log_all_reduce_backend_selection(self) -> None:
255257
enabled_ar_backends.append("NCCL_SYMM_MEM")
256258
if self.qr_comm is not None and not self.qr_comm.disabled:
257259
enabled_ar_backends.append("QUICK_REDUCE")
258-
if self.fi_ar_comm is not None and not self.fi_ar_comm.disabled:
259-
enabled_ar_backends.append("FLASHINFER")
260260
if self.aiter_ar_comm is not None and not self.aiter_ar_comm.disabled:
261261
enabled_ar_backends.append("AITER_CUSTOM")
262262
if self.ca_comm is not None and not self.ca_comm.disabled:
@@ -276,16 +276,23 @@ def _log_all_reduce_backend_selection(self) -> None:
276276
)
277277

278278
def all_reduce(self, input_):
279+
fi_ar_comm = self.fi_ar_comm
280+
use_fi_ar = (
281+
fi_ar_comm is not None
282+
and not fi_ar_comm.disabled
283+
and fi_ar_comm.should_use_fi_ar(input_)
284+
)
285+
279286
# since currently we perform copy input -> symm_input -> out-of-place AR
280287
# return symm_output, we don't need to check if input is symmetric
281-
if self.pynccl_comm is not None and should_nccl_symm_mem_allreduce(
282-
self.pynccl_comm.world_size, input_
288+
if (
289+
self.pynccl_comm is not None
290+
and not use_fi_ar
291+
and should_nccl_symm_mem_allreduce(self.pynccl_comm.world_size, input_)
283292
):
284293
out = torch.ops.vllm.all_reduce_symmetric_with_copy(input_)
285294
if out is not None:
286295
return out
287-
# always try quick reduce first, then flashinfer, then the AITER or vLLM
288-
# custom allreduce, and then pynccl. (quick reduce just for ROCM MI3*)
289296
qr_comm = self.qr_comm
290297
if (
291298
qr_comm is not None
@@ -295,12 +302,8 @@ def all_reduce(self, input_):
295302
out = qr_comm.quick_all_reduce(input_)
296303
assert out is not None
297304
return out
298-
fi_ar_comm = self.fi_ar_comm
299-
if (
300-
fi_ar_comm is not None
301-
and not fi_ar_comm.disabled
302-
and fi_ar_comm.should_use_fi_ar(input_)
303-
):
305+
if use_fi_ar:
306+
assert fi_ar_comm is not None
304307
out = fi_ar_comm.all_reduce(input_)
305308
assert out is not None
306309
return out

vllm/distributed/device_communicators/flashinfer_all_reduce.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515
import vllm.envs as envs
1616
from vllm.config.compilation import PassConfig
17-
from vllm.distributed.parallel_state import get_node_count
17+
from vllm.distributed.device_communicators.all_reduce_utils import (
18+
FI_MNNVL_ALLREDUCE_MAX_SIZE_MB,
19+
)
20+
from vllm.distributed.parallel_state import _node_count, get_node_count
1821
from vllm.logger import init_logger
1922
from vllm.platforms import current_platform
2023

@@ -23,6 +26,8 @@
2326
# The empirical value for small batch
2427
PDL_ADVANCE_LAUNCH_TOKENS = 16
2528

29+
MiB = 1024 * 1024
30+
2631
fi_ar_available = False
2732
try:
2833
import flashinfer.comm as flashinfer_comm # type: ignore[no-redef]
@@ -43,6 +48,23 @@
4348
_fi_ar_workspace_groups: dict[int, ProcessGroup] = {}
4449

4550

51+
def _get_tuned_standalone_max_size(
52+
world_size: int,
53+
backend: str,
54+
group: ProcessGroup,
55+
) -> int | None:
56+
if backend != "mnnvl":
57+
return None
58+
capability = current_platform.get_device_capability()
59+
if capability is None:
60+
return None
61+
max_size_mb = FI_MNNVL_ALLREDUCE_MAX_SIZE_MB.get(
62+
(capability.to_int(), world_size, _node_count(group))
63+
)
64+
# Tuned cutoffs are exclusive; store the largest accepted size.
65+
return None if max_size_mb is None else int(max_size_mb * MiB) - 1
66+
67+
4668
def _create_workspace(
4769
backend: str,
4870
world_size: int,
@@ -165,6 +187,14 @@ def get_fi_ar_workspace(
165187
"'trtllm' backend. Please use 'mnnvl' backend instead."
166188
)
167189

190+
if (
191+
envs.VLLM_ALLREDUCE_USE_FLASHINFER
192+
and (max_size := _get_tuned_standalone_max_size(world_size, backend, group))
193+
is not None
194+
):
195+
element_size = torch.empty((), dtype=dtype, device="cpu").element_size()
196+
max_token_num = max(max_token_num, max_size // (hidden_dim * element_size))
197+
168198
def _get_or_create(be: str):
169199
# Reuse the quant workspace if it was already created with the same backend
170200
if _fi_ar_quant_workspace is not None and _fi_ar_quant_workspace.backend == be:
@@ -346,20 +376,28 @@ def __init__(
346376
if self.world_size == 1:
347377
return
348378

349-
# Use the same threshold as the allreduce-rms fusion pass
350-
# TODO: tune the threshold
351-
MiB = 1024 * 1024
352-
max_workspace_size = PassConfig.default_fi_allreduce_fusion_max_size_mb().get(
353-
self.world_size, None
379+
default_max_size_mb = PassConfig.default_fi_allreduce_fusion_max_size_mb().get(
380+
self.world_size
354381
)
355-
if not max_workspace_size:
382+
if not default_max_size_mb:
356383
logger.warning(
357384
"FlashInfer All Reduce is disabled because it "
358385
"is not supported for world_size=%d.",
359386
self.world_size,
360387
)
361388
return
362-
self.max_workspace_size = max_workspace_size * MiB
389+
390+
backend, _ = _resolve_fi_ar_backend()
391+
tuned_max_size = _get_tuned_standalone_max_size(
392+
self.world_size,
393+
backend,
394+
self.group,
395+
)
396+
self.max_workspace_size = (
397+
tuned_max_size
398+
if tuned_max_size is not None
399+
else int(default_max_size_mb * MiB)
400+
)
363401
self.max_num_tokens = 0
364402
self.disabled = False
365403

@@ -394,6 +432,16 @@ def should_use_fi_ar(self, input_tensor: torch.Tensor) -> bool:
394432
if len(input_tensor.shape) != 2:
395433
return False
396434

435+
if input_tensor.dtype not in (
436+
torch.float16,
437+
torch.bfloat16,
438+
torch.float32,
439+
):
440+
return False
441+
442+
if input_tensor.nbytes > self.max_workspace_size:
443+
return False
444+
397445
num_tokens, hidden_dim = input_tensor.shape
398446
if not self.max_num_tokens:
399447
element_size = torch.tensor([], dtype=input_tensor.dtype).element_size()

0 commit comments

Comments
 (0)