main
cuh 362 lines 14.2 KB
Raw
1 #pragma once
2
3 #include "custom_collective_common.cuh"
4
5 namespace vllm {
6
7 template <typename T, int ngpus>
8 __global__ void __launch_bounds__(512, 1)
9 cross_device_reduce_1stage(RankData* _dp, RankSignals sg, Signal* self_sg,
10 T* __restrict__ result, int rank, int size) {
11 using P = typename packed_t<T>::P;
12 using A = typename packed_t<T>::A;
13 // note: we don't reorder the address so the accumulation order is the same
14 // for all ranks, ensuring bitwise identical results
15 auto dp = *_dp;
16 barrier_at_start<ngpus>(sg, self_sg, rank);
17 // do the actual reduction
18 for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < size;
19 idx += gridDim.x * blockDim.x) {
20 ((P*)result)[idx] = packed_reduce<P, ngpus, A>((const P**)&dp.ptrs[0], idx);
21 }
22 barrier_at_end<ngpus, true>(sg, self_sg, rank);
23 }
24
25 template <typename P>
26 DINLINE P* get_tmp_buf(Signal* sg) {
27 return (P*)(((Signal*)sg) + 1);
28 }
29
30 template <typename T, int ngpus>
31 __global__ void __launch_bounds__(512, 1)
32 cross_device_reduce_2stage(RankData* _dp, RankSignals sg, Signal* self_sg,
33 T* __restrict__ result, int rank, int size) {
34 int tid = blockIdx.x * blockDim.x + threadIdx.x;
35 int stride = gridDim.x * blockDim.x;
36 using P = typename packed_t<T>::P;
37 using A = typename packed_t<T>::A;
38 int part = size / ngpus;
39 int start = rank * part;
40 int end = rank == ngpus - 1 ? size : start + part;
41 int largest_part = part + size % ngpus;
42 const P* ptrs[ngpus];
43 P* tmps[ngpus];
44 #pragma unroll
45 for (int i = 0; i < ngpus; i++) {
46 int target = (rank + i) % ngpus;
47 ptrs[i] = (const P*)_dp->ptrs[target];
48 tmps[i] = get_tmp_buf<P>(sg.signals[target]);
49 }
50 auto tmp_out = tmps[0];
51 barrier_at_start<ngpus>(sg, self_sg, rank);
52
53 // stage 1: reduce scatter
54 for (int idx = start + tid; idx < end; idx += stride) {
55 tmp_out[idx - start] = packed_reduce<P, ngpus, A>(ptrs, idx);
56 }
57 barrier_at_end<ngpus>(sg, self_sg, rank);
58
59 // stage 2: allgather. Note: it's important to match the tid between
60 // the two stages, because visibility across devices is only guaranteed
61 // between threads that have the same tid. If thread i computes the sum of
62 // start + i in the first stage, then thread i also gathers start + i from
63 // all ranks.
64
65 for (int idx = tid; idx < largest_part; idx += stride) {
66 #pragma unroll
67 for (int i = 0; i < ngpus; i++) {
68 int gather_from_rank = ((rank + i) % ngpus);
69 if (gather_from_rank == ngpus - 1 || idx < part) {
70 int dst_idx = gather_from_rank * part + idx;
71 ((P*)result)[dst_idx] = tmps[i][idx];
72 }
73 }
74 }
75 }
76
77 using IPC_KEY = std::array<uint8_t, sizeof(cudaIpcMemHandle_t)>;
78 static_assert(sizeof(IPC_KEY) == sizeof(cudaIpcMemHandle_t));
79 static_assert(alignof(IPC_KEY) == alignof(cudaIpcMemHandle_t));
80
81 class CustomAllreduce {
82 public:
83 int rank_;
84 int world_size_;
85 // Full NVLink or xGMI connection between GPUs.
86 bool fully_connected_;
87
88 RankSignals sg_;
89 // Stores a map from a pointer to its peer pointers from all ranks.
90 std::unordered_map<void*, RankData*> buffers_;
91 Signal* self_sg_;
92
93 // Stores rank data from all ranks. This is mainly for cuda graph purposes.
94 // For cuda graph to work, all kernel arguments must be fixed during graph
95 // capture time. However, the peer pointers are not known during graph
96 // capture time. Therefore, during capture, we increment the rank data
97 // pointer and use that as the argument to the kernel. The kernel arguments
98 // are stored in graph_unreg_buffers_. The actual peer pointers will be
99 // filled in at the memory pointed to by the pointers in
100 // graph_unreg_buffers_ when the IPC handles are exchanged between ranks.
101 //
102 // The overall process looks like this:
103 // 1. Graph capture.
104 // 2. Each rank obtains the IPC handles for each addresses used during cuda
105 // graph capture using get_graph_buffer_ipc_meta.
106 // 3. (In Python) all gather the IPC handles.
107 // 4. Obtain the peer pointers by opening the IPC handles, and store them in
108 // the rank data array at corresponding positions.
109 RankData *d_rank_data_base_, *d_rank_data_end_;
110 std::vector<void*> graph_unreg_buffers_;
111 // a map from IPC handles to opened IPC pointers
112 std::map<IPC_KEY, char*> ipc_handles_;
113
114 /**
115 * Signals are an array of ipc-enabled buffers from all ranks.
116 * For each of the buffer, the layout is as follows:
117 * | -- sizeof(Signal) -- | ------ a few MB ----- |
118 * The first section is for allreduce synchronization, and the second
119 * section is for storing the intermediate results required by some
120 * allreduce algos.
121 *
122 * Note: this class does not own any device memory. Any required buffers
123 * are passed in from the constructor.
124 */
125 CustomAllreduce(Signal** signals, void* rank_data, size_t rank_data_sz,
126 int rank, int world_size, bool fully_connected = true)
127 : rank_(rank),
128 world_size_(world_size),
129 fully_connected_(fully_connected),
130 self_sg_(signals[rank]),
131 d_rank_data_base_(reinterpret_cast<RankData*>(rank_data)),
132 d_rank_data_end_(d_rank_data_base_ + rank_data_sz / sizeof(RankData)) {
133 for (int i = 0; i < world_size_; i++) {
134 sg_.signals[i] = signals[i];
135 }
136 }
137
138 char* open_ipc_handle(const void* ipc_handle) {
139 auto [it, new_handle] =
140 ipc_handles_.insert({*((IPC_KEY*)ipc_handle), nullptr});
141 if (new_handle) {
142 char* ipc_ptr;
143 CUDACHECK(cudaIpcOpenMemHandle((void**)&ipc_ptr,
144 *((const cudaIpcMemHandle_t*)ipc_handle),
145 cudaIpcMemLazyEnablePeerAccess));
146 it->second = ipc_ptr;
147 }
148 return it->second;
149 }
150
151 std::pair<std::string, std::vector<int64_t>> get_graph_buffer_ipc_meta() {
152 auto num_buffers = graph_unreg_buffers_.size();
153 auto handle_sz = sizeof(cudaIpcMemHandle_t);
154 std::string handles(handle_sz * num_buffers, static_cast<char>(0));
155 std::vector<int64_t> offsets(num_buffers);
156 for (int i = 0; i < num_buffers; i++) {
157 auto ptr = graph_unreg_buffers_[i];
158 void* base_ptr;
159 // note: must share the base address of each allocation, or we get wrong
160 // address
161 if (cuPointerGetAttribute(&base_ptr, rangeStartAddrAttr,
162 (CUdeviceptr)ptr) != CUDA_SUCCESS)
163 throw std::runtime_error("failed to get pointer attr");
164 CUDACHECK(cudaIpcGetMemHandle(
165 (cudaIpcMemHandle_t*)&handles[i * handle_sz], base_ptr));
166 offsets[i] = ((char*)ptr) - ((char*)base_ptr);
167 }
168 return std::make_pair(handles, offsets);
169 }
170
171 void check_rank_data_capacity(size_t num = 1) {
172 if (d_rank_data_base_ + num > d_rank_data_end_)
173 throw std::runtime_error(
174 "Rank data buffer is overflowed by " +
175 std::to_string(d_rank_data_base_ + num - d_rank_data_end_));
176 }
177
178 /**
179 * Register already-shared IPC pointers.
180 */
181 void register_buffer(void** ptrs) {
182 check_rank_data_capacity();
183 RankData data;
184 for (int i = 0; i < world_size_; i++) {
185 data.ptrs[i] = ptrs[i];
186 }
187 auto d_data = d_rank_data_base_++;
188 CUDACHECK(
189 cudaMemcpy(d_data, &data, sizeof(RankData), cudaMemcpyHostToDevice));
190 buffers_[ptrs[rank_]] = d_data;
191 }
192
193 // Note: when registering graph buffers, we intentionally choose to not
194 // deduplicate the addresses. That means if the allocator reuses some
195 // addresses, they will be registered again. This is to account for the
196 // remote possibility of different allocation patterns between ranks. For
197 // example, rank 1 may get the same input address for the second allreduce,
198 // but rank 2 got a different address. IPC handles have internal reference
199 // counting mechanism so overhead should be small.
200 void register_graph_buffers(
201 const std::vector<std::string>& handles,
202 const std::vector<std::vector<int64_t>>& offsets) {
203 auto num_buffers = graph_unreg_buffers_.size();
204 check_rank_data_capacity(num_buffers);
205 std::vector<RankData> rank_data(num_buffers);
206 for (int i = 0; i < num_buffers; i++) {
207 auto self_ptr = graph_unreg_buffers_[i];
208 auto& rd = rank_data[i];
209 for (int j = 0; j < world_size_; j++) {
210 if (j != rank_) {
211 char* handle =
212 open_ipc_handle(&handles[j][i * sizeof(cudaIpcMemHandle_t)]);
213 handle += offsets[j][i];
214 rd.ptrs[j] = handle;
215 } else {
216 rd.ptrs[j] = self_ptr;
217 }
218 }
219 }
220 CUDACHECK(cudaMemcpy(d_rank_data_base_, rank_data.data(),
221 sizeof(RankData) * num_buffers,
222 cudaMemcpyHostToDevice));
223 d_rank_data_base_ += num_buffers;
224 graph_unreg_buffers_.clear();
225 }
226
227 /**
228 * Performs allreduce, assuming input has already been registered.
229 *
230 * Block and grid default configs are results after careful grid search.
231 * Using 36 blocks give the best or close to the best runtime on the devices
232 * I tried: A100, A10, A30, T4, V100. You'll notice that NCCL kernels also
233 * only take a small amount of SMs. Not quite sure the underlying reason,
234 * but my guess is that too many SMs will cause contention on NVLink bus.
235 */
236 template <typename T>
237 void allreduce(cudaStream_t stream, T* input, T* output, int size,
238 int threads = 512, int block_limit = defaultBlockLimit) {
239 auto d = packed_t<T>::P::size;
240 if (size % d != 0)
241 throw std::runtime_error(
242 "custom allreduce currently requires input length to be multiple "
243 "of " +
244 std::to_string(d));
245 if (block_limit > kMaxBlocks)
246 throw std::runtime_error("max supported block limit is " +
247 std::to_string(kMaxBlocks) + ". Got " +
248 std::to_string(block_limit));
249
250 RankData* ptrs;
251 cudaStreamCaptureStatus status;
252 CUDACHECK(cudaStreamIsCapturing(stream, &status));
253 if (status == cudaStreamCaptureStatusActive) {
254 ptrs = d_rank_data_base_ + graph_unreg_buffers_.size();
255 graph_unreg_buffers_.push_back(input);
256 } else {
257 auto it = buffers_.find(input);
258 if (it == buffers_.end())
259 throw std::runtime_error(
260 "buffer address " +
261 std::to_string(reinterpret_cast<uint64_t>(input)) +
262 " is not registered!");
263 ptrs = it->second;
264 }
265
266 size /= d;
267 auto bytes = size * sizeof(typename packed_t<T>::P);
268 int blocks = std::min(block_limit, (size + threads - 1) / threads);
269
270 // Check environment variable once
271 const char* env_algo = std::getenv("VLLM_CUSTOM_ALLREDUCE_ALGO");
272 bool force_1stage = false;
273 bool force_2stage = false;
274 if (env_algo != nullptr) {
275 if (std::strcmp(env_algo, "1stage") == 0 ||
276 std::strcmp(env_algo, "oneshot") == 0) {
277 force_1stage = true;
278 } else if (std::strcmp(env_algo, "2stage") == 0 ||
279 std::strcmp(env_algo, "twoshot") == 0) {
280 force_2stage = true;
281 } else {
282 throw std::runtime_error(
283 "Invalid VLLM_CUSTOM_ALLREDUCE_ALGO: " + std::string(env_algo) +
284 ". Valid values: 1stage, oneshot, 2stage, twoshot");
285 }
286 }
287
288 #define KL(ngpus, name) \
289 name<T, ngpus><<<blocks, threads, 0, stream>>>(ptrs, sg_, self_sg_, output, \
290 rank_, size);
291 #define REDUCE_CASE(ngpus) \
292 case ngpus: { \
293 if (force_1stage) { \
294 KL(ngpus, cross_device_reduce_1stage); \
295 } else if (force_2stage) { \
296 KL(ngpus, cross_device_reduce_2stage); \
297 } else { \
298 if (world_size_ == 2) { \
299 KL(ngpus, cross_device_reduce_1stage); \
300 } else if (fully_connected_) { \
301 if ((world_size_ <= 4 && bytes < 512 * 1024) || \
302 (world_size_ <= 8 && bytes < 256 * 1024)) { \
303 KL(ngpus, cross_device_reduce_1stage); \
304 } else { \
305 KL(ngpus, cross_device_reduce_2stage); \
306 } \
307 } \
308 } \
309 break; \
310 }
311
312 switch (world_size_) {
313 REDUCE_CASE(2)
314 REDUCE_CASE(4)
315 REDUCE_CASE(6)
316 REDUCE_CASE(8)
317 default:
318 throw std::runtime_error(
319 "custom allreduce only supports num gpus in (2,4,6,8). Actual "
320 "num "
321 "gpus = " +
322 std::to_string(world_size_));
323 }
324 #undef REDUCE_CASE
325 #undef KL
326 }
327
328 void allgather(cudaStream_t stream, void* input, void* output, int size_bytes,
329 int threads = 512, int block_limit = defaultBlockLimit);
330 template <typename T>
331 void mnnvl_lamport_allgather(cudaStream_t stream, T* input, T* output,
332 void* local_buffer, void* multicast_buffer,
333 uint32_t* epochs, int size_bytes,
334 int stage_size_bytes);
335 template <typename T>
336 void reduce_scatter(cudaStream_t stream, T* input, T* output, int size,
337 int threads = 512, int block_limit = defaultBlockLimit);
338 template <typename T>
339 void mnnvl_lamport_reduce_scatter(cudaStream_t stream, T* input, T* output,
340 void* local_buffer, uint32_t* epochs,
341 int size, int stage_size_bytes);
342 template <typename T>
343 void mnnvl_multimem_reduce_scatter(cudaStream_t stream,
344 const T* multicast_input, T* output,
345 Signal* local_signal,
346 Signal* multicast_signal, int size,
347 int block_limit);
348
349 ~CustomAllreduce() {
350 for (auto [_, ptr] : ipc_handles_) {
351 CUDACHECK(cudaIpcCloseMemHandle(ptr));
352 }
353 }
354 };
355
356 /**
357 * To inspect PTX/SASS, copy paste this header file to compiler explorer and
358 * add a template instantiation:
359 * template void vllm::CustomAllreduce::allreduce<half>(cudaStream_t, half *,
360 * half *, int, int, int);
361 */
362 } // namespace vllm