[Core][Distributed] Add process-checkpoint lifecycle hooks for communicators (starting with Flashinfer) (#46877)

Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.com>

Schwinn Saereesitthipitak committed Jul 26, 2026 at 11:47 UTC b5b61c622c941426cf78a83672e878b5b829fe90
10 files changed +185 -4
tests/distributed/test_comm_ops.py
+40
@@ -7,6 +7,7 @@ Run `pytest tests/distributed/test_comm_ops.py`.
7
8 from collections.abc import Callable
9 from typing import Any
10 +from unittest.mock import Mock
11
12 import pytest
13 import ray
@@ -19,6 +20,8 @@ from vllm.distributed import (
20 tensor_model_parallel_all_reduce,
21 tensor_model_parallel_reduce_scatter,
22 )
23 +from vllm.distributed.device_communicators import flashinfer_all_reduce
24 +from vllm.distributed.device_communicators.cuda_communicator import CudaCommunicator
25 from vllm.distributed.parallel_state import GroupCoordinator, TensorMetadata
26 from vllm.v1.worker.gpu_worker import AsyncIntermediateTensors
27
@@ -278,6 +281,43 @@ def test_irecv_tensor_dict_send_allgather_postprocess_binds_keys(
281 torch.testing.assert_close(td["b"], torch.ones(4, dtype=torch.int32))
282
283
284 +@pytest.mark.parametrize("aliased", [False, True])
285 +def test_cuda_communicator_checkpoints_flashinfer_workspaces(
286 + monkeypatch: pytest.MonkeyPatch,
287 + aliased: bool,
288 +) -> None:
289 + group = object()
290 + normal_workspace = Mock()
291 + quant_workspace = normal_workspace if aliased else Mock()
292 + unique_workspaces = (
293 + [normal_workspace] if aliased else [normal_workspace, quant_workspace]
294 + )
295 +
296 + monkeypatch.setattr(flashinfer_all_reduce, "_fi_ar_workspace", normal_workspace)
297 + monkeypatch.setattr(
298 + flashinfer_all_reduce, "_fi_ar_quant_workspace", quant_workspace
299 + )
300 + monkeypatch.setattr(
301 + flashinfer_all_reduce,
302 + "_fi_ar_workspace_groups",
303 + {id(workspace): group for workspace in unique_workspaces},
304 + )
305 + monkeypatch.setattr(
306 + flashinfer_all_reduce, "TorchDistBackend", lambda group: group, raising=False
307 + )
308 +
309 + communicator = CudaCommunicator.__new__(CudaCommunicator)
310 + communicator.cpu_group = group
311 + communicator.fi_ar_comm = None
312 + communicator.all2all_manager = None
313 + communicator.checkpoint_prepare()
314 + communicator.checkpoint_restore()
315 +
316 + for workspace in unique_workspaces:
317 + workspace.checkpoint_prepare.assert_called_once_with()
318 + workspace.checkpoint_restore.assert_called_once_with(group)
319 +
320 +
321 def test_async_intermediate_tensors_lazy_wait() -> None:
322 work = _DummyWork()
323 post_calls = {"n": 0}
vllm/compilation/passes/fusion/allreduce_rms_fusion.py
+2 -2
@@ -225,7 +225,7 @@ if flashinfer_comm is not None:
225 max_token_num=max_token_num,
226 hidden_dim=hidden_size,
227 dtype=allreduce_in.dtype,
228 - group=get_tp_group().device_group,
228 + group=get_tp_group().cpu_group,
229 )
230 assert workspace is not None, (
231 "Flashinfer allreduce workspace must be initialized when using flashinfer"
@@ -996,7 +996,7 @@ class AllReduceFusionPass(VllmPatternMatcherPass):
996 )
997 return
998 self.hidden_dim = config.model_config.get_hidden_size()
999 - self.group = get_tp_group().device_group
999 + self.group = get_tp_group().cpu_group
1000 rank = get_tensor_model_parallel_rank()
1001 if flashinfer_comm is None:
1002 logger.warning(
vllm/distributed/device_communicators/all2all.py
+14
@@ -885,6 +885,20 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
885 self.mapping = None
886 self.initialized = False
887
888 + def checkpoint_prepare(self) -> None:
889 + if self.initialized:
890 + assert self.moe_alltoall is not None
891 + self.moe_alltoall.checkpoint_prepare()
892 +
893 + def checkpoint_restore(self) -> None:
894 + if self.initialized:
895 + assert self.moe_alltoall is not None
896 + from vllm.distributed.device_communicators.mnnvl_compat import (
897 + CustomCommunicator,
898 + )
899 +
900 + self.moe_alltoall.checkpoint_restore(CustomCommunicator(self.cpu_group))
901 +
902
903 class MoriAll2AllManager(All2AllManagerBase):
904 def __init__(self, cpu_group, all2all_backend: str):
vllm/distributed/device_communicators/base_device_communicator.py
+21
@@ -7,8 +7,11 @@ import torch
7 import torch.distributed as dist
8 from torch.distributed import ProcessGroup
9
10 +from vllm.logger import init_logger
11 from vllm.utils import is_moe_layer
12
13 +logger = init_logger(__name__)
14 +
15
16 class Cache:
17 def __init__(self):
@@ -137,6 +140,18 @@ class All2AllManagerBase:
140 def max_sms_used(self) -> int | None:
141 return None # None means it could use the whole GPU
142
143 + def checkpoint_prepare(self) -> None:
144 + logger.warning_once(
145 + "%s.checkpoint_prepare is not implemented; skipping.",
146 + type(self).__name__,
147 + )
148 +
149 + def checkpoint_restore(self) -> None:
150 + logger.warning_once(
151 + "%s.checkpoint_restore is not implemented; skipping.",
152 + type(self).__name__,
153 + )
154 +
155 def combine(self, hidden_states: torch.Tensor, is_sequence_parallel: bool = False):
156 raise NotImplementedError
157
@@ -216,6 +231,12 @@ class DeviceCommunicatorBase:
231 dist.all_reduce(input_, group=self.device_group)
232 return input_
233
234 + def checkpoint_prepare(self) -> None:
235 + """Prepare reclaimable communicator state for checkpoint (default: no-op)."""
236 +
237 + def checkpoint_restore(self) -> None:
238 + """Restore communicator state after checkpoint (default: no-op)."""
239 +
240 def all_gather(self, input_: torch.Tensor, dim: int = -1) -> torch.Tensor:
241 if dim < 0:
242 # Convert negative dim to positive.
vllm/distributed/device_communicators/cuda_communicator.py
+16
@@ -571,6 +571,22 @@ class CudaCommunicator(DeviceCommunicatorBase):
571 self.all2all_manager.destroy()
572 self.all2all_manager = None # type: ignore[assignment]
573
574 + def checkpoint_prepare(self) -> None:
575 + # Only FlashInfer all-reduce and FlashInfer all2all are supported for now.
576 + from .flashinfer_all_reduce import checkpoint_prepare_fi_ar_workspaces
577 +
578 + checkpoint_prepare_fi_ar_workspaces(self.cpu_group)
579 + if self.all2all_manager is not None:
580 + self.all2all_manager.checkpoint_prepare()
581 +
582 + def checkpoint_restore(self) -> None:
583 + # Only FlashInfer all-reduce and FlashInfer all2all are supported for now.
584 + from .flashinfer_all_reduce import checkpoint_restore_fi_ar_workspaces
585 +
586 + checkpoint_restore_fi_ar_workspaces(self.cpu_group)
587 + if self.all2all_manager is not None:
588 + self.all2all_manager.checkpoint_restore()
589 +
590 def all_gatherv(
591 self,
592 input_: torch.Tensor | list[torch.Tensor],
vllm/distributed/device_communicators/flashinfer_all_reduce.py
+41 -1
@@ -6,6 +6,7 @@ import atexit
6 import os
7 import random
8 import threading
9 +from typing import Any
10
11 import torch
12 import torch.distributed as dist
@@ -39,6 +40,7 @@ _fi_ar_workspace = None
40 # allreduce backend or a fallback backend when the primary workspace is not
41 # available on the current topology.
42 _fi_ar_quant_workspace = None
43 +_fi_ar_workspace_groups: dict[int, ProcessGroup] = {}
44
45
46 def _create_workspace(
@@ -81,6 +83,14 @@ def _create_workspace(
83 return None
84 finally:
85 random.setstate(rng_state)
86 + workspace_id = id(workspace)
87 + workspace_group = _fi_ar_workspace_groups.get(workspace_id)
88 + if workspace_group is not None and workspace_group is not group:
89 + raise RuntimeError(
90 + "FlashInfer returned an all-reduce workspace already associated "
91 + "with a different process group"
92 + )
93 + _fi_ar_workspace_groups[workspace_id] = group
94 logger.debug(
95 "Initialized FlashInfer All Reduce workspace: backend=%s, "
96 "world_size=%d, rank=%d, max_token_num=%d, hidden_dim=%d, dtype=%s",
@@ -111,7 +121,7 @@ def _resolve_fi_ar_backend() -> tuple[str, bool]:
121 # Default to mnnvl for both single- and multi-node setups. The mnnvl
122 # cudagraph hang that previously forced single-node to trtllm
123 # (https://github.com/vllm-project/vllm/issues/35772) was fixed upstream in
114 - # FlashInfer (>= 0.6.12, vLLM pins 0.6.13), so mnnvl is safe here. trtllm
124 + # FlashInfer (>= 0.6.12, vLLM pins 0.6.15), so mnnvl is safe here. trtllm
125 # does not support multi-node allreduce, so mnnvl is required there anyway.
126 # mnnvl needs NVSwitch multicast; on single-node topologies without it,
127 # fall back to trtllm so fused allreduce stays enabled.
@@ -268,6 +278,36 @@ def destroy_fi_ar_workspace():
278 _fi_ar_quant_workspace.destroy()
279
280 _fi_ar_workspace = _fi_ar_quant_workspace = None
281 + _fi_ar_workspace_groups.clear()
282 +
283 +
284 +def _fi_ar_workspaces_for_group(group: ProcessGroup) -> list[Any]:
285 + workspaces = [_fi_ar_workspace]
286 + if _fi_ar_quant_workspace is not _fi_ar_workspace:
287 + workspaces.append(_fi_ar_quant_workspace)
288 +
289 + group_workspaces = []
290 + for workspace in workspaces:
291 + if workspace is None:
292 + continue
293 + workspace_group = _fi_ar_workspace_groups.get(id(workspace))
294 + if workspace_group is None:
295 + raise RuntimeError(
296 + "FlashInfer all-reduce workspace process group was not retained"
297 + )
298 + if workspace_group is group:
299 + group_workspaces.append(workspace)
300 + return group_workspaces
301 +
302 +
303 +def checkpoint_prepare_fi_ar_workspaces(group: ProcessGroup) -> None:
304 + for workspace in _fi_ar_workspaces_for_group(group):
305 + workspace.checkpoint_prepare()
306 +
307 +
308 +def checkpoint_restore_fi_ar_workspaces(group: ProcessGroup) -> None:
309 + for workspace in _fi_ar_workspaces_for_group(group):
310 + workspace.checkpoint_restore(TorchDistBackend(group=group))
311
312
313 atexit.register(destroy_fi_ar_workspace)
vllm/distributed/parallel_state.py
+36
@@ -127,6 +127,28 @@ def _register_group(group: "GroupCoordinator") -> None:
127 _groups[group.unique_name] = weakref.ref(group)
128
129
130 +def _apply_to_device_comms(
131 + action: Callable[[DeviceCommunicatorBase], None],
132 +) -> None:
133 + """Apply ``action`` to every group's device communicator.
134 +
135 + Walks the registered parallel groups and skips those without a device
136 + communicator (absent at ``world_size == 1``).
137 + """
138 + comms = []
139 + for group_ref in _groups.values():
140 + group = group_ref()
141 + if group is None:
142 + continue
143 + dc = group.device_communicator
144 + if dc is None:
145 + continue
146 + comms.append(dc)
147 +
148 + for dc in comms:
149 + action(dc)
150 +
151 +
152 def all_reduce(tensor: torch.Tensor, group_name: str) -> torch.Tensor:
153 assert group_name in _groups, f"Group {group_name} is not found."
154 group = _groups[group_name]()
@@ -2028,6 +2050,20 @@ def prepare_communication_buffer_for_model(model: torch.nn.Module):
2050 _EPLB.prepare_communication_buffer_for_model(model)
2051
2052
2053 +def checkpoint_prepare_distributed_state() -> None:
2054 + """Prepare every device communicator for a process checkpoint."""
2055 + torch.accelerator.synchronize()
2056 + _apply_to_device_comms(lambda comm: comm.checkpoint_prepare())
2057 + torch.accelerator.synchronize()
2058 +
2059 +
2060 +def checkpoint_restore_distributed_state() -> None:
2061 + """Restore every device communicator after a process checkpoint."""
2062 + torch.accelerator.synchronize()
2063 + _apply_to_device_comms(lambda comm: comm.checkpoint_restore())
2064 + torch.accelerator.synchronize()
2065 +
2066 +
2067 def model_parallel_is_initialized():
2068 """Check if tensor and pipeline parallel groups are initialized."""
2069 return _TP is not None and _PP is not None
vllm/model_executor/layers/fused_allreduce_gemma_rms_norm.py
+1 -1
@@ -93,7 +93,7 @@ def _can_use_flashinfer(hidden_states: torch.Tensor, tp_size: int) -> tuple[bool
93 max_token_num=max_token_num,
94 hidden_dim=hidden_size,
95 dtype=hidden_states.dtype,
96 - group=get_tp_group().device_group,
96 + group=get_tp_group().cpu_group,
97 )
98 if workspace is None:
99 return False, 0
vllm/v1/engine/async_llm.py
+6
@@ -943,6 +943,12 @@ class AsyncLLM(EngineClient):
943 if self.logger_manager is not None:
944 self.logger_manager.record_sleep_state(0, 0)
945
946 + async def checkpoint_prepare(self) -> None:
947 + await self.collective_rpc("checkpoint_prepare")
948 +
949 + async def checkpoint_restore(self) -> None:
950 + await self.collective_rpc("checkpoint_restore")
951 +
952 async def is_sleeping(self) -> bool:
953 return await self.engine_core.is_sleeping_async()
954
vllm/v1/worker/gpu_worker.py
+8
@@ -41,6 +41,8 @@ from vllm.distributed.kv_transfer.kv_connector.v1.base import (
41 )
42 from vllm.distributed.parallel_state import (
43 Handle,
44 + checkpoint_prepare_distributed_state,
45 + checkpoint_restore_distributed_state,
46 get_pp_group,
47 get_tp_group,
48 )
@@ -244,6 +246,12 @@ class Worker(WorkerBase):
246 if tags is None or "kv_cache" in tags:
247 self.model_runner.post_kv_cache_wake_up()
248
249 + def checkpoint_prepare(self) -> None:
250 + checkpoint_prepare_distributed_state()
251 +
252 + def checkpoint_restore(self) -> None:
253 + checkpoint_restore_distributed_state()
254 +
255 def _maybe_get_memory_pool_context(self, tag: str) -> AbstractContextManager:
256 if (
257 current_platform.is_cuda_alike()