main
py 262 lines 9.37 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Shared NCCL initialization helpers for weight transfer engines.
4
5 The dense (`NCCLWeightTransferEngine`) and sparse
6 (`SparseNCCLWeightTransferEngine`) backends are independent engines that share
7 *only* their process-group initialization. That common logic lives here so the
8 sparse engine does not have to subclass the dense one.
9 """
10
11 from dataclasses import asdict, dataclass, field
12 from typing import TYPE_CHECKING, Protocol
13
14 import pybase64 as base64
15 import torch
16
17 if TYPE_CHECKING:
18 from vllm.config.parallel import ParallelConfig
19 from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
20
21 from vllm.distributed.device_communicators.pynccl_wrapper import (
22 NCCL_UNIQUE_ID_BYTES,
23 )
24 from vllm.distributed.weight_transfer.base import WeightTransferInitInfo
25 from vllm.distributed.weight_transfer.packed_tensor import (
26 DEFAULT_PACKED_BUFFER_SIZE_BYTES,
27 DEFAULT_PACKED_NUM_BUFFERS,
28 )
29 from vllm.utils.nccl import unpinned_nccl_env
30
31
32 def decode_nccl_unique_id(
33 *,
34 master_address: str | None,
35 master_port: int | None,
36 nccl_unique_id_b64: str | None,
37 ctx: str,
38 ) -> bytes | None:
39 """Validate the rendezvous mode and decode a pre-shared unique id."""
40 has_uid = bool(nccl_unique_id_b64)
41 has_addr = master_address is not None
42 has_port = master_port is not None
43
44 if has_uid and (has_addr or has_port):
45 raise ValueError(
46 f"{ctx}: pass nccl_unique_id_b64 OR master_address/master_port, not both"
47 )
48 if not has_uid:
49 if not (has_addr and has_port):
50 raise ValueError(
51 f"{ctx}: need nccl_unique_id_b64, or both master_address and "
52 f"master_port (got master_address={master_address!r}, "
53 f"master_port={master_port!r})"
54 )
55 return None
56 try:
57 decoded = base64.b64decode(nccl_unique_id_b64, validate=True)
58 except (ValueError, TypeError) as e:
59 raise ValueError(
60 f"{ctx}: nccl_unique_id_b64 is not valid standard base64 "
61 f"(URL-safe base64 is not accepted)"
62 ) from e
63 if len(decoded) != NCCL_UNIQUE_ID_BYTES:
64 raise ValueError(
65 f"{ctx}: nccl_unique_id_b64 must decode to {NCCL_UNIQUE_ID_BYTES} "
66 f"bytes, got {len(decoded)}"
67 )
68 return decoded
69
70
71 @dataclass(kw_only=True)
72 class NCCLWeightTransferInitInfo(WeightTransferInitInfo):
73 """Worker-side initialization info for NCCL-based weight transfer backends.
74
75 Keyword-only (`kw_only`): adding the optional `nccl_unique_id_b64` field
76 means the rendezvous fields can no longer keep a fixed positional slot, so a
77 stale positional call fails loudly instead of silently swapping arguments.
78
79 Provide exactly one rendezvous mode:
80
81 * ``master_address`` + ``master_port`` -- TCPStore / ``StatelessProcessGroup``
82 rendezvous (requires torch on every rank, including the trainer), or
83 * ``nccl_unique_id_b64`` -- standard (RFC 4648, *not* URL-safe) base64 of the
84 128 raw bytes from ``ncclGetUniqueId``, for torch-free trainers (e.g. JAX)
85 that mint the unique id out of band and share it (over HTTP, etc.). Note a
86 JAX peer must use ``base64.b64encode``, not ``urlsafe_b64encode``.
87
88 On the unique-id path all ranks must enter init concurrently (there is no
89 store barrier), and every peer must honor the warm-up handshake: the
90 worker's communicator issues a one-element ``all_reduce`` immediately after
91 ``ncclCommInitRank`` (see ``PyNcclCommunicator.from_unique_id_bytes``), so a
92 foreign peer must issue a matching one-element ``all_reduce`` before any
93 other collective or all ranks deadlock.
94 """
95
96 master_address: str | None = None
97 master_port: int | None = None
98 rank_offset: int
99 world_size: int
100 nccl_unique_id_b64: str | None = field(default=None, repr=False)
101 packed: bool = False
102 packed_buffer_size_bytes: int = DEFAULT_PACKED_BUFFER_SIZE_BYTES
103 packed_num_buffers: int = DEFAULT_PACKED_NUM_BUFFERS
104
105 def __post_init__(self) -> None:
106 _ = self.nccl_unique_id_bytes
107
108 @property
109 def nccl_unique_id_bytes(self) -> bytes | None:
110 return decode_nccl_unique_id(
111 master_address=self.master_address,
112 master_port=self.master_port,
113 nccl_unique_id_b64=self.nccl_unique_id_b64,
114 ctx="NCCLWeightTransferInitInfo",
115 )
116
117
118 def worker_init_payload(init_info: NCCLWeightTransferInitInfo) -> dict:
119 """Serialize a worker init info for `init_weight_transfer_engine`, dropping
120 the unset rendezvous field (the UID in TCP mode) so the wire payload carries
121 only the mode actually in use. Shared by the dense and sparse trainer
122 engines so the two cannot drift."""
123 return {key: value for key, value in asdict(init_info).items() if value is not None}
124
125
126 class NCCLRendezvous(Protocol):
127 """The TCP rendezvous fields `trainer_init` needs.
128
129 Structural so each backend can keep its own trainer init info next to its
130 engine (dense `NCCLTrainerInitInfo`, `SparseNCCLTrainerInitInfo`) without
131 this shared module importing either.
132 """
133
134 master_address: str
135 master_port: int
136 world_size: int
137
138
139 def stateless_init_process_group(
140 master_address: str,
141 master_port: int,
142 rank: int,
143 world_size: int,
144 device,
145 ) -> "PyNcclCommunicator":
146 """VLLM provides `StatelessProcessGroup` to create a process group
147 without considering the global process group in torch.distributed.
148 It is recommended to create `StatelessProcessGroup`, and then initialize
149 the data-plane communication (NCCL) between external (train processes)
150 and vLLM workers.
151 """
152 from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
153 from vllm.distributed.utils import StatelessProcessGroup
154
155 pg = StatelessProcessGroup.create(
156 host=master_address, port=master_port, rank=rank, world_size=world_size
157 )
158 with unpinned_nccl_env():
159 return PyNcclCommunicator(pg, device=device)
160
161
162 def uid_init_process_group(
163 nccl_unique_id_bytes: bytes,
164 rank: int,
165 world_size: int,
166 device,
167 ) -> "PyNcclCommunicator":
168 """Join the NCCL group from pre-shared ``ncclUniqueId`` bytes.
169
170 The torch-free rendezvous alternative to `stateless_init_process_group`: no
171 TCPStore, and therefore no barrier -- every rank must enter concurrently.
172 """
173 from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
174
175 with unpinned_nccl_env():
176 return PyNcclCommunicator.from_unique_id_bytes(
177 nccl_unique_id_bytes,
178 rank=rank,
179 world_size=world_size,
180 device=device,
181 )
182
183
184 def worker_init_process_group(
185 init_info: NCCLWeightTransferInitInfo,
186 parallel_config: "ParallelConfig",
187 ) -> "PyNcclCommunicator":
188 """Create the trainer<->worker NCCL group on an inference worker.
189
190 Computes a unique rank for this worker across all data-parallel groups and
191 joins the trainer via whichever rendezvous mode `init_info` carries.
192 """
193 # Calculate the global rank in the trainer-worker process group.
194 # Must account for data parallel to get unique ranks across all workers.
195 dp_rank = parallel_config.data_parallel_index
196 world_size_per_dp = parallel_config.world_size # TP * PP
197 rank_within_dp = parallel_config.rank
198
199 # Unique rank across all DP groups
200 worker_rank = dp_rank * world_size_per_dp + rank_within_dp
201 rank = worker_rank + init_info.rank_offset
202
203 device = torch.accelerator.current_device_index()
204 unique_id_bytes = init_info.nccl_unique_id_bytes
205 if unique_id_bytes is not None:
206 return uid_init_process_group(
207 unique_id_bytes,
208 rank,
209 init_info.world_size,
210 device,
211 )
212 # __post_init__ guarantees the TCP pair when there is no unique id.
213 assert init_info.master_address is not None
214 assert init_info.master_port is not None
215 return stateless_init_process_group(
216 init_info.master_address,
217 init_info.master_port,
218 rank,
219 init_info.world_size,
220 device=device,
221 )
222
223
224 def trainer_init(
225 init_info: NCCLRendezvous | dict,
226 rank: int = 0,
227 ) -> "PyNcclCommunicator":
228 """Initialize NCCL process group for trainer-side weight transfer.
229
230 Uses the current CUDA device (torch.accelerator.current_device_index()).
231
232 Args:
233 init_info: Any object carrying the `NCCLRendezvous` fields (a trainer or
234 worker NCCL init info), or a dict with keys:
235 - master_address: str
236 - master_port: int
237 - world_size: int
238 rank: This trainer process's rank in the group. The broadcast backends
239 have a single trainer at rank 0; multi-rank trainers (m2n) occupy
240 ranks `[0, num_trainer_ranks)` and the workers start after them.
241
242 Returns:
243 PyNcclCommunicator for weight transfer.
244
245 """
246 if isinstance(init_info, dict):
247 master_address = init_info["master_address"]
248 master_port = init_info["master_port"]
249 world_size = init_info["world_size"]
250 else:
251 master_address = init_info.master_address
252 master_port = init_info.master_port
253 world_size = init_info.world_size
254
255 device = torch.accelerator.current_device_index()
256 return stateless_init_process_group(
257 master_address,
258 master_port,
259 rank,
260 world_size,
261 device,
262 )