[Feature] Detect all2all peer fault with fault tolerance backend and prevent corrupted output (#43637)

Signed-off-by: fangyuchu <fangyuchu@qq.com> Co-authored-by: Tyler Michael Smith <tlrmchlsmth@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

fangyuchu committed Jul 1, 2026 at 00:00 UTC 245888ff77d4754a76bc899727fcf4290733dfd4
5 files changed +82 -4
vllm/distributed/device_communicators/all2all.py
+46
@@ -261,8 +261,13 @@ class DeepEPLLAll2AllManager(DeepEPAll2AllManagerBase):
261 All2All communication based on DeepEP Low-Latency kernels.
262 """
263
264 + _buffer: Any = None
265 + _mask: torch.Tensor | None = None
266 + _last_mask: torch.Tensor | None = None
267 +
268 def __init__(self, cpu_group, tcp_store_group=None):
269 super().__init__(cpu_group, tcp_store_group)
270 + self.support_fault_tolerance = False # TODO: set to True when FT is supported.
271
272 def _make_all2all_kwargs(
273 self,
@@ -304,6 +309,7 @@ class DeepEPLLAll2AllManager(DeepEPAll2AllManagerBase):
309 allow_nvlink_for_low_latency_mode=True,
310 allow_mnnvl=envs.VLLM_DEEPEP_LOW_LATENCY_USE_MNNVL,
311 explicitly_destroy=True,
312 + enable_shrink=self.support_fault_tolerance,
313 )
314 return kwargs
315
@@ -319,12 +325,30 @@ class DeepEPLLAll2AllManager(DeepEPAll2AllManagerBase):
325 handle: deep_ep.Buffer = self.handle_cache.get_or_create(
326 buffer_kwargs, deep_ep.Buffer
327 )
328 + DeepEPLLAll2AllManager._buffer = handle
329 return handle
330
331 # DeepEP LL uses RDMA so no SMs are used for communication
332 def max_sms_used(self) -> int | None:
333 return 0
334
335 + def query_active_mask(self) -> torch.Tensor:
336 + buf = DeepEPLLAll2AllManager._buffer
337 + assert buf is not None
338 + if DeepEPLLAll2AllManager._mask is None:
339 + DeepEPLLAll2AllManager._mask = torch.zeros(
340 + self.world_size, device="cuda", dtype=torch.int32
341 + )
342 + buf.low_latency_query_mask_buffer(DeepEPLLAll2AllManager._mask)
343 + return DeepEPLLAll2AllManager._mask
344 +
345 + def query_fault(self) -> torch.Tensor:
346 + current = self.query_active_mask()
347 + if DeepEPLLAll2AllManager._last_mask is None:
348 + DeepEPLLAll2AllManager._last_mask = torch.zeros_like(current)
349 + has_fault = (current != DeepEPLLAll2AllManager._last_mask).any()
350 + return has_fault
351 +
352
353 @dataclass
354 class _NixlEPBufferState:
@@ -341,6 +365,8 @@ class NixlEPAll2AllManager(All2AllManagerBase):
365
366 _buffer: _NixlEPBufferState | None = None
367 _lock = threading.RLock()
368 + _mask: torch.Tensor | None = None
369 + _last_mask: torch.Tensor | None = None
370
371 def __init__(self, cpu_group, tcp_store_group=None):
372 if tcp_store_group is None:
@@ -350,6 +376,7 @@ class NixlEPAll2AllManager(All2AllManagerBase):
376 store=dist.PrefixStore("nixl_ep", cpu_group.get_group_store()),
377 )
378 super().__init__(cpu_group, tcp_store_group)
379 + self.support_fault_tolerance = True
380
381 self.max_num_ep_ranks = envs.VLLM_NIXL_EP_MAX_NUM_RANKS
382
@@ -508,6 +535,25 @@ class NixlEPAll2AllManager(All2AllManagerBase):
535 def max_sms_used(self) -> int | None:
536 return 0
537
538 + def query_active_mask(self) -> torch.Tensor:
539 + state = NixlEPAll2AllManager._buffer
540 + assert state is not None
541 + if NixlEPAll2AllManager._mask is None:
542 + NixlEPAll2AllManager._mask = torch.zeros(
543 + self.max_num_ep_ranks, device="cuda", dtype=torch.int32
544 + )
545 + state.buffer.query_mask_buffer(NixlEPAll2AllManager._mask)
546 + return NixlEPAll2AllManager._mask[: state.active_ep_size]
547 +
548 + def query_fault(self) -> torch.Tensor:
549 + current = self.query_active_mask()
550 + last = NixlEPAll2AllManager._last_mask
551 + if last is None or last.shape != current.shape:
552 + NixlEPAll2AllManager._last_mask = torch.zeros_like(current)
553 + last = NixlEPAll2AllManager._last_mask
554 + has_fault = (current != last).any()
555 + return has_fault
556 +
557
558 class FlashInferNVLinkTwoSidedManager(All2AllManagerBase):
559 """
vllm/distributed/device_communicators/base_device_communicator.py
+9
@@ -62,6 +62,8 @@ class All2AllManagerBase:
62 in_the_same_node_as(tcp_store_group, source_rank=0)
63 )
64
65 + self.support_fault_tolerance = False
66 +
67 def get_handle(self, kwargs):
68 # get a handle for the all2all communication,
69 # based on the kwargs.
@@ -102,6 +104,13 @@ class All2AllManagerBase:
104 # - raise a clear error if extra_tensors is not supported.
105 raise NotImplementedError
106
107 + def query_active_mask(self) -> torch.Tensor:
108 + raise NotImplementedError
109 +
110 + def query_fault(self) -> torch.Tensor:
111 + """Returns has_fault scalar."""
112 + raise NotImplementedError
113 +
114 def set_num_sms(self, num_sms: int):
115 pass
116
vllm/model_executor/layers/fused_moe/all2all_utils.py
+3 -3
@@ -56,7 +56,7 @@ if current_platform.is_cuda_alike():
56 )
57
58
59 -def _get_ep_all2all_manager(eep_stage: bool = False) -> Any:
59 +def get_ep_all2all_manager(eep_stage: bool = False) -> Any:
60 if eep_stage:
61 from vllm.distributed.elastic_ep.standby_state import get_standby_ep_group
62
@@ -146,7 +146,7 @@ def maybe_make_prepare_finalize(
146 "Detected DP deployment with no --enable-expert-parallel. "
147 "Falling back to AllGather+ReduceScatter dispatch/combine."
148 )
149 - all2all_manager = _get_ep_all2all_manager(eep_stage)
149 + all2all_manager = get_ep_all2all_manager(eep_stage)
150 return make_moe_prepare_and_finalize_naive_dp_ep(
151 is_sequence_parallel=moe.moe_parallel_config.is_sequence_parallel,
152 num_dispatchers=all2all_manager.world_size,
@@ -155,7 +155,7 @@ def maybe_make_prepare_finalize(
155 else:
156 return make_moe_prepare_and_finalize_no_dp_ep(use_monolithic)
157
158 - all2all_manager = _get_ep_all2all_manager(eep_stage)
158 + all2all_manager = get_ep_all2all_manager(eep_stage)
159
160 prepare_finalize: FusedMoEPrepareAndFinalize | None = None
161
vllm/v1/executor/multiproc_executor.py
+5 -1
@@ -939,7 +939,11 @@ class WorkerProc:
939 converted to a FAILURE response.
940 """
941 if isinstance(output, AsyncModelRunnerOutput):
942 - output = output.get_output()
942 + try:
943 + output = output.get_output()
944 + except Exception as e:
945 + logger.exception("Error getting async model runner output")
946 + output = e
947
948 if isinstance(output, Exception):
949 result = (WorkerProc.ResponseStatus.FAILURE, str(output))
vllm/v1/worker/gpu_model_runner.py
+19
@@ -58,6 +58,7 @@ from vllm.logger import init_logger
58 from vllm.lora.layers import LoRAMapping, LoRAMappingType
59 from vllm.model_executor.layers.attention import Attention, MLAAttention
60 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
61 +from vllm.model_executor.layers.fused_moe.all2all_utils import get_ep_all2all_manager
62 from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
63 RoutedExpertsCapturer,
64 )
@@ -249,6 +250,7 @@ class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
250 async_output_copy_stream: torch.cuda.Stream,
251 vocab_size: int,
252 routed_experts: RoutedExpertsTensors | None = None,
253 + check_ep_fault: bool = False,
254 ):
255 self._model_runner_output = model_runner_output
256 self._invalid_req_indices = invalid_req_indices
@@ -262,6 +264,7 @@ class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
264 self.vocab_size = vocab_size
265 self._logprobs_tensors = logprobs_tensors
266 self._routed_experts = routed_experts
267 + self._has_fault: torch.Tensor | None = None
268
269 # Initiate the copy on a separate stream, but do not synchronize it.
270 default_stream = torch.cuda.current_stream()
@@ -280,6 +283,9 @@ class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
283 if self._routed_experts is not None
284 else None
285 )
286 + if check_ep_fault:
287 + has_fault = get_ep_all2all_manager().query_fault()
288 + self._has_fault = has_fault.to("cpu", non_blocking=True)
289 self.async_copy_ready_event.record()
290
291 def get_output(self) -> ModelRunnerOutput:
@@ -316,6 +322,14 @@ class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
322 output.routed_experts = self._routed_experts_cpu.tolists()
323 del self._routed_experts
324
325 + if self._has_fault is not None and self._has_fault.item():
326 + mask = get_ep_all2all_manager().query_active_mask()
327 + raise RuntimeError(
328 + "Fault detected in EP all2all communication: "
329 + "one or more ranks timed out during dispatch/combine. "
330 + f"Mask: {mask.cpu().tolist()}"
331 + )
332 +
333 return output
334
335
@@ -445,6 +459,10 @@ class GPUModelRunner(
459 self.device = device
460 self.dtype = self.model_config.dtype
461
462 + self.check_ep_fault = False
463 + if parallel_config.data_parallel_size > 1 and self.model_config.is_moe:
464 + self.check_ep_fault = get_ep_all2all_manager().support_fault_tolerance
465 +
466 self.kv_cache_dtype = kv_cache_dtype_str_to_dtype(
467 cache_config.cache_dtype, self.model_config
468 )
@@ -4695,6 +4713,7 @@ class GPUModelRunner(
4713 async_output_copy_stream=self._get_or_create_async_output_copy_stream(),
4714 vocab_size=self.input_batch.vocab_size,
4715 routed_experts=routed_experts_snapshot,
4716 + check_ep_fault=self.check_ep_fault,
4717 )
4718 with record_function_or_nullcontext(
4719 "gpu_model_runner: set_async_sampled_token_ids"