[Distributed] Enable custom all-reduce under VLLM_BATCH_INVARIANT (#58623)
Signed-off-by: Flora Feng <4florafeng@gmail.com> Signed-off-by: sfeng33 <4florafeng@gmail.com>
Flora Feng committed
Oct 1, 2026 at 22:25 UTC
785b990116ab2b7c77f5d946b2f52c2a8f9b9515
7 files changed
+130
-31
docs/features/batch_invariance.md
+6
-2
@@ -146,11 +146,15 @@ When batch invariance is enabled, vLLM:
146
147
1. Uses deterministic kernel implementations for attention and other operations
148
2. Ensures consistent numerical behavior across different batch sizes
149
-3. Disables certain optimizations that may introduce non-determinism (such as custom all-reduce operations in tensor parallel mode, and sequence parallelism / async TP, whose reduce-scatter path is not batch-invariant)
150
-4. On CUDA devices with tuned matmul table entries for the model's bf16 unquantized forward linear layers (Ada, Hopper, Blackwell),
149
+3. Disables certain optimizations that may introduce non-determinism (such as sequence parallelism / async TP, whose reduce-scatter path is not batch-invariant)
150
+4. Under tensor parallelism, keeps custom all-reduce on with a fixed reduction order (the 1-stage kernel is pinned, and large inputs are reduced in fixed-size chunks), and disables FlashInfer, AITER and QuickReduce all-reduce
151
+5. On CUDA devices with tuned matmul table entries for the model's bf16 unquantized forward linear layers (Ada, Hopper, Blackwell),
152
runs without `torch.compile` using breakable CUDA graphs so tile configs follow the runtime batch size; set `VLLM_USE_BREAKABLE_CUDAGRAPH=0` to opt out
153
(not applied when sequence parallelism / async TP are enabled, since those are `torch.compile` passes).
154
155
+!!! warning
156
+ Batch invariance under tensor parallelism is not yet supported for all-reduces whose size isn't a multiple of 16 bytes, for example a hidden size that isn't a multiple of 8 in fp16/bf16. Such tensors can switch between NCCL and custom all-reduce depending on batch size. All validated models meet this requirement.
157
+
158
!!! note
159
Enabling batch invariance may impact performance compared to the default non-deterministic mode. This trade-off is intentional to guarantee reproducibility.
160
tests/distributed/test_custom_all_reduce.py
+90
@@ -205,6 +205,42 @@ def test_custom_allreduce_filters_dtype(
205
assert communicator.should_custom_ar(torch.empty(16, dtype=dtype)) is expected
206
207
208
+@pytest.mark.parametrize("batch_invariant", [False, True])
209
+def test_custom_allreduce_size_gate_ignored_under_batch_invariance(
210
+ batch_invariant: bool,
211
+) -> None:
212
+ """Batch invariance must not switch backends based on the input size."""
213
+ communicator = car.CustomAllreduce.__new__(car.CustomAllreduce)
214
+ communicator.disabled = False
215
+ communicator.world_size = 2
216
+ communicator.max_size = 1024
217
+ communicator.batch_invariant = batch_invariant
218
+ communicator._ptr = 0
219
+
220
+ oversized = torch.empty(1024, dtype=torch.float16)
221
+ assert communicator.should_custom_ar(oversized) is batch_invariant
222
+
223
+
224
+@pytest.mark.parametrize("batch_invariant", [False, True])
225
+def test_custom_reduce_scatter_disabled_under_batch_invariance(
226
+ monkeypatch, batch_invariant: bool
227
+) -> None:
228
+ """Reduce-scatter stays on one backend under batch invariance."""
229
+ monkeypatch.setattr(car.current_platform, "is_cuda", lambda: True)
230
+ communicator = car.CustomAllreduce.__new__(car.CustomAllreduce)
231
+ communicator.disabled = False
232
+ communicator.world_size = 2
233
+ communicator.fully_connected = True
234
+ communicator.mnnvl_only = False
235
+ communicator.mnnvl_multicast_ptr = 0
236
+ communicator.max_reduce_scatter_size = 1024
237
+ communicator.max_mnnvl_reduce_scatter_size = 1024
238
+ communicator.batch_invariant = batch_invariant
239
+
240
+ in_range = torch.empty(64, dtype=torch.float16)
241
+ assert communicator.should_custom_reduce_scatter(in_range) is not batch_invariant
242
+
243
+
244
@pytest.mark.parametrize(
245
("major", "local_multicast", "expected"),
246
[
@@ -622,6 +658,60 @@ def eager_allreduce(
658
torch.testing.assert_close(out, inp * (tp_size**num_communication))
659
660
661
+@ray.remote(num_gpus=1, max_calls=1)
662
+def chunked_allreduce(
663
+ monkeypatch: pytest.MonkeyPatch,
664
+ tp_size,
665
+ pp_size,
666
+ rank,
667
+ distributed_init_port,
668
+):
669
+ """Inputs above max_size are reduced in chunks and match the fixed-order
670
+ reference bitwise, both eagerly and inside a CUDA graph."""
671
+ with monkeypatch.context() as m:
672
+ m.delenv("CUDA_VISIBLE_DEVICES", raising=False)
673
+ m.delenv("HIP_VISIBLE_DEVICES", raising=False)
674
+ m.setenv("VLLM_CUSTOM_ALLREDUCE_ALGO", "1stage")
675
+ device = torch.device(f"cuda:{rank}")
676
+ torch.accelerator.set_device_index(device)
677
+ init_test_distributed_environment(tp_size, pp_size, rank, distributed_init_port)
678
+ ensure_model_parallel_initialized(tp_size, pp_size)
679
+ group = get_tp_group().device_group
680
+ fa = get_tp_group().device_communicator.ca_comm
681
+
682
+ # Two full chunks plus a partial one.
683
+ dtype = torch.bfloat16
684
+ chunk_numel = fa.max_size // dtype.itemsize
685
+ inp = torch.randn(2 * chunk_numel + 4096, device=device).to(dtype)
686
+ gathered = [torch.empty_like(inp) for _ in range(tp_size)]
687
+ dist.all_gather(gathered, inp, group=group)
688
+ ref = gathered[0].float()
689
+ for peer in gathered[1:]:
690
+ ref = ref + peer.float()
691
+ ref = ref.to(torch.bfloat16)
692
+
693
+ out = fa.all_reduce(inp, registered=False)
694
+ assert torch.equal(out, ref)
695
+
696
+ # Weak-contiguous but not C-contiguous: a transposed matrix.
697
+ inp_t, ref_t = inp.view(-1, 4096).t(), ref.view(-1, 4096).t()
698
+ assert torch.equal(fa.all_reduce(inp_t, registered=False), ref_t)
699
+
700
+ with fa.capture():
701
+ graph = torch.cuda.CUDAGraph()
702
+ with torch.cuda.graph(graph):
703
+ out = fa.all_reduce(inp, registered=True)
704
+ graph.replay()
705
+ assert torch.equal(out, ref)
706
+
707
+
708
+@pytest.mark.parametrize("tp_size", [2, 4])
709
+def test_custom_allreduce_chunked(monkeypatch: pytest.MonkeyPatch, tp_size):
710
+ if tp_size > torch.accelerator.device_count():
711
+ pytest.skip("Not enough GPUs to run the test.")
712
+ multi_process_parallel(monkeypatch, tp_size, 1, chunked_allreduce)
713
+
714
+
715
@pytest.mark.parametrize("tp_size", [2])
716
@pytest.mark.parametrize("pipeline_parallel_size", [1, 2])
717
@pytest.mark.parametrize("test_target", [eager_allreduce, graph_allreduce])
tests/v1/determinism/test_batch_invariance.py
-20
@@ -176,17 +176,6 @@ def test_logprobs_bitwise_batch_invariance_bs1_vs_bsN(
176
random.seed(seed)
177
tp_size = int(os.getenv("VLLM_TEST_TP_SIZE", "1"))
178
179
- # For batch invariance, disable custom all-reduce to ensure deterministic
180
- # all-reduce operations (custom all-reduce may not be deterministic)
181
- import vllm.envs as envs
182
-
183
- disable_custom_ar = envs.VLLM_BATCH_INVARIANT
184
-
185
- if disable_custom_ar:
186
- print(f"\n{'=' * 80}")
187
- print(f"BATCH INVARIANCE MODE: Disabling custom all-reduce (TP={tp_size})")
188
- print(f"{'=' * 80}\n")
189
-
179
llm = LLM(
180
model=TEST_MODEL,
181
tensor_parallel_size=tp_size,
@@ -744,15 +733,6 @@ def test_decode_logprobs_match_prefill_logprobs(
733
random.seed(seed)
734
tp_size = int(os.getenv("VLLM_TEST_TP_SIZE", "1"))
735
747
- import vllm.envs as envs
748
-
749
- disable_custom_ar = envs.VLLM_BATCH_INVARIANT
750
-
751
- if disable_custom_ar:
752
- print(f"\n{'=' * 80}")
753
- print(f"BATCH INVARIANCE MODE: Disabling custom all-reduce (TP={tp_size})")
754
- print(f"{'=' * 80}\n")
755
-
736
llm = LLM(
737
model=TEST_MODEL,
738
tensor_parallel_size=tp_size,
vllm/config/parallel.py
-4
@@ -1087,10 +1087,6 @@ class ParallelConfig:
1087
# Lazy import to avoid circular import
1088
from vllm.v1.executor import Executor
1089
1090
- # Enable batch invariance settings if requested
1091
- if envs.VLLM_BATCH_INVARIANT:
1092
- self.disable_custom_all_reduce = True
1093
-
1090
if (
1091
self.distributed_executor_backend is not None
1092
and not isinstance(self.distributed_executor_backend, str)
vllm/distributed/device_communicators/cuda_communicator.py
+11
-3
@@ -68,8 +68,11 @@ class CudaCommunicator(DeviceCommunicatorBase):
68
envs.VLLM_ALLREDUCE_USE_FLASHINFER_PCIE_IPC
69
and not envs.VLLM_BATCH_INVARIANT
70
)
71
- use_aiter_allreduce = use_custom_allreduce and bool(
72
- rocm_aiter_ops.is_custom_all_reduce_enabled()
71
+ # Neither AITER nor QuickReduce all-reduce has a fixed reduction order.
72
+ use_aiter_allreduce = (
73
+ use_custom_allreduce
74
+ and not envs.VLLM_BATCH_INVARIANT
75
+ and bool(rocm_aiter_ops.is_custom_all_reduce_enabled())
76
)
77
78
self.use_custom_allreduce = use_custom_allreduce
@@ -166,7 +169,12 @@ class CudaCommunicator(DeviceCommunicatorBase):
169
else:
170
self.use_aiter_ag_rs = True
171
169
- if use_custom_allreduce and self.world_size > 1 and current_platform.is_rocm():
172
+ if (
173
+ use_custom_allreduce
174
+ and self.world_size > 1
175
+ and current_platform.is_rocm()
176
+ and not envs.VLLM_BATCH_INVARIANT
177
+ ):
178
# Initialize a custom quick all-reduce implementation for AMD.
179
# Quick reduce is designed as a complement to custom allreduce
180
# (vLLM's or AITER's), so it is initialized for either backend.
vllm/distributed/device_communicators/custom_all_reduce.py
+21
-2
@@ -119,6 +119,7 @@ from vllm.distributed.utils import is_weak_contiguous # noqa: E402
119
120
class CustomAllreduce:
121
_SUPPORTED_WORLD_SIZES = [2, 4, 6, 8, 16]
122
+ batch_invariant = False
123
_DEFAULT_ALL_GATHER_MAX_SIZE = 2 * 1024 * 1024
124
_DEFAULT_MNNVL_ALL_GATHER_MAX_SIZES = {
125
2: 8 * 1024 * 1024,
@@ -179,6 +180,7 @@ class CustomAllreduce:
180
self.mnnvl_multimem_rs_local_ptr = 0
181
self.mnnvl_multimem_rs_multicast_ptr = 0
182
self.mnnvl_only = False
183
+ self.batch_invariant = envs.VLLM_BATCH_INVARIANT
184
185
if not custom_ar:
186
# disable because of missing custom allreduce library
@@ -504,7 +506,7 @@ class CustomAllreduce:
506
# for 4 or more non NVLink-capable GPUs, custom allreduce provides
507
# little performance improvement over NCCL.
508
if self.world_size == 2 or self.fully_connected:
507
- return inp_size < self.max_size
509
+ return self.batch_invariant or inp_size < self.max_size
510
return False
511
512
def all_reduce(
@@ -518,13 +520,27 @@ class CustomAllreduce:
520
"""
521
if out is None:
522
out = torch.empty_like(inp)
523
+ chunk_numel = self.max_size // inp.element_size()
524
+ if inp.numel() <= chunk_numel:
525
+ self._all_reduce_chunk(inp, out, registered)
526
+ return out
527
+ # Storage-order views: the gate only admits one contiguous block.
528
+ flat_inp = inp.as_strided((inp.numel(),), (1,), inp.storage_offset())
529
+ flat_out = out.as_strided((out.numel(),), (1,), out.storage_offset())
530
+ for start in range(0, flat_inp.numel(), chunk_numel):
531
+ end = start + chunk_numel
532
+ self._all_reduce_chunk(flat_inp[start:end], flat_out[start:end], registered)
533
+ return out
534
+
535
+ def _all_reduce_chunk(
536
+ self, inp: torch.Tensor, out: torch.Tensor, registered: bool
537
+ ) -> None:
538
if registered:
539
ops.all_reduce(self._ptr, inp, out, 0, 0)
540
else:
541
ops.all_reduce(
542
self._ptr, inp, out, self.buffer_ptrs[self.rank], self.max_size
543
)
527
- return out
544
545
def custom_all_reduce(self, input: torch.Tensor) -> torch.Tensor | None:
546
"""The main allreduce API that provides support for cuda graph."""
@@ -603,6 +619,9 @@ class CustomAllreduce:
619
) -> _ReduceScatterBackend | None:
620
if self.disabled or not current_platform.is_cuda():
621
return None
622
+ # The size gates below would switch backends per batch.
623
+ if self.batch_invariant:
624
+ return None
625
if self.world_size == 16 and not self.mnnvl_only:
626
return None
627
inp_size = inp.nbytes
vllm/model_executor/determinism/batch_invariant.py
+2
@@ -1139,6 +1139,8 @@ def enable_batch_invariant_mode():
1139
1140
def override_envs_for_invariance():
1141
os.environ["VLLM_ALLREDUCE_USE_SYMM_MEM"] = "0"
1142
+ # Only the 1-stage kernel has a size- and rank-independent reduction order.
1143
+ os.environ["VLLM_CUSTOM_ALLREDUCE_ALGO"] = "1stage"
1144
1145
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
1146