| 1 | #pragma once |
| 2 | |
| 3 | #include <vector> |
| 4 | #include <hip/hip_runtime.h> |
| 5 | #include "quick_reduce_impl.cuh" |
| 6 | |
| 7 | #define HIP_CHECK(err) \ |
| 8 | do { \ |
| 9 | hipError_t err_ = (err); \ |
| 10 | if (err_ != hipSuccess) { \ |
| 11 | std::printf("HIP error %d at %s:%d. %s\n", err_, __FILE__, __LINE__, \ |
| 12 | hipGetErrorString(err_)); \ |
| 13 | throw std::runtime_error("HIP error"); \ |
| 14 | } \ |
| 15 | } while (0) |
| 16 | |
| 17 | namespace quickreduce { |
| 18 | using fptr_t = int64_t; |
| 19 | static_assert(sizeof(void*) == sizeof(fptr_t)); |
| 20 | |
| 21 | template <typename AllReduceKernel, typename T> |
| 22 | __global__ __quickreduce_launch_bounds_two_shot__ static void |
| 23 | allreduce_prototype_twoshot(T const* A, T* B, uint32_t N, uint32_t num_blocks, |
| 24 | int rank, uint8_t** dbuffer_list, |
| 25 | uint32_t data_offset, uint32_t* d_flag_counters, |
| 26 | int64_t data_size_per_phase) { |
| 27 | int block = blockIdx.x; |
| 28 | int grid = gridDim.x; |
| 29 | |
| 30 | // Load this block's counter from device memory and advance it on-device, |
| 31 | // so the color keeps changing across graph replays instead of being frozen. |
| 32 | uint32_t flag_color = d_flag_counters[blockIdx.x]; |
| 33 | |
| 34 | while (block < num_blocks) { |
| 35 | AllReduceKernel::run(A, B, N, block, rank, dbuffer_list, data_offset, |
| 36 | flag_color, data_size_per_phase); |
| 37 | block += grid; |
| 38 | flag_color++; |
| 39 | } |
| 40 | // All threads compute the same final value; one writer per block is enough. |
| 41 | if (threadIdx.x == 0 && threadIdx.y == 0) { |
| 42 | d_flag_counters[blockIdx.x] = flag_color; |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | #define TWOSHOT_DISPATCH(__codec) \ |
| 47 | if (world_size == 2) { \ |
| 48 | using LineCodec = __codec<T, 2>; \ |
| 49 | using AllReduceKernel = AllReduceTwoshot<T, LineCodec, cast_bf2half>; \ |
| 50 | hipLaunchKernelGGL((allreduce_prototype_twoshot<AllReduceKernel, T>), \ |
| 51 | dim3(grid), dim3(kBlockTwoShot), 0, stream, A, B, N, \ |
| 52 | num_blocks, rank, dbuffer_list, data_offset, \ |
| 53 | d_flag_counters, this->kMaxProblemSize); \ |
| 54 | } else if (world_size == 4) { \ |
| 55 | using LineCodec = __codec<T, 4>; \ |
| 56 | using AllReduceKernel = AllReduceTwoshot<T, LineCodec, cast_bf2half>; \ |
| 57 | hipLaunchKernelGGL((allreduce_prototype_twoshot<AllReduceKernel, T>), \ |
| 58 | dim3(grid), dim3(kBlockTwoShot), 0, stream, A, B, N, \ |
| 59 | num_blocks, rank, dbuffer_list, data_offset, \ |
| 60 | d_flag_counters, this->kMaxProblemSize); \ |
| 61 | } else if (world_size == 8) { \ |
| 62 | using LineCodec = __codec<T, 8>; \ |
| 63 | using AllReduceKernel = AllReduceTwoshot<T, LineCodec, cast_bf2half>; \ |
| 64 | hipLaunchKernelGGL((allreduce_prototype_twoshot<AllReduceKernel, T>), \ |
| 65 | dim3(grid), dim3(kBlockTwoShot), 0, stream, A, B, N, \ |
| 66 | num_blocks, rank, dbuffer_list, data_offset, \ |
| 67 | d_flag_counters, this->kMaxProblemSize); \ |
| 68 | } |
| 69 | |
| 70 | // INT3 only retains good performance on TP2 (world_size == 2). On TP4/TP8 |
| 71 | // the 3-bit codec's pack/unpack overhead outweighs the reduced communication |
| 72 | // volume, so INT3 is restricted to a TP2-only dispatch here. |
| 73 | #define TWOSHOT_DISPATCH_TP2_ONLY(__codec) \ |
| 74 | if (world_size == 2) { \ |
| 75 | using LineCodec = __codec<T, 2>; \ |
| 76 | using AllReduceKernel = AllReduceTwoshot<T, LineCodec, cast_bf2half>; \ |
| 77 | hipLaunchKernelGGL((allreduce_prototype_twoshot<AllReduceKernel, T>), \ |
| 78 | dim3(grid), dim3(kBlockTwoShot), 0, stream, A, B, N, \ |
| 79 | num_blocks, rank, dbuffer_list, data_offset, \ |
| 80 | d_flag_counters, this->kMaxProblemSize); \ |
| 81 | } else { \ |
| 82 | throw std::runtime_error( \ |
| 83 | "INT3 quick all-reduce is only supported for world_size == 2 " \ |
| 84 | "(TP2); use INT4/NONE for larger world sizes."); \ |
| 85 | } |
| 86 | |
| 87 | enum QuickReduceQuantLevel { |
| 88 | // Keep these ids in sync with Python QuickReduceRegime enum. |
| 89 | F16 = 0, // full-precision fp16/bf16 communication |
| 90 | INT8 = 1, // symmetric int8 + per-block scale |
| 91 | INT6 = 2, // symmetric int6 + per-block scale |
| 92 | INT4 = 3, // symmetric int4 + per-block scale |
| 93 | INT3 = 4, // symmetric int3 + per-block scale (TP2 only) |
| 94 | }; |
| 95 | |
| 96 | struct DeviceComms { |
| 97 | // Max problem size is 2GB (in bytes) or half of uint32_t max value. |
| 98 | int64_t kMaxProblemSize = |
| 99 | static_cast<int64_t>(std::numeric_limits<int32_t>::max()) + 1; |
| 100 | |
| 101 | // Max TP-8 |
| 102 | static int constexpr kMaxWorldSize = 8; |
| 103 | |
| 104 | bool initialized = false; |
| 105 | uint32_t* d_flag_counters = nullptr; |
| 106 | int world_size; |
| 107 | int rank; |
| 108 | |
| 109 | uint8_t* dbuffer; |
| 110 | uint8_t** dbuffer_list; |
| 111 | hipIpcMemHandle_t buffer_ipc_handle; |
| 112 | std::vector<hipIpcMemHandle_t> all_buffer_ipc_handles; |
| 113 | std::vector<uint8_t*> buffer_list; |
| 114 | uint32_t data_offset; |
| 115 | |
| 116 | DeviceComms() : initialized(false), world_size(1), rank(0) {} |
| 117 | ~DeviceComms() { destroy(); } |
| 118 | |
| 119 | void init(int world_size, int rank, |
| 120 | std::optional<int64_t> max_problem_size = std::nullopt) { |
| 121 | destroy(); |
| 122 | this->world_size = world_size; |
| 123 | this->rank = rank; |
| 124 | if (max_problem_size.has_value() && max_problem_size.value() > 0) { |
| 125 | this->kMaxProblemSize = max_problem_size.value(); |
| 126 | } |
| 127 | // Allocate buffer size for worst case: F16 2-stage buffer. |
| 128 | uint32_t flags_buffer_size = |
| 129 | 2 * world_size * kMaxNumBlocks * sizeof(uint32_t); |
| 130 | static int64_t data_buffer_size = 2 * this->kMaxProblemSize; |
| 131 | int64_t total_buffer_size = flags_buffer_size + data_buffer_size; |
| 132 | data_offset = flags_buffer_size; |
| 133 | HIP_CHECK(hipExtMallocWithFlags((void**)&dbuffer, total_buffer_size, |
| 134 | hipDeviceMallocUncached)); |
| 135 | |
| 136 | // Clear the flags buffer. |
| 137 | HIP_CHECK(hipMemset(dbuffer, 0, flags_buffer_size)); |
| 138 | |
| 139 | // One flag-color counter per block, advanced by the kernel. Start at 1 |
| 140 | // to stay clear of the flags buffer we just zeroed. |
| 141 | HIP_CHECK(hipMalloc(&d_flag_counters, kMaxNumBlocks * sizeof(uint32_t))); |
| 142 | { |
| 143 | std::vector<uint32_t> init_color(kMaxNumBlocks, 1u); |
| 144 | HIP_CHECK(hipMemcpy(d_flag_counters, init_color.data(), |
| 145 | kMaxNumBlocks * sizeof(uint32_t), |
| 146 | hipMemcpyHostToDevice)); |
| 147 | } |
| 148 | |
| 149 | // Device-side list of IPC buffers. |
| 150 | buffer_list.resize(world_size); |
| 151 | HIP_CHECK(hipMalloc(&dbuffer_list, world_size * sizeof(uint8_t*))); |
| 152 | |
| 153 | // Create IPC handles for rank's communication buffer. |
| 154 | all_buffer_ipc_handles.resize(world_size); |
| 155 | HIP_CHECK(hipIpcGetMemHandle(&buffer_ipc_handle, dbuffer)); |
| 156 | |
| 157 | initialized = true; |
| 158 | } |
| 159 | int get_world_size() { return world_size; } |
| 160 | int get_rank() { return rank; } |
| 161 | bool status() { return initialized; } |
| 162 | hipIpcMemHandle_t const get_handle() { return buffer_ipc_handle; } |
| 163 | |
| 164 | void destroy() { |
| 165 | // Allocated before `initialized` flips true, so free it on its own guard |
| 166 | // to avoid a leak if init fails partway through. |
| 167 | if (d_flag_counters) { |
| 168 | HIP_CHECK(hipFree(d_flag_counters)); |
| 169 | d_flag_counters = nullptr; |
| 170 | } |
| 171 | if (initialized) { |
| 172 | for (int i = 0; i < world_size; i++) { |
| 173 | if (i != rank) { |
| 174 | HIP_CHECK(hipIpcCloseMemHandle(dbuffer_list[i])); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | HIP_CHECK(hipFree(dbuffer)); |
| 179 | HIP_CHECK(hipFree(dbuffer_list)); |
| 180 | |
| 181 | initialized = false; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | void open_ipc_handles(std::vector<hipIpcMemHandle_t> const& ipc_handles) { |
| 186 | assert(ipc_handles.size() == all_buffer_ipc_handles.size()); |
| 187 | for (int i = 0; i < world_size; i++) { |
| 188 | all_buffer_ipc_handles[i] = ipc_handles[i]; |
| 189 | } |
| 190 | |
| 191 | // Open device memory access to the IPC communication buffers. |
| 192 | // Note: For our own rank, we do not need to open a handle. |
| 193 | for (int i = 0; i < world_size; i++) { |
| 194 | if (i != rank) { |
| 195 | HIP_CHECK(hipIpcOpenMemHandle((void**)&buffer_list[i], |
| 196 | all_buffer_ipc_handles[i], |
| 197 | hipIpcMemLazyEnablePeerAccess)); |
| 198 | } else { |
| 199 | buffer_list[i] = dbuffer; |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | HIP_CHECK(hipMemcpy(dbuffer_list, buffer_list.data(), |
| 204 | world_size * sizeof(uint8_t*), hipMemcpyHostToDevice)); |
| 205 | } |
| 206 | |
| 207 | template <typename T, bool cast_bf2half> |
| 208 | void allreduce(T const* A, T* B, uint32_t N, int quant_level, |
| 209 | hipStream_t stream) { |
| 210 | if (world_size != 2 && world_size != 4 && world_size != 8) { |
| 211 | throw std::runtime_error("All Reduce not supported for world_size = " + |
| 212 | std::to_string(world_size)); |
| 213 | } |
| 214 | |
| 215 | // Configuration. |
| 216 | uint32_t msg_size = N * sizeof(T); |
| 217 | uint32_t num_blocks = divceil(msg_size, kTileSize); |
| 218 | uint32_t grid = min(kMaxNumBlocks, num_blocks); |
| 219 | auto quant_level_ = static_cast<QuickReduceQuantLevel>(quant_level); |
| 220 | switch (quant_level_) { |
| 221 | case QuickReduceQuantLevel::INT8: |
| 222 | TWOSHOT_DISPATCH(CodecQ8) |
| 223 | break; |
| 224 | case QuickReduceQuantLevel::INT6: |
| 225 | TWOSHOT_DISPATCH(CodecQ6) |
| 226 | break; |
| 227 | case QuickReduceQuantLevel::INT4: |
| 228 | TWOSHOT_DISPATCH(CodecQ4) |
| 229 | break; |
| 230 | case QuickReduceQuantLevel::INT3: |
| 231 | TWOSHOT_DISPATCH_TP2_ONLY(CodecQ3) |
| 232 | break; |
| 233 | default: |
| 234 | TWOSHOT_DISPATCH(CodecFP) |
| 235 | break; |
| 236 | } |
| 237 | HIP_CHECK(cudaGetLastError()); |
| 238 | } |
| 239 | }; |
| 240 | |
| 241 | } // namespace quickreduce |