arx.cu 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. // arx: one-shot all-reduce over RoCE for GB10, no NCCL.
  2. //
  3. // GB10's CPU and GPU share one coherent memory, so pinned host buffers are
  4. // GPU buffers at full speed and the ConnectX can DMA into them without
  5. // GPUDirect. Per all-reduce:
  6. // 1. the GPU writes its partial into send[seq & 1] and publishes seq;
  7. // 2. a CPU proxy sees seq, RDMA-writes the partial into every peer's
  8. // recv[seq & 1][rank], then writes seq into the peer's flag[rank]
  9. // (same QP, so the data is placed first);
  10. // 3. the GPU waits for every peer's flag to reach seq and sums.
  11. // Two buffers by parity are enough: a rank reaches seq + 2 only after every
  12. // peer consumed seq + 1, which needed this rank's seq + 1 flag, which the
  13. // proxy posts after this rank's seq data was read.
  14. //
  15. // The proxy posts every seq in order. The GPU can publish seq + 1 before the
  16. // proxy has seen seq (finishing seq needs only the peers' data), and skipping
  17. // seq would let peers accept seq + 1's flag over stale seq data.
  18. //
  19. // Every partial is split in half, one half per ConnectX root, each followed by
  20. // its own flag on that root's QP: flag[src * 2 + root]. Rank r sends to
  21. // r+1, r+2, ... in turn, so no receiver takes every sender at once.
  22. //
  23. // vLLM form: prepare() returns this rank's connection details, the caller
  24. // all-gathers them over its own group, connect() wires the QPs and starts the
  25. // proxy, then allreduce(in, out) runs on the current stream and is
  26. // CUDA-graph capturable. in/out are bf16 device tensors.
  27. #include <arpa/inet.h>
  28. #include <cuda_bf16.h>
  29. #include <cuda_runtime.h>
  30. #include <infiniband/verbs.h>
  31. #include <netinet/in.h>
  32. #include <netinet/tcp.h>
  33. #include <sys/socket.h>
  34. #include <unistd.h>
  35. #include <atomic>
  36. #include <chrono>
  37. #include <cstdint>
  38. #include <cstdio>
  39. #include <cstdlib>
  40. #include <cstring>
  41. #include <thread>
  42. #include <vector>
  43. #include <ATen/cuda/CUDAContext.h>
  44. #include <torch/extension.h>
  45. #define CK(x) do { cudaError_t e_ = (x); if (e_ != cudaSuccess) { fprintf(stderr, "%s:%d %s\n", __FILE__, __LINE__, cudaGetErrorString(e_)); exit(1); } } while (0)
  46. #define IBCK(x) do { if (!(x)) { fprintf(stderr, "%s:%d %s failed: %s\n", __FILE__, __LINE__, #x, strerror(errno)); exit(1); } } while (0)
  47. constexpr int kMaxWorld = 8;
  48. constexpr size_t kMaxBytes = 512 << 10; // one partial: bf16 [32, 4096] is 256 KB
  49. struct Ctl { // pinned; written by the GPU, read by the proxy
  50. volatile uint64_t seq; // last published partial
  51. volatile uint64_t bytes[2]; // size of the partial in send[parity]
  52. volatile uint64_t stop;
  53. volatile uint64_t done; // last seq the GPU finished summing (for the watchdog)
  54. };
  55. struct PeerInfo { // what rank r tells everyone about its QP towards peer j
  56. uint32_t qpn[kMaxWorld][2]; // towards peer j, per root
  57. uint8_t gid[2][16]; // per ConnectX root
  58. uint64_t recv_addr, flag_addr;
  59. uint32_t recv_rkey[2], flag_rkey[2];
  60. };
  61. // ---- GPU side ---------------------------------------------------------------
  62. __device__ __forceinline__ uint64_t ld_acquire_sys(const volatile uint64_t* p) {
  63. uint64_t v;
  64. asm volatile("ld.acquire.sys.global.u64 %0, [%1];" : "=l"(v) : "l"(p) : "memory");
  65. return v;
  66. }
  67. __device__ __forceinline__ uint4 ld_cv(const void* p) { // bypass caches: the NIC wrote it
  68. uint4 v;
  69. asm volatile("ld.global.cv.v4.u32 {%0,%1,%2,%3}, [%4];" : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) : "l"(p));
  70. return v;
  71. }
  72. struct Dev {
  73. __nv_bfloat16* send; // [2][kMaxBytes / 2]
  74. const __nv_bfloat16* recv; // [2][world][kMaxBytes / 2]
  75. const volatile uint64_t* flag; // [2 * world]
  76. Ctl* ctl;
  77. unsigned* seq_dev; // device: last completed seq
  78. unsigned* blocks; // device [2]: blocks past the copy, blocks finished
  79. unsigned* go; // device: seq whose peer data has all arrived
  80. int rank, world;
  81. };
  82. // out = sum over ranks of in. in and out may alias.
  83. __global__ void arx_kernel(Dev d, const __nv_bfloat16* __restrict__ in, __nv_bfloat16* out, int n) {
  84. const uint64_t seq = *d.seq_dev + 1;
  85. const int par = seq & 1;
  86. __nv_bfloat16* mine = d.send + (size_t)par * (kMaxBytes / 2);
  87. const int stride = gridDim.x * blockDim.x * 8;
  88. for (int i = (blockIdx.x * blockDim.x + threadIdx.x) * 8; i < n; i += stride)
  89. *reinterpret_cast<uint4*>(mine + i) = *reinterpret_cast<const uint4*>(in + i);
  90. __threadfence_system();
  91. __syncthreads();
  92. // Separate counters per phase: blocks that start late would otherwise add
  93. // their first count after early blocks added their second, and the
  94. // publish would be skipped or would go out before every block copied.
  95. if (threadIdx.x == 0 && atomicAdd(&d.blocks[0], 1) == gridDim.x - 1) { // last block publishes
  96. d.blocks[0] = 0;
  97. d.ctl->bytes[par] = (uint64_t)n * 2;
  98. __threadfence_system();
  99. d.ctl->seq = seq;
  100. }
  101. // Block 0 polls the pinned flags the NIC writes; the others wait on a word in
  102. // device memory, so only a few threads touch lines the NIC is filling.
  103. if (blockIdx.x == 0) {
  104. if (threadIdx.x < 2 * d.world && threadIdx.x / 2 != d.rank)
  105. while (ld_acquire_sys(d.flag + threadIdx.x) < seq) {}
  106. __syncthreads();
  107. if (threadIdx.x == 0) { __threadfence(); atomicExch(d.go, (unsigned)seq); }
  108. } else {
  109. if (threadIdx.x == 0)
  110. while (true) {
  111. unsigned g;
  112. asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(g) : "l"(d.go) : "memory");
  113. if (g >= (unsigned)seq) break;
  114. }
  115. __syncthreads();
  116. }
  117. for (int i = (blockIdx.x * blockDim.x + threadIdx.x) * 8; i < n; i += stride) {
  118. uint4 v[kMaxWorld];
  119. #pragma unroll
  120. for (int src = 0; src < kMaxWorld; ++src)
  121. if (src < d.world)
  122. v[src] = src == d.rank ? *reinterpret_cast<const uint4*>(mine + i)
  123. : ld_cv(d.recv + ((size_t)par * d.world + src) * (kMaxBytes / 2) + i);
  124. float acc[8] = {};
  125. #pragma unroll
  126. for (int src = 0; src < kMaxWorld; ++src) // rank order: every rank gets the same bits
  127. if (src < d.world) {
  128. const __nv_bfloat16* b = reinterpret_cast<const __nv_bfloat16*>(&v[src]);
  129. #pragma unroll
  130. for (int k = 0; k < 8; ++k) acc[k] += __bfloat162float(b[k]);
  131. }
  132. __align__(16) __nv_bfloat16 o[8];
  133. #pragma unroll
  134. for (int k = 0; k < 8; ++k) o[k] = __float2bfloat16(acc[k]);
  135. *reinterpret_cast<uint4*>(out + i) = *reinterpret_cast<const uint4*>(o);
  136. }
  137. __syncthreads();
  138. if (threadIdx.x == 0 && atomicAdd(&d.blocks[1], 1) == gridDim.x - 1) { // last block out
  139. d.blocks[1] = 0;
  140. *d.seq_dev = (unsigned)seq;
  141. d.ctl->done = seq;
  142. }
  143. }
  144. // ---- host side ---------------------------------------------------------------
  145. namespace {
  146. struct State {
  147. int rank = -1, world = 0, gid_idx = 0;
  148. uint8_t *send_h = nullptr, *recv_h = nullptr;
  149. uint64_t* flag_h = nullptr;
  150. Ctl* ctl = nullptr;
  151. ibv_context* ctx[2] = {};
  152. ibv_pd* pd[2] = {};
  153. ibv_mr *send_mr[2] = {}, *recv_mr[2] = {}, *flag_mr[2] = {};
  154. ibv_cq* cq[2] = {};
  155. ibv_qp* qp[kMaxWorld][2] = {};
  156. PeerInfo mine{};
  157. std::vector<PeerInfo> all;
  158. Dev dev{};
  159. bool connected = false;
  160. };
  161. State S;
  162. std::atomic<bool> g_proxy_err{false};
  163. void proxy_loop() {
  164. uint64_t seq = 0, posted[kMaxWorld][2] = {};
  165. const int rank = S.rank, world = S.world;
  166. auto last_move = std::chrono::steady_clock::now();
  167. uint64_t last_done = 0;
  168. bool reported = false;
  169. while (!S.ctl->stop) {
  170. if (S.ctl->seq <= seq) {
  171. // Watchdog: published work that has not completed for 2 s is a stall,
  172. // and so is a peer whose flags are ahead of what this rank posted.
  173. // Print what this rank sent and what it has heard from each peer.
  174. const uint64_t done = S.ctl->done;
  175. const auto now = std::chrono::steady_clock::now();
  176. bool behind = false;
  177. for (int j = 0; j < world; ++j)
  178. if (j != rank) behind |= S.flag_h[j * 2] > seq;
  179. if (done != last_done || (done >= seq && !behind)) {
  180. last_done = done; last_move = now; reported = false;
  181. }
  182. else if (!reported && now - last_move > std::chrono::seconds(2)) {
  183. reported = true;
  184. char buf[512];
  185. int o = snprintf(buf, sizeof buf, "arx rank %d STALL: published %lu posted %lu done %lu; flags from peers:",
  186. rank, (unsigned long)S.ctl->seq, (unsigned long)seq, (unsigned long)done);
  187. for (int j = 0; j < world; ++j)
  188. if (j != rank)
  189. o += snprintf(buf + o, sizeof buf - o, " r%d=%lu/%lu", j, (unsigned long)S.flag_h[j * 2],
  190. (unsigned long)S.flag_h[j * 2 + 1]);
  191. fprintf(stderr, "%s\n", buf);
  192. }
  193. continue;
  194. }
  195. ++seq; // every seq, in order
  196. std::atomic_thread_fence(std::memory_order_acquire);
  197. const int par = seq & 1;
  198. const uint32_t bytes = (uint32_t)S.ctl->bytes[par];
  199. const uint32_t half = (bytes / 2 + 15) & ~15u; // root 0 takes the first half
  200. for (int step = 1; step < world; ++step) {
  201. const int j = (rank + step) % world;
  202. for (int r = 0; r < 2; ++r) {
  203. const uint32_t off = r ? half : 0, len = r ? bytes - half : half;
  204. ibv_sge sg{(uint64_t)(S.send_h + par * kMaxBytes + off), len, S.send_mr[r]->lkey};
  205. ibv_send_wr w{}, f{}, *bad;
  206. w.opcode = IBV_WR_RDMA_WRITE; w.sg_list = &sg; w.num_sge = 1;
  207. w.wr.rdma.remote_addr = S.all[j].recv_addr + ((uint64_t)par * world + rank) * kMaxBytes + off;
  208. w.wr.rdma.rkey = S.all[j].recv_rkey[r];
  209. uint64_t sv = seq;
  210. ibv_sge fs{(uint64_t)&sv, 8, 0};
  211. f.opcode = IBV_WR_RDMA_WRITE; f.sg_list = &fs; f.num_sge = 1; f.send_flags = IBV_SEND_INLINE;
  212. f.wr.rdma.remote_addr = S.all[j].flag_addr + (rank * 2 + r) * 8; f.wr.rdma.rkey = S.all[j].flag_rkey[r];
  213. // Unsignalled WRs are reclaimed only by a later signalled one on the
  214. // same QP, so the count is per QP.
  215. if ((++posted[j][r] & 31) == 0) f.send_flags |= IBV_SEND_SIGNALED;
  216. w.next = &f;
  217. if (int e = ibv_post_send(S.qp[j][r], &w, &bad)) {
  218. fprintf(stderr, "arx rank %d: post to %d root %d failed: %s\n", rank, j, r, strerror(e));
  219. g_proxy_err = true;
  220. return;
  221. }
  222. }
  223. }
  224. for (int r = 0; r < 2; ++r) {
  225. ibv_wc wc[16];
  226. const int n = ibv_poll_cq(S.cq[r], 16, wc);
  227. for (int i = 0; i < n; ++i)
  228. if (wc[i].status != IBV_WC_SUCCESS) {
  229. fprintf(stderr, "arx rank %d: completion error %d\n", rank, wc[i].status);
  230. g_proxy_err = true;
  231. return;
  232. }
  233. }
  234. }
  235. }
  236. } // namespace
  237. // Allocates buffers, opens both roots, creates the QPs; returns this rank's
  238. // PeerInfo for the caller to all-gather.
  239. py::bytes arx_prepare(int64_t rank, int64_t world, std::string dev0, std::string dev1, int64_t gid_idx) {
  240. TORCH_CHECK(S.rank < 0, "arx is already prepared in this process");
  241. TORCH_CHECK(world >= 2 && world <= kMaxWorld);
  242. S.rank = rank; S.world = world; S.gid_idx = gid_idx;
  243. CK(cudaHostAlloc(&S.send_h, 2 * kMaxBytes, cudaHostAllocMapped));
  244. CK(cudaHostAlloc(&S.recv_h, 2 * world * kMaxBytes, cudaHostAllocMapped));
  245. CK(cudaHostAlloc(&S.flag_h, 2 * kMaxWorld * sizeof(uint64_t), cudaHostAllocMapped));
  246. CK(cudaHostAlloc(&S.ctl, sizeof(Ctl), cudaHostAllocMapped));
  247. memset(S.flag_h, 0, 2 * kMaxWorld * sizeof(uint64_t));
  248. memset((void*)S.ctl, 0, sizeof(Ctl));
  249. const char* names[2] = {dev0.c_str(), dev1.c_str()};
  250. int ndev;
  251. ibv_device** list = ibv_get_device_list(&ndev);
  252. const int acc = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE;
  253. for (int r = 0; r < 2; ++r) {
  254. for (int i = 0; i < ndev; ++i)
  255. if (!strcmp(ibv_get_device_name(list[i]), names[r])) S.ctx[r] = ibv_open_device(list[i]);
  256. TORCH_CHECK(S.ctx[r], "arx: no RDMA device ", names[r]);
  257. S.pd[r] = ibv_alloc_pd(S.ctx[r]); IBCK(S.pd[r]);
  258. S.send_mr[r] = ibv_reg_mr(S.pd[r], S.send_h, 2 * kMaxBytes, acc); IBCK(S.send_mr[r]);
  259. S.recv_mr[r] = ibv_reg_mr(S.pd[r], S.recv_h, 2 * world * kMaxBytes, acc); IBCK(S.recv_mr[r]);
  260. S.flag_mr[r] = ibv_reg_mr(S.pd[r], S.flag_h, 2 * kMaxWorld * sizeof(uint64_t), acc); IBCK(S.flag_mr[r]);
  261. S.cq[r] = ibv_create_cq(S.ctx[r], 4096, nullptr, nullptr, 0); IBCK(S.cq[r]);
  262. ibv_gid gid;
  263. IBCK(ibv_query_gid(S.ctx[r], 1, gid_idx, &gid) == 0);
  264. memcpy(S.mine.gid[r], gid.raw, 16);
  265. S.mine.recv_rkey[r] = S.recv_mr[r]->rkey;
  266. S.mine.flag_rkey[r] = S.flag_mr[r]->rkey;
  267. }
  268. ibv_free_device_list(list);
  269. for (int j = 0; j < world; ++j)
  270. for (int r = 0; r < 2; ++r) {
  271. if (j == rank) continue;
  272. ibv_qp_init_attr ia{};
  273. ia.send_cq = S.cq[r]; ia.recv_cq = S.cq[r]; ia.qp_type = IBV_QPT_RC;
  274. ia.cap.max_send_wr = 1024; ia.cap.max_recv_wr = 1; ia.cap.max_send_sge = 1; ia.cap.max_recv_sge = 1;
  275. ia.cap.max_inline_data = 64;
  276. S.qp[j][r] = ibv_create_qp(S.pd[r], &ia); IBCK(S.qp[j][r]);
  277. ibv_qp_attr a{};
  278. a.qp_state = IBV_QPS_INIT; a.pkey_index = 0; a.port_num = 1; a.qp_access_flags = acc;
  279. IBCK(ibv_modify_qp(S.qp[j][r], &a, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS) == 0);
  280. S.mine.qpn[j][r] = S.qp[j][r]->qp_num;
  281. }
  282. S.mine.recv_addr = (uint64_t)S.recv_h;
  283. S.mine.flag_addr = (uint64_t)S.flag_h;
  284. return py::bytes(reinterpret_cast<const char*>(&S.mine), sizeof(PeerInfo));
  285. }
  286. // Takes every rank's PeerInfo in rank order, brings the QPs up, starts the
  287. // proxy. The caller must barrier its group before the first allreduce.
  288. void arx_connect(std::vector<std::string> infos) {
  289. TORCH_CHECK(S.rank >= 0 && !S.connected && (int)infos.size() == S.world);
  290. S.all.resize(S.world);
  291. for (int j = 0; j < S.world; ++j) {
  292. TORCH_CHECK(infos[j].size() == sizeof(PeerInfo), "arx: PeerInfo size mismatch");
  293. memcpy(&S.all[j], infos[j].data(), sizeof(PeerInfo));
  294. }
  295. for (int j = 0; j < S.world; ++j)
  296. for (int r = 0; r < 2; ++r) {
  297. if (j == S.rank) continue;
  298. ibv_qp_attr a{};
  299. a.qp_state = IBV_QPS_RTR; a.path_mtu = IBV_MTU_4096; a.dest_qp_num = S.all[j].qpn[S.rank][r]; a.rq_psn = 0;
  300. a.max_dest_rd_atomic = 1; a.min_rnr_timer = 12;
  301. a.ah_attr.is_global = 1; a.ah_attr.port_num = 1; a.ah_attr.grh.hop_limit = 1;
  302. a.ah_attr.grh.sgid_index = S.gid_idx;
  303. memcpy(a.ah_attr.grh.dgid.raw, S.all[j].gid[r], 16);
  304. IBCK(ibv_modify_qp(S.qp[j][r], &a, IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN |
  305. IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER) == 0);
  306. a = {};
  307. a.qp_state = IBV_QPS_RTS; a.timeout = 14; a.retry_cnt = 7; a.rnr_retry = 7; a.sq_psn = 0; a.max_rd_atomic = 1;
  308. IBCK(ibv_modify_qp(S.qp[j][r], &a, IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY |
  309. IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC) == 0);
  310. }
  311. Dev& d = S.dev;
  312. d.send = (__nv_bfloat16*)S.send_h; d.recv = (const __nv_bfloat16*)S.recv_h; d.flag = S.flag_h; d.ctl = S.ctl;
  313. d.rank = S.rank; d.world = S.world;
  314. CK(cudaMalloc(&d.seq_dev, 4)); CK(cudaMalloc(&d.blocks, 8)); CK(cudaMalloc(&d.go, 4));
  315. CK(cudaMemset(d.seq_dev, 0, 4)); CK(cudaMemset(d.blocks, 0, 8)); CK(cudaMemset(d.go, 0, 4));
  316. CK(cudaDeviceSynchronize());
  317. std::thread(proxy_loop).detach();
  318. S.connected = true;
  319. }
  320. void arx_allreduce(torch::Tensor in, torch::Tensor out) {
  321. TORCH_CHECK(S.connected, "arx is not connected");
  322. TORCH_CHECK(!g_proxy_err, "arx proxy failed");
  323. TORCH_CHECK(in.scalar_type() == at::kBFloat16 && out.scalar_type() == at::kBFloat16 && in.is_contiguous() &&
  324. out.is_contiguous() && in.numel() == out.numel());
  325. const int n = in.numel();
  326. TORCH_CHECK(n % 8 == 0 && (size_t)n * 2 <= kMaxBytes);
  327. const int threads = 256, blocks = std::max(1, std::min(8, n / (threads * 8)));
  328. arx_kernel<<<blocks, threads, 0, at::cuda::getCurrentCUDAStream()>>>(
  329. S.dev, (const __nv_bfloat16*)in.data_ptr(), (__nv_bfloat16*)out.data_ptr(), n);
  330. }
  331. int64_t arx_max_bytes() { return kMaxBytes; }
  332. PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
  333. m.def("prepare", &arx_prepare);
  334. m.def("connect", &arx_connect);
  335. m.def("allreduce", &arx_allreduce);
  336. m.def("max_bytes", &arx_max_bytes);
  337. }