Skip to content

Commit 8907026

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 e3f6026 commit 8907026

4 files changed

Lines changed: 225 additions & 22 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
4+
from unittest.mock import Mock
5+
6+
import pytest
7+
import torch
8+
9+
from vllm.distributed.device_communicators import (
10+
cuda_communicator as cuda_communicator_module,
11+
)
12+
from vllm.distributed.device_communicators import flashinfer_all_reduce
13+
from vllm.distributed.device_communicators.cuda_communicator import CudaCommunicator
14+
15+
16+
def _mock_sm103_tp8_mnnvl(
17+
monkeypatch: pytest.MonkeyPatch, tp_group_node_count: int
18+
) -> object:
19+
capability = Mock()
20+
capability.to_int.return_value = 103
21+
group = object()
22+
monkeypatch.setattr(flashinfer_all_reduce, "fi_ar_available", True)
23+
monkeypatch.setattr(flashinfer_all_reduce.current_platform, "is_cuda", lambda: True)
24+
monkeypatch.setattr(
25+
flashinfer_all_reduce.current_platform,
26+
"get_device_capability",
27+
lambda: capability,
28+
)
29+
monkeypatch.setattr(flashinfer_all_reduce.dist, "get_world_size", lambda _: 8)
30+
monkeypatch.setattr(flashinfer_all_reduce.dist, "get_rank", lambda _: 0)
31+
monkeypatch.setattr(
32+
flashinfer_all_reduce, "get_node_count", lambda: tp_group_node_count
33+
)
34+
monkeypatch.setattr(
35+
flashinfer_all_reduce, "_node_count", lambda _: tp_group_node_count
36+
)
37+
monkeypatch.setattr(
38+
flashinfer_all_reduce, "_resolve_fi_ar_backend", lambda: ("mnnvl", False)
39+
)
40+
monkeypatch.setattr(
41+
flashinfer_all_reduce.PassConfig,
42+
"default_fi_allreduce_fusion_max_size_mb",
43+
lambda: {8: 2},
44+
)
45+
return group
46+
47+
48+
@pytest.mark.parametrize(
49+
("tp_group_node_count", "expected_size_mb", "expected_exclusive"),
50+
[(2, 64, True), (1, 2, False), (4, 2, False)],
51+
)
52+
def test_standalone_threshold_uses_tp_group_topology(
53+
monkeypatch: pytest.MonkeyPatch,
54+
tp_group_node_count: int,
55+
expected_size_mb: int,
56+
expected_exclusive: bool,
57+
) -> None:
58+
group = _mock_sm103_tp8_mnnvl(monkeypatch, tp_group_node_count)
59+
60+
communicator = flashinfer_all_reduce.FlashInferAllReduce(
61+
group=group, # type: ignore[arg-type]
62+
device="cuda",
63+
)
64+
65+
assert (
66+
communicator.max_workspace_size == expected_size_mb * flashinfer_all_reduce.MiB
67+
)
68+
assert communicator.max_size_is_exclusive is expected_exclusive
69+
70+
71+
@pytest.mark.parametrize(
72+
("standalone_enabled", "expected_max_token_num"),
73+
[(True, 4681), (False, 128)],
74+
)
75+
def test_workspace_promotion_respects_standalone_flag(
76+
monkeypatch: pytest.MonkeyPatch,
77+
standalone_enabled: bool,
78+
expected_max_token_num: int,
79+
) -> None:
80+
group = _mock_sm103_tp8_mnnvl(monkeypatch, tp_group_node_count=2)
81+
monkeypatch.setattr(
82+
flashinfer_all_reduce.envs,
83+
"VLLM_ALLREDUCE_USE_FLASHINFER",
84+
standalone_enabled,
85+
)
86+
87+
max_token_num = flashinfer_all_reduce._promote_max_token_num_for_standalone(
88+
max_token_num=128,
89+
world_size=8,
90+
hidden_dim=7168,
91+
dtype=torch.bfloat16,
92+
backend="mnnvl",
93+
group=group, # type: ignore[arg-type]
94+
)
95+
96+
assert max_token_num == expected_max_token_num
97+
98+
99+
def test_enabled_flashinfer_dispatches_before_nccl(
100+
monkeypatch: pytest.MonkeyPatch,
101+
) -> None:
102+
input_tensor = torch.empty(1)
103+
output = torch.empty(2)
104+
fi_ar_comm = Mock(disabled=False)
105+
fi_ar_comm.should_use_fi_ar.return_value = True
106+
fi_ar_comm.all_reduce.return_value = output
107+
communicator = CudaCommunicator.__new__(CudaCommunicator)
108+
communicator.pynccl_comm = Mock(world_size=8, disabled=False)
109+
communicator.fi_ar_comm = fi_ar_comm
110+
communicator.qr_comm = None
111+
communicator.aiter_ar_comm = None
112+
communicator.ca_comm = None
113+
communicator.symm_mem_comm = None
114+
nccl_selector = Mock(return_value=True)
115+
monkeypatch.setattr(
116+
cuda_communicator_module,
117+
"should_nccl_symm_mem_allreduce",
118+
nccl_selector,
119+
)
120+
121+
assert communicator.all_reduce(input_tensor) is output
122+
nccl_selector.assert_not_called()

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: 13 additions & 14 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,6 +276,16 @@ 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+
if (
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+
out = fi_ar_comm.all_reduce(input_)
286+
assert out is not None
287+
return out
288+
279289
# since currently we perform copy input -> symm_input -> out-of-place AR
280290
# return symm_output, we don't need to check if input is symmetric
281291
if self.pynccl_comm is not None and should_nccl_symm_mem_allreduce(
@@ -284,8 +294,6 @@ def all_reduce(self, input_):
284294
out = torch.ops.vllm.all_reduce_symmetric_with_copy(input_)
285295
if out is not None:
286296
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*)
289297
qr_comm = self.qr_comm
290298
if (
291299
qr_comm is not None
@@ -295,15 +303,6 @@ def all_reduce(self, input_):
295303
out = qr_comm.quick_all_reduce(input_)
296304
assert out is not None
297305
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-
):
304-
out = fi_ar_comm.all_reduce(input_)
305-
assert out is not None
306-
return out
307306
aiter_ar_comm = self.aiter_ar_comm
308307
if (
309308
aiter_ar_comm is not None

vllm/distributed/device_communicators/flashinfer_all_reduce.py

Lines changed: 84 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,16 @@
2326
# The empirical value for small batch
2427
PDL_ADVANCE_LAUNCH_TOKENS = 16
2528

29+
MiB = 1024 * 1024
30+
31+
# Standalone FlashInfer all-reduce supports these dtypes. Fusion patterns have
32+
# their own, narrower dtype checks.
33+
FI_ALLREDUCE_SUPPORTED_DTYPES = (
34+
torch.float16,
35+
torch.bfloat16,
36+
torch.float32,
37+
)
38+
2639
fi_ar_available = False
2740
try:
2841
import flashinfer.comm as flashinfer_comm # type: ignore[no-redef]
@@ -43,6 +56,44 @@
4356
_fi_ar_workspace_groups: dict[int, ProcessGroup] = {}
4457

4558

59+
def _get_tuned_standalone_max_size_mb(
60+
world_size: int,
61+
backend: str,
62+
group: ProcessGroup,
63+
) -> float | None:
64+
if backend != "mnnvl":
65+
return None
66+
capability = current_platform.get_device_capability()
67+
if capability is None:
68+
return None
69+
return FI_MNNVL_ALLREDUCE_MAX_SIZE_MB.get(
70+
(capability.to_int(), world_size, _node_count(group))
71+
)
72+
73+
74+
def _promote_max_token_num_for_standalone(
75+
max_token_num: int,
76+
world_size: int,
77+
hidden_dim: int,
78+
dtype: torch.dtype,
79+
backend: str,
80+
group: ProcessGroup,
81+
) -> int:
82+
"""Make a first workspace allocation safe for standalone all-reduce."""
83+
if not envs.VLLM_ALLREDUCE_USE_FLASHINFER:
84+
return max_token_num
85+
max_size_mb = _get_tuned_standalone_max_size_mb(
86+
world_size,
87+
backend,
88+
group,
89+
)
90+
if max_size_mb is None:
91+
return max_token_num
92+
element_size = torch.empty((), dtype=dtype, device="cpu").element_size()
93+
standalone_max_token_num = int(max_size_mb * MiB) // (hidden_dim * element_size)
94+
return max(max_token_num, standalone_max_token_num)
95+
96+
4697
def _create_workspace(
4798
backend: str,
4899
world_size: int,
@@ -165,6 +216,15 @@ def get_fi_ar_workspace(
165216
"'trtllm' backend. Please use 'mnnvl' backend instead."
166217
)
167218

219+
max_token_num = _promote_max_token_num_for_standalone(
220+
max_token_num,
221+
world_size,
222+
hidden_dim,
223+
dtype,
224+
backend,
225+
group,
226+
)
227+
168228
def _get_or_create(be: str):
169229
# Reuse the quant workspace if it was already created with the same backend
170230
if _fi_ar_quant_workspace is not None and _fi_ar_quant_workspace.backend == be:
@@ -346,20 +406,27 @@ def __init__(
346406
if self.world_size == 1:
347407
return
348408

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
409+
default_max_size_mb = PassConfig.default_fi_allreduce_fusion_max_size_mb().get(
410+
self.world_size
354411
)
355-
if not max_workspace_size:
412+
if not default_max_size_mb:
356413
logger.warning(
357414
"FlashInfer All Reduce is disabled because it "
358415
"is not supported for world_size=%d.",
359416
self.world_size,
360417
)
361418
return
362-
self.max_workspace_size = max_workspace_size * MiB
419+
420+
backend, _ = _resolve_fi_ar_backend()
421+
tuned_max_size_mb = _get_tuned_standalone_max_size_mb(
422+
self.world_size,
423+
backend,
424+
self.group,
425+
)
426+
max_workspace_size_mb = tuned_max_size_mb or default_max_size_mb
427+
assert max_workspace_size_mb is not None
428+
self.max_workspace_size = int(max_workspace_size_mb * MiB)
429+
self.max_size_is_exclusive = tuned_max_size_mb is not None
363430
self.max_num_tokens = 0
364431
self.disabled = False
365432

@@ -394,6 +461,15 @@ def should_use_fi_ar(self, input_tensor: torch.Tensor) -> bool:
394461
if len(input_tensor.shape) != 2:
395462
return False
396463

464+
if input_tensor.dtype not in FI_ALLREDUCE_SUPPORTED_DTYPES:
465+
return False
466+
467+
if input_tensor.nbytes > self.max_workspace_size or (
468+
self.max_size_is_exclusive
469+
and input_tensor.nbytes >= self.max_workspace_size
470+
):
471+
return False
472+
397473
num_tokens, hidden_dim = input_tensor.shape
398474
if not self.max_num_tokens:
399475
element_size = torch.tensor([], dtype=input_tensor.dtype).element_size()

0 commit comments

Comments
 (0)