cuda_communicator.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. import os
  4. import torch
  5. from torch.distributed import ProcessGroup
  6. import vllm.envs as envs
  7. from vllm._aiter_ops import rocm_aiter_ops
  8. from vllm.distributed.device_communicators.all_reduce_utils import (
  9. NCCL_SYMM_MEM_ALL_REDUCE_CONFIG,
  10. should_nccl_symm_mem_ag_rs,
  11. should_nccl_symm_mem_allreduce,
  12. )
  13. from vllm.distributed.device_communicators.pynccl import register_nccl_symmetric_ops
  14. from vllm.distributed.device_communicators.pynccl_allocator import (
  15. is_symmetric_memory_enabled,
  16. )
  17. from vllm.logger import init_logger
  18. from vllm.platforms import current_platform
  19. from ..utils import StatelessProcessGroup
  20. from .aiter_custom_all_reduce import AiterCustomAllreduce
  21. from .base_device_communicator import DeviceCommunicatorBase
  22. logger = init_logger(__name__)
  23. class CudaCommunicator(DeviceCommunicatorBase):
  24. def __init__(
  25. self,
  26. cpu_group: ProcessGroup,
  27. device: torch.device | None = None,
  28. device_group: ProcessGroup | None = None,
  29. unique_name: str = "",
  30. global_ranks: list[int] | None = None,
  31. global_world_size: int | None = None,
  32. tcp_store_group: StatelessProcessGroup | None = None,
  33. use_all2all: bool = False,
  34. ):
  35. super().__init__(
  36. cpu_group,
  37. device,
  38. device_group,
  39. unique_name,
  40. global_ranks,
  41. global_world_size,
  42. use_all2all=use_all2all,
  43. )
  44. # Match the group name exactly so ETP does not enable TP-only backends.
  45. if unique_name.split(":")[0] != "tp":
  46. # custom allreduce or torch symm mem can be used only by tp
  47. use_custom_allreduce = False
  48. use_torch_symm_mem = False
  49. use_flashinfer_allreduce = False
  50. use_flashinfer_pcie_ipc_allreduce = False
  51. use_aiter_allreduce = False
  52. else:
  53. from vllm.distributed.parallel_state import _ENABLE_CUSTOM_ALL_REDUCE
  54. use_custom_allreduce = _ENABLE_CUSTOM_ALL_REDUCE
  55. use_torch_symm_mem = envs.VLLM_ALLREDUCE_USE_SYMM_MEM
  56. # FlashInfer all-reduce does not provide a fixed reduction order.
  57. use_flashinfer_allreduce = (
  58. envs.VLLM_ALLREDUCE_USE_FLASHINFER and not envs.VLLM_BATCH_INVARIANT
  59. )
  60. use_flashinfer_pcie_ipc_allreduce = (
  61. envs.VLLM_ALLREDUCE_USE_FLASHINFER_PCIE_IPC
  62. and not envs.VLLM_BATCH_INVARIANT
  63. )
  64. use_aiter_allreduce = use_custom_allreduce and bool(
  65. rocm_aiter_ops.is_custom_all_reduce_enabled()
  66. )
  67. self.use_custom_allreduce = use_custom_allreduce
  68. self.use_torch_symm_mem = use_torch_symm_mem
  69. self.use_flashinfer_allreduce = use_flashinfer_allreduce
  70. self.use_flashinfer_pcie_ipc_allreduce = use_flashinfer_pcie_ipc_allreduce
  71. self.use_aiter_allreduce = use_aiter_allreduce
  72. # lazy import to avoid documentation build error
  73. from vllm.distributed.device_communicators.custom_all_reduce import (
  74. CustomAllreduce,
  75. )
  76. from vllm.distributed.device_communicators.flashinfer_all_reduce import (
  77. FlashInferAllReduce,
  78. )
  79. from vllm.distributed.device_communicators.flashinfer_pcie_ipc_all_reduce import ( # noqa: E501
  80. FlashInferPcieIpcAllReduce,
  81. )
  82. from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
  83. from vllm.distributed.device_communicators.quick_all_reduce import (
  84. QuickAllReduce,
  85. )
  86. from vllm.distributed.device_communicators.symm_mem import SymmMemCommunicator
  87. self.pynccl_comm: PyNcclCommunicator | None = None
  88. if self.world_size > 1:
  89. self.pynccl_comm = PyNcclCommunicator(
  90. group=self.cpu_group if tcp_store_group is None else tcp_store_group,
  91. device=self.device,
  92. )
  93. if is_symmetric_memory_enabled():
  94. register_nccl_symmetric_ops(self.pynccl_comm)
  95. # GB10: RDMA all-reduce for decode-sized tensors (arx.py).
  96. self.arx_comm = None
  97. if (
  98. unique_name.split(":")[0] == "tp"
  99. and self.world_size > 1
  100. and os.environ.get("VLLM_ARX_ALLREDUCE") == "1"
  101. ):
  102. from vllm.distributed.device_communicators.arx import ArxCommunicator
  103. self.arx_comm = ArxCommunicator(self.cpu_group, self.device)
  104. self.arxbig = None
  105. if self.arx_comm is not None and os.environ.get("VLLM_ARXBIG") == "1":
  106. from vllm.distributed.device_communicators.arx import ArxBig
  107. self.arxbig = ArxBig(self.cpu_group, self.device)
  108. self.ca_comm: CustomAllreduce | None = None
  109. self.qr_comm: QuickAllReduce | None = None
  110. self.symm_mem_comm: SymmMemCommunicator | None = None
  111. self.fi_ar_comm: FlashInferAllReduce | None = None
  112. self.fi_pcie_ipc_ar_comm: FlashInferPcieIpcAllReduce | None = None
  113. self.aiter_ar_comm: AiterCustomAllreduce | None = None
  114. self.use_aiter_ag_rs: bool = False
  115. if use_torch_symm_mem and current_platform.is_cuda():
  116. self.symm_mem_comm = SymmMemCommunicator(
  117. group=self.cpu_group,
  118. device=self.device,
  119. )
  120. if self.use_flashinfer_allreduce and self.world_size > 1:
  121. self.fi_ar_comm = FlashInferAllReduce(
  122. group=self.cpu_group,
  123. device=self.device,
  124. )
  125. if (
  126. self.use_flashinfer_pcie_ipc_allreduce
  127. and self.world_size > 1
  128. and self.device_group is not None
  129. ):
  130. self.fi_pcie_ipc_ar_comm = FlashInferPcieIpcAllReduce(
  131. group=self.device_group,
  132. tune_group=self.cpu_group,
  133. device=self.device,
  134. )
  135. if self.use_aiter_allreduce and self.world_size > 1:
  136. self.aiter_ar_comm = AiterCustomAllreduce(
  137. group=self.cpu_group,
  138. device=self.device,
  139. )
  140. if use_custom_allreduce and self.aiter_ar_comm is None and self.world_size > 1:
  141. # Initialize a custom fast all-reduce implementation.
  142. self.ca_comm = CustomAllreduce(
  143. group=self.cpu_group,
  144. device=self.device,
  145. symm_mem_enabled=(
  146. self.symm_mem_comm is not None and not self.symm_mem_comm.disabled
  147. ),
  148. )
  149. # AITER custom all-gather/reduce-scatter DP-attention dispatch/combine
  150. if (
  151. "dp" in unique_name
  152. and self.world_size in (2, 4, 8)
  153. and current_platform.is_rocm()
  154. and rocm_aiter_ops.is_custom_all_reduce_enabled()
  155. ):
  156. self.aiter_ar_comm = AiterCustomAllreduce(
  157. group=self.cpu_group,
  158. device=self.device,
  159. )
  160. if self.aiter_ar_comm.disabled:
  161. self.aiter_ar_comm = None
  162. else:
  163. self.use_aiter_ag_rs = True
  164. if use_custom_allreduce and self.world_size > 1 and current_platform.is_rocm():
  165. # Initialize a custom quick all-reduce implementation for AMD.
  166. # Quick reduce is designed as a complement to custom allreduce
  167. # (vLLM's or AITER's), so it is initialized for either backend.
  168. # Based on quickreduce (https://github.com/mk1-project/quickreduce).
  169. # On ROCm, 'use_custom_allreduce==True' means it must currently be
  170. # an MI300 series.
  171. self.qr_comm = QuickAllReduce(group=self.cpu_group, device=self.device)
  172. if self.world_size > 1:
  173. self._log_all_reduce_backend_selection()
  174. if self.use_all2all:
  175. if self.all2all_backend in ("naive", "allgather_reducescatter"):
  176. from .all2all import AgRsAll2AllManager
  177. self.all2all_manager = AgRsAll2AllManager(
  178. self.cpu_group, tcp_store_group
  179. )
  180. elif self.all2all_backend == "deepep_high_throughput":
  181. from .all2all import DeepEPHTAll2AllManager
  182. self.all2all_manager = DeepEPHTAll2AllManager(
  183. self.cpu_group, tcp_store_group
  184. )
  185. elif self.all2all_backend == "deepep_low_latency":
  186. from .all2all import DeepEPLLAll2AllManager
  187. self.all2all_manager = DeepEPLLAll2AllManager(
  188. self.cpu_group, tcp_store_group
  189. )
  190. elif self.all2all_backend in (
  191. "mori_high_throughput",
  192. "mori_low_latency",
  193. ):
  194. from .all2all import MoriAll2AllManager
  195. self.all2all_manager = MoriAll2AllManager(
  196. self.cpu_group, self.all2all_backend
  197. )
  198. elif self.all2all_backend == "deepep_v2":
  199. from .all2all import DeepEPV2All2AllManager
  200. self.all2all_manager = DeepEPV2All2AllManager(
  201. self.cpu_group,
  202. tcp_store_group,
  203. device_group=self.device_group,
  204. )
  205. elif self.all2all_backend == "moonep":
  206. from .all2all import MoonEPAll2AllManager
  207. self.all2all_manager = MoonEPAll2AllManager(
  208. self.cpu_group,
  209. tcp_store_group,
  210. device_group=self.device_group,
  211. )
  212. elif self.all2all_backend == "nixl_ep":
  213. from .all2all import NixlEPAll2AllManager
  214. self.all2all_manager = NixlEPAll2AllManager(
  215. self.cpu_group, tcp_store_group
  216. )
  217. elif (
  218. self.all2all_backend == "flashinfer_all2allv"
  219. or self.all2all_backend == "flashinfer_nvlink_two_sided"
  220. ):
  221. if self.all2all_backend == "flashinfer_all2allv":
  222. logger.warning_once(
  223. "'flashinfer_all2allv' is deprecated and has been renamed to"
  224. "'flashinfer_nvlink_two_sided'. It will be removed in a future"
  225. "release."
  226. )
  227. from .all2all import FlashInferNVLinkTwoSidedManager
  228. self.all2all_manager = FlashInferNVLinkTwoSidedManager(
  229. self.cpu_group, tcp_store_group
  230. )
  231. elif self.all2all_backend == "flashinfer_nvlink_one_sided":
  232. from .all2all import FlashInferNVLinkOneSidedManager
  233. self.all2all_manager = FlashInferNVLinkOneSidedManager(self.cpu_group)
  234. else:
  235. raise ValueError(f"Unknown all2all backend: {self.all2all_backend}")
  236. logger.info_once(
  237. "Using %s all2all manager.",
  238. self.all2all_manager.__class__.__name__,
  239. scope="global",
  240. )
  241. def _log_all_reduce_backend_selection(self) -> None:
  242. """Log the all-reduce backends that are active for this group.
  243. The dispatch chain in ``all_reduce`` tries backends in this order and
  244. falls through to the next one if the current backend rejects the
  245. input (size/dtype gates) or is disabled. The list of "enabled"
  246. backends below is the subset of potential backends that may be
  247. chosen at dispatch time for this group; the actual per-call choice
  248. depends on the input tensor.
  249. """
  250. all_potential_ar_backends = [
  251. "FLASHINFER_PCIE_IPC",
  252. "FLASHINFER",
  253. "NCCL_SYMM_MEM",
  254. "QUICK_REDUCE",
  255. "AITER_CUSTOM",
  256. "CUSTOM",
  257. "SYMM_MEM",
  258. "PYNCCL",
  259. ]
  260. enabled_ar_backends: list[str] = []
  261. if (
  262. self.fi_pcie_ipc_ar_comm is not None
  263. and not self.fi_pcie_ipc_ar_comm.disabled
  264. ):
  265. enabled_ar_backends.append("FLASHINFER_PCIE_IPC")
  266. if self.fi_ar_comm is not None and not self.fi_ar_comm.disabled:
  267. enabled_ar_backends.append("FLASHINFER")
  268. # Mirror the static preconditions of `should_nccl_symm_mem_allreduce`:
  269. # VLLM_BATCH_INVARIANT off, NCCL symm mem enabled, world_size meets
  270. # min_world_size, and world_size either has a tuned entry in
  271. # `custom_ar_preferred_ranges` or is greater than
  272. # `always_use_above_world_size`. World sizes that fail the latter (e.g.
  273. # 5/6/7 with the default config) never dispatch NCCL symm mem
  274. # regardless of input. The per-tensor-size check inside the function
  275. # stays as a runtime decision.
  276. nccl_symm_ws_ok = self.world_size >= NCCL_SYMM_MEM_ALL_REDUCE_CONFIG[
  277. "min_world_size"
  278. ] and (
  279. self.world_size
  280. in NCCL_SYMM_MEM_ALL_REDUCE_CONFIG["custom_ar_preferred_ranges"]
  281. or self.world_size
  282. > NCCL_SYMM_MEM_ALL_REDUCE_CONFIG["always_use_above_world_size"]
  283. )
  284. if (
  285. self.pynccl_comm is not None
  286. and not self.pynccl_comm.disabled
  287. and is_symmetric_memory_enabled()
  288. and not envs.VLLM_BATCH_INVARIANT
  289. and nccl_symm_ws_ok
  290. ):
  291. enabled_ar_backends.append("NCCL_SYMM_MEM")
  292. if self.qr_comm is not None and not self.qr_comm.disabled:
  293. enabled_ar_backends.append("QUICK_REDUCE")
  294. if (
  295. self.use_aiter_allreduce
  296. and self.aiter_ar_comm is not None
  297. and not self.aiter_ar_comm.disabled
  298. ):
  299. enabled_ar_backends.append("AITER_CUSTOM")
  300. if self.ca_comm is not None and not self.ca_comm.disabled:
  301. enabled_ar_backends.append("CUSTOM")
  302. if self.symm_mem_comm is not None and not self.symm_mem_comm.disabled:
  303. enabled_ar_backends.append("SYMM_MEM")
  304. if self.pynccl_comm is not None and not self.pynccl_comm.disabled:
  305. enabled_ar_backends.append("PYNCCL")
  306. logger.info_once(
  307. "Using %s all-reduce backends (in dispatch order) for group "
  308. "'%s' out of potential backends: %s.",
  309. "[" + ", ".join(f"'{b}'" for b in enabled_ar_backends) + "]",
  310. self.unique_name or "<unnamed>",
  311. "[" + ", ".join(f"'{b}'" for b in all_potential_ar_backends) + "]",
  312. scope="global",
  313. )
  314. # GB10: sequence-parallel collectives over RDMA, tried first by
  315. # custom_all_gather / custom_reduce_scatter below: arx all-reduces for
  316. # decode sizes (~13-27 us against ~80 for NCCL), arxbig for prefill-sized
  317. # gathers. Returning None hands the call on.
  318. def _arx_reduce_scatter(self, input_):
  319. arx_comm = self.arx_comm
  320. if arx_comm is None or arx_comm.disabled or not arx_comm.should_use(input_):
  321. return None
  322. chunk = input_.shape[0] // self.world_size
  323. return arx_comm.all_reduce(input_)[self.rank_in_group * chunk:(self.rank_in_group + 1) * chunk]
  324. def _arx_all_gather(self, input_):
  325. # arxbig's gather lands in pinned memory, and the GEMMs that read it
  326. # there ran 16% slower overall than NCCL's gather into device memory.
  327. big = self.arxbig
  328. if big is not None and not big.disabled and big.gather and big.should_gather(input_):
  329. return big.all_gather(input_)
  330. # Each rank's rows in their own slot of a zero tensor: the sum is the
  331. # concatenation, exactly (x + 0 is x).
  332. arx_comm = self.arx_comm
  333. if (arx_comm is None or arx_comm.disabled or input_.dtype != torch.bfloat16
  334. or input_.numel() * 2 * self.world_size > arx_comm.max_bytes):
  335. return None
  336. full = input_.new_zeros(input_.shape[0] * self.world_size, *input_.shape[1:])
  337. if not arx_comm.should_use(full):
  338. return None
  339. chunk = input_.shape[0]
  340. full[self.rank_in_group * chunk:(self.rank_in_group + 1) * chunk] = input_
  341. return arx_comm.all_reduce(full)
  342. def all_reduce(self, input_):
  343. arx_comm = self.arx_comm
  344. if arx_comm is not None and not arx_comm.disabled and arx_comm.should_use(input_, two_shot=True):
  345. return arx_comm.all_reduce(input_)
  346. fi_ar_comm = self.fi_ar_comm
  347. use_fi_ar = (
  348. fi_ar_comm is not None
  349. and not fi_ar_comm.disabled
  350. and fi_ar_comm.should_use_fi_ar(input_)
  351. )
  352. # since currently we perform copy input -> symm_input -> out-of-place AR
  353. # return symm_output, we don't need to check if input is symmetric
  354. if (
  355. self.pynccl_comm is not None
  356. and not use_fi_ar
  357. and should_nccl_symm_mem_allreduce(self.pynccl_comm.world_size, input_)
  358. ):
  359. out = torch.ops.vllm.all_reduce_symmetric_with_copy(input_)
  360. if out is not None:
  361. return out
  362. qr_comm = self.qr_comm
  363. if (
  364. qr_comm is not None
  365. and not qr_comm.disabled
  366. and qr_comm.should_quick_allreduce(input_)
  367. ):
  368. out = qr_comm.quick_all_reduce(input_)
  369. assert out is not None
  370. return out
  371. fi_pcie_ipc_ar_comm = self.fi_pcie_ipc_ar_comm
  372. if fi_pcie_ipc_ar_comm is not None and fi_pcie_ipc_ar_comm.should_use(input_):
  373. return fi_pcie_ipc_ar_comm.all_reduce(input_)
  374. if use_fi_ar:
  375. assert fi_ar_comm is not None
  376. out = fi_ar_comm.all_reduce(input_)
  377. assert out is not None
  378. return out
  379. aiter_ar_comm = self.aiter_ar_comm
  380. if (
  381. self.use_aiter_allreduce
  382. and aiter_ar_comm is not None
  383. and not aiter_ar_comm.disabled
  384. and aiter_ar_comm.should_custom_ar(input_)
  385. ):
  386. out = aiter_ar_comm.custom_all_reduce(input_)
  387. assert out is not None
  388. return out
  389. ca_comm = self.ca_comm
  390. if (
  391. ca_comm is not None
  392. and not ca_comm.disabled
  393. and ca_comm.should_custom_ar(input_)
  394. ):
  395. out = ca_comm.custom_all_reduce(input_)
  396. assert out is not None
  397. return out
  398. symm_mem_comm = self.symm_mem_comm
  399. if symm_mem_comm is not None and symm_mem_comm.should_use_symm_mem(input_):
  400. out = symm_mem_comm.all_reduce(input_)
  401. assert out is not None
  402. return out
  403. pynccl_comm = self.pynccl_comm
  404. if pynccl_comm is None or pynccl_comm.disabled:
  405. out = input_.clone()
  406. torch.distributed.all_reduce(out, group=self.device_group)
  407. return out
  408. assert pynccl_comm is not None
  409. out = pynccl_comm.all_reduce(input_)
  410. if out is None:
  411. # fall back to the default all-reduce using PyTorch.
  412. # this usually happens during testing.
  413. # when we run the model, allreduce only happens for the TP
  414. # group, where we always have either custom allreduce or pynccl.
  415. out = input_.clone()
  416. torch.distributed.all_reduce(out, group=self.device_group)
  417. return out
  418. def custom_all_gather(self, input_: torch.Tensor) -> torch.Tensor | None:
  419. out = self._arx_all_gather(input_)
  420. if out is not None:
  421. return out
  422. ca_comm = self.ca_comm
  423. if ca_comm is None:
  424. return None
  425. return ca_comm.custom_all_gather(input_.contiguous())
  426. def custom_reduce_scatter(self, input_: torch.Tensor) -> torch.Tensor | None:
  427. out = self._arx_reduce_scatter(input_)
  428. if out is not None:
  429. return out
  430. ca_comm = self.ca_comm
  431. if ca_comm is None:
  432. return None
  433. return ca_comm.custom_reduce_scatter(input_.contiguous())
  434. def all_gather(self, input_: torch.Tensor, dim: int = -1) -> torch.Tensor:
  435. # Route uniform dim-0 all-gathers through NVLS symmetric memory when
  436. # enabled (mirrors reduce_scatter); otherwise fall back to the
  437. # PyNccl/base-class all-gather. Sequence parallelism's
  438. # gather-before-GEMM uses dim=0 with tp-aligned (uniform) shards.
  439. if dim < 0:
  440. dim += input_.dim()
  441. if dim == 0 and should_nccl_symm_mem_ag_rs():
  442. return self._all_gather_symm_mem(input_.contiguous())
  443. pynccl_comm = self.pynccl_comm
  444. if pynccl_comm is None or pynccl_comm.disabled:
  445. return super().all_gather(input_, dim)
  446. # On ROCm, the base-class all_gather (all_gather_into_tensor) is faster
  447. # than the manual pynccl + torch.empty + movedim + reshape path below,
  448. # which adds a per-call output allocation and (for dim != 0) an extra
  449. # copy on every step. This is on the hot path for TP forward passes, so
  450. # keep ROCm on the base-class collective to avoid a decode regression.
  451. if current_platform.is_rocm():
  452. return super().all_gather(input_, dim)
  453. input_size = input_.size()
  454. output_size = (input_size[0] * self.world_size,) + input_size[1:]
  455. output_tensor = torch.empty(
  456. output_size, dtype=input_.dtype, device=input_.device
  457. )
  458. pynccl_comm.all_gather(output_tensor, input_.contiguous())
  459. output_tensor = output_tensor.reshape((self.world_size,) + input_size)
  460. output_tensor = output_tensor.movedim(0, dim)
  461. return output_tensor.reshape(
  462. input_size[:dim]
  463. + (self.world_size * input_size[dim],)
  464. + input_size[dim + 1 :]
  465. )
  466. def reduce_scatter(self, input_: torch.Tensor, dim: int = -1):
  467. world_size = self.world_size
  468. pynccl_comm = self.pynccl_comm
  469. assert pynccl_comm is not None
  470. if dim < 0:
  471. # Convert negative dim to positive.
  472. dim += input_.dim()
  473. # Note: This will produce an incorrect answer if we don't make
  474. # the input_tensor contiguous. Possible bug in reduce_scatter_tensor?
  475. input_tensor = input_.movedim(0, dim).contiguous()
  476. assert input_tensor.shape[0] % world_size == 0
  477. chunk_size = input_tensor.shape[0] // world_size
  478. output_shape = (chunk_size,) + input_tensor.shape[1:]
  479. if should_nccl_symm_mem_ag_rs():
  480. output = self._reduce_scatter_symm_mem(input_tensor)
  481. else:
  482. output = torch.empty(
  483. output_shape, dtype=input_tensor.dtype, device=input_tensor.device
  484. )
  485. pynccl_comm.reduce_scatter(output, input_tensor)
  486. # Reshape before returning
  487. return output.movedim(0, dim).contiguous()
  488. def reduce_scatterv(
  489. self, input_: torch.Tensor, dim: int = -1, sizes: list[int] | None = None
  490. ):
  491. world_size = self.world_size
  492. pynccl_comm = self.pynccl_comm
  493. assert pynccl_comm is not None
  494. if dim < 0:
  495. # Convert negative dim to positive.
  496. dim += input_.dim()
  497. # 'sizes' is not needed if all inputs in the same group have the same
  498. # shape
  499. if sizes is not None and all(s == sizes[0] for s in sizes):
  500. sizes = None
  501. # Note: This will produce an incorrect answer if we don't make
  502. # the input_tensor contiguous. Possible bug in reduce_scatter_tensor?
  503. input_tensor = input_.movedim(0, dim).contiguous()
  504. if sizes is not None:
  505. assert len(sizes) == world_size, f"{len(sizes)} == {world_size}"
  506. assert input_tensor.shape[0] == sum(sizes)
  507. chunk_size = sizes[self.rank_in_group]
  508. else:
  509. assert input_tensor.shape[0] % world_size == 0
  510. chunk_size = input_tensor.shape[0] // world_size
  511. output_shape = (chunk_size,) + input_tensor.shape[1:]
  512. if self._can_use_aiter_ag_rs(sizes):
  513. aiter_comm = self.aiter_ar_comm
  514. assert aiter_comm is not None
  515. if aiter_comm.should_custom_rs(input_tensor, dim=0):
  516. output = torch.empty(
  517. output_shape, dtype=input_tensor.dtype, device=input_tensor.device
  518. )
  519. aiter_comm.custom_reduce_scatter(input_tensor, output, dim=0)
  520. return output.movedim(0, dim).contiguous()
  521. # Symmetric memory is only used when all ranks have uniform sizes.
  522. # ncclCommWindowRegister is collective: asymmetric pool allocations
  523. # from variable per-rank sizes cause deadlocks.
  524. use_symm_mem = sizes is None and should_nccl_symm_mem_ag_rs()
  525. if use_symm_mem:
  526. output = self._reduce_scatter_symm_mem(input_tensor)
  527. else:
  528. output = torch.empty(
  529. output_shape, dtype=input_tensor.dtype, device=input_tensor.device
  530. )
  531. use_deterministic_rs = envs.VLLM_BATCH_INVARIANT and world_size > 2
  532. if use_deterministic_rs:
  533. # Reduce to a fixed root (0) for determinism
  534. reduced = torch.empty_like(input_tensor)
  535. sizes = sizes if sizes else [chunk_size] * world_size
  536. pynccl_comm.reduce(reduced, input_tensor, root=0)
  537. pynccl_comm.scatter(output, reduced, sizes, root=0)
  538. elif sizes is not None and sizes.count(sizes[0]) != len(sizes):
  539. pynccl_comm.reduce_scatterv(output, input_tensor, sizes=sizes)
  540. else:
  541. pynccl_comm.reduce_scatter(output, input_tensor)
  542. # Reshape before returning
  543. return output.movedim(0, dim).contiguous()
  544. def _get_symm_scratch(
  545. self,
  546. role: str,
  547. shape: tuple[int, ...],
  548. dtype: torch.dtype,
  549. device: torch.device,
  550. ) -> torch.Tensor:
  551. """Persistent, pre-registered NCCL symmetric-memory scratch buffer.
  552. Allocating a fresh symm tensor per collective pays the
  553. ``nccl_symm_mem_context`` snapshot + window-registration scan on every
  554. call (~0.5 ms/RS+AG pair, dwarfing the NVLS transfer itself). Instead we
  555. allocate once per ``(role, shape, dtype)``, register once, and reuse.
  556. Safe for serial (eager) sequence parallelism: each collective's result
  557. is consumed on the same stream before the next same-role collective
  558. reuses the buffer. Distinct roles (e.g. ``rs_in`` vs ``ag_out``, both
  559. full-size) get distinct buffers so a reduce-scatter input copy never
  560. clobbers a still-live all-gather output.
  561. """
  562. from vllm.distributed.device_communicators.pynccl_allocator import (
  563. nccl_symm_mem_context,
  564. )
  565. pynccl_comm = self.pynccl_comm
  566. assert pynccl_comm is not None
  567. cache = self.__dict__.setdefault("_symm_scratch_bufs", {})
  568. key = (role, tuple(shape), dtype)
  569. buf = cache.get(key)
  570. if buf is None:
  571. with nccl_symm_mem_context(pynccl_comm):
  572. buf = torch.empty(shape, dtype=dtype, device=device)
  573. cache[key] = buf
  574. return buf
  575. def _reduce_scatter_symm_mem(
  576. self,
  577. input_tensor: torch.Tensor,
  578. ) -> torch.Tensor:
  579. """ReduceScatter using NCCL symmetric memory (NVLS).
  580. Only called for uniform-size reduce_scatter (variable sizes are
  581. guarded out by the caller to avoid asymmetric ncclCommWindowRegister).
  582. Uses persistent pre-registered scratch (see _get_symm_scratch).
  583. """
  584. from vllm.distributed.device_communicators.pynccl_allocator import (
  585. is_symmetric_memory_tensor,
  586. )
  587. pynccl_comm = self.pynccl_comm
  588. assert pynccl_comm is not None
  589. chunk = input_tensor.shape[0] // self.world_size
  590. output_shape = (chunk,) + tuple(input_tensor.shape[1:])
  591. symm_output = self._get_symm_scratch(
  592. "rs_out", output_shape, input_tensor.dtype, input_tensor.device
  593. )
  594. # NVLS reduce-scatter (LDMC) requires the input in symmetric memory.
  595. if is_symmetric_memory_tensor(input_tensor):
  596. symm_input = input_tensor
  597. else:
  598. symm_input = self._get_symm_scratch(
  599. "rs_in",
  600. tuple(input_tensor.shape),
  601. input_tensor.dtype,
  602. input_tensor.device,
  603. )
  604. symm_input.copy_(input_tensor)
  605. pynccl_comm.reduce_scatter(symm_output, symm_input)
  606. return symm_output
  607. def send(self, tensor: torch.Tensor, dst: int | None = None) -> None:
  608. """Sends a tensor to the destination rank in a blocking way."""
  609. """NOTE: `dst` is the local rank of the destination rank."""
  610. if dst is None:
  611. dst = (self.rank_in_group + 1) % self.world_size
  612. pynccl_comm = self.pynccl_comm
  613. if pynccl_comm is not None and not pynccl_comm.disabled:
  614. pynccl_comm.send(tensor, dst)
  615. else:
  616. torch.distributed.send(tensor, self.ranks[dst], self.device_group)
  617. def recv(
  618. self, size: torch.Size, dtype: torch.dtype, src: int | None = None
  619. ) -> torch.Tensor:
  620. """Receives a tensor from the source rank."""
  621. """NOTE: `src` is the local rank of the source rank."""
  622. if src is None:
  623. src = (self.rank_in_group - 1) % self.world_size
  624. tensor = torch.empty(size, dtype=dtype, device=self.device)
  625. pynccl_comm = self.pynccl_comm
  626. if pynccl_comm is not None and not pynccl_comm.disabled:
  627. pynccl_comm.recv(tensor, src)
  628. else:
  629. torch.distributed.recv(tensor, self.ranks[src], self.device_group)
  630. return tensor
  631. def broadcast(self, tensor: torch.Tensor, src: int = 0) -> torch.Tensor:
  632. """Broadcast a tensor from source rank to all ranks."""
  633. if self.world_size == 1:
  634. return tensor
  635. pynccl_comm = self.pynccl_comm
  636. if pynccl_comm is not None and not pynccl_comm.disabled:
  637. pynccl_comm.broadcast(tensor, src)
  638. return tensor
  639. else:
  640. raise ValueError("No PyNCCL communicator found")
  641. def destroy(self):
  642. if self.pynccl_comm is not None:
  643. self.pynccl_comm.destroy()
  644. self.pynccl_comm = None
  645. if self.ca_comm is not None:
  646. self.ca_comm = None
  647. if self.aiter_ar_comm is not None:
  648. self.aiter_ar_comm.close()
  649. self.aiter_ar_comm = None
  650. if self.fi_ar_comm is not None:
  651. self.fi_ar_comm.destroy()
  652. self.fi_ar_comm = None
  653. if self.fi_pcie_ipc_ar_comm is not None:
  654. self.fi_pcie_ipc_ar_comm.destroy()
  655. self.fi_pcie_ipc_ar_comm = None
  656. if self.all2all_manager is not None:
  657. self.all2all_manager.destroy()
  658. self.all2all_manager = None # type: ignore[assignment]
  659. def _can_use_aiter_ag_rs(self, sizes: list[int] | None) -> bool:
  660. """Whether the AITER custom AG/RS fast path may run for this collective.
  661. Requires:
  662. - uniform batches
  663. - FULL CUDAgraphs
  664. """
  665. if (
  666. not self.use_aiter_ag_rs
  667. or self.aiter_ar_comm is None
  668. or self.aiter_ar_comm.disabled
  669. ):
  670. return False
  671. if sizes is not None:
  672. return False
  673. from vllm.config.compilation import CUDAGraphMode
  674. from vllm.forward_context import get_forward_context
  675. try:
  676. ctx = get_forward_context()
  677. except AssertionError:
  678. return False
  679. if ctx.cudagraph_runtime_mode != CUDAGraphMode.FULL:
  680. return False
  681. bd = ctx.batch_descriptor
  682. return bd is not None and bd.uniform
  683. def suspend(self) -> None:
  684. if self.pynccl_comm is not None:
  685. self.pynccl_comm.suspend()
  686. def resume(self) -> None:
  687. if self.pynccl_comm is not None:
  688. self.pynccl_comm.resume()
  689. def checkpoint_prepare(self) -> None:
  690. # Only FlashInfer all-reduce and FlashInfer all2all are supported for now.
  691. from .flashinfer_all_reduce import checkpoint_prepare_fi_ar_workspaces
  692. checkpoint_prepare_fi_ar_workspaces(self.cpu_group)
  693. if self.all2all_manager is not None:
  694. self.all2all_manager.checkpoint_prepare()
  695. def checkpoint_restore(self) -> None:
  696. # Only FlashInfer all-reduce and FlashInfer all2all are supported for now.
  697. from .flashinfer_all_reduce import checkpoint_restore_fi_ar_workspaces
  698. checkpoint_restore_fi_ar_workspaces(self.cpu_group)
  699. if self.all2all_manager is not None:
  700. self.all2all_manager.checkpoint_restore()
  701. def all_gatherv(
  702. self,
  703. input_: torch.Tensor | list[torch.Tensor],
  704. dim: int = 0,
  705. sizes: list[int] | None = None,
  706. ):
  707. if dim != 0:
  708. raise NotImplementedError("only dim 0 all-gatherv is supported")
  709. world_size = self.world_size
  710. # 'sizes' is not needed if all inputs in the same group have the same
  711. # shape
  712. if sizes is not None and all(s == sizes[0] for s in sizes):
  713. sizes = None
  714. if self._can_use_aiter_ag_rs(sizes):
  715. aiter_comm = self.aiter_ar_comm
  716. assert aiter_comm is not None
  717. if isinstance(input_, torch.Tensor):
  718. if aiter_comm.should_custom_ag(input_):
  719. out = aiter_comm.custom_all_gather(input_, dim=0)
  720. if out is not None:
  721. return out
  722. elif all(aiter_comm.should_custom_ag(inp) for inp in input_):
  723. outs = [aiter_comm.custom_all_gather(inp, dim=0) for inp in input_]
  724. if all(o is not None for o in outs):
  725. return outs
  726. pynccl_comm = self.pynccl_comm
  727. assert pynccl_comm is not None and not pynccl_comm.disabled
  728. # Symmetric memory is only used when all ranks have uniform sizes.
  729. # ncclCommWindowRegister is collective: asymmetric pool allocations
  730. # from variable per-rank sizes cause deadlocks.
  731. if sizes is None and should_nccl_symm_mem_ag_rs():
  732. if isinstance(input_, torch.Tensor):
  733. return self._all_gather_symm_mem(input_)
  734. return self._all_gather_batched_symm_mem(input_)
  735. def _all_gather_single(input_: torch.Tensor, sizes: list[int] | None = None):
  736. input_size = input_.size()
  737. if sizes is not None:
  738. assert len(sizes) == world_size
  739. assert input_.shape[dim] == sizes[self.rank_in_group], (
  740. f"{input_.shape[dim]} != {sizes[self.rank_in_group]}"
  741. )
  742. output_size = (sum(sizes),) + input_size[1:]
  743. else:
  744. output_size = (input_size[0] * world_size,) + input_size[1:]
  745. # Allocate output tensor.
  746. output_tensor = torch.empty(
  747. output_size, dtype=input_.dtype, device=input_.device
  748. )
  749. if sizes is not None:
  750. pynccl_comm.all_gatherv(output_tensor, input_, sizes=sizes)
  751. else:
  752. pynccl_comm.all_gather(output_tensor, input_)
  753. return output_tensor
  754. if isinstance(input_, torch.Tensor):
  755. return _all_gather_single(input_, sizes)
  756. output_list = []
  757. pynccl_comm.group_start()
  758. for inp in input_:
  759. output_list.append(_all_gather_single(inp, sizes=sizes))
  760. pynccl_comm.group_end()
  761. return output_list
  762. def _all_gather_symm_mem(self, input_: torch.Tensor) -> torch.Tensor:
  763. """AllGather a single tensor using NCCL symmetric memory (NVLS).
  764. Only the output needs to be in symmetric memory; NCCL does not
  765. require the AG input to be symmetrically allocated.
  766. """
  767. pynccl_comm = self.pynccl_comm
  768. assert pynccl_comm is not None
  769. out_size = (input_.size(0) * self.world_size,) + tuple(input_.size()[1:])
  770. # Persistent pre-registered scratch avoids the per-call symm-mem context
  771. # snapshot/registration overhead (see _get_symm_scratch).
  772. symm_output = self._get_symm_scratch(
  773. "ag_out", out_size, input_.dtype, input_.device
  774. )
  775. pynccl_comm.all_gather(symm_output, input_)
  776. return symm_output
  777. def _all_gather_batched_symm_mem(
  778. self, inputs: list[torch.Tensor]
  779. ) -> list[torch.Tensor]:
  780. """AllGather a list of tensors using NCCL symmetric memory (NVLS).
  781. Uses group_start/group_end to batch the collectives.
  782. Only the output needs to be in symmetric memory (see
  783. _all_gather_symm_mem).
  784. """
  785. from vllm.distributed.device_communicators.pynccl_allocator import (
  786. nccl_symm_mem_context,
  787. )
  788. pynccl_comm = self.pynccl_comm
  789. assert pynccl_comm is not None
  790. world_size = self.world_size
  791. symm_outputs = []
  792. with nccl_symm_mem_context(pynccl_comm):
  793. for inp in inputs:
  794. out_size = (inp.size(0) * world_size,) + inp.size()[1:]
  795. symm_outputs.append(
  796. torch.empty(out_size, dtype=inp.dtype, device=inp.device)
  797. )
  798. pynccl_comm.group_start()
  799. for symm_out, inp in zip(symm_outputs, inputs):
  800. pynccl_comm.all_gather(symm_out, inp)
  801. pynccl_comm.group_end()
  802. return symm_outputs
  803. def dispatch_router_logits(
  804. self,
  805. hidden_states: torch.Tensor,
  806. router_logits: torch.Tensor,
  807. is_sequence_parallel: bool = False,
  808. extra_tensors: list[torch.Tensor] | None = None,
  809. ) -> (
  810. tuple[torch.Tensor, torch.Tensor]
  811. | tuple[torch.Tensor, torch.Tensor, list[torch.Tensor]]
  812. ):
  813. """Dispatch the hidden states and router logits to the appropriate device.
  814. This is a no-op in the base class.
  815. """
  816. assert self.all2all_manager is not None
  817. return self.all2all_manager.dispatch_router_logits(
  818. hidden_states,
  819. router_logits,
  820. is_sequence_parallel,
  821. extra_tensors,
  822. )
  823. def dispatch(
  824. self,
  825. hidden_states: torch.Tensor,
  826. topk_weights: torch.Tensor,
  827. topk_ids: torch.Tensor,
  828. is_sequence_parallel: bool = False,
  829. extra_tensors: list[torch.Tensor] | None = None,
  830. ) -> (
  831. tuple[torch.Tensor, torch.Tensor, torch.Tensor]
  832. | tuple[torch.Tensor, torch.Tensor, torch.Tensor, list[torch.Tensor]]
  833. ):
  834. """Dispatch the hidden states and topk weights/ids to the appropriate device.
  835. This is a no-op in the base class.
  836. """
  837. assert self.all2all_manager is not None
  838. return self.all2all_manager.dispatch(
  839. hidden_states,
  840. topk_weights,
  841. topk_ids,
  842. is_sequence_parallel,
  843. extra_tensors=extra_tensors,
  844. )
  845. def combine(
  846. self, hidden_states: torch.Tensor, is_sequence_parallel: bool = False
  847. ) -> torch.Tensor:
  848. """Combine the hidden states and router logits from the appropriate device.
  849. This is a no-op in the base class.
  850. """
  851. assert self.all2all_manager is not None
  852. return self.all2all_manager.combine(
  853. hidden_states,
  854. is_sequence_parallel,
  855. )
  856. def batch_isend_irecv(self, p2p_ops: list):
  857. pynccl_comm = self.pynccl_comm
  858. if pynccl_comm is not None and not pynccl_comm.disabled:
  859. pynccl_comm.batch_isend_irecv(p2p_ops)
  860. else:
  861. raise ValueError("No PyNCCL communicator found")