arx_vllm.cu 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  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. // Ring mode is for boxes cabled in a ring with no switch. The ConnectX cannot
  24. // forward RoCE for other boxes, so QPs go only to prev = r-1 and next = r+1,
  25. // each over the two functions of the port facing that neighbour. A rank sends
  26. // its partial to next, and at world 3 and 4 to prev as well. At world 4 the
  27. // proxy forwards the partial from prev on to next. The relayed data lands in
  28. // the same recv[seq & 1][src] and flag[src * 2 + root] as a direct write, so
  29. // the GPU side is unchanged. At world 3 prev and next are the two other ranks,
  30. // so every partial goes direct and nothing is relayed. World 2 sends to next
  31. // only.
  32. //
  33. // The relay adds one reuse hazard. Rank m reads recv[par][m-1] to forward it
  34. // to m+1, and m-1 writes seq + 2 there next. m-1 publishes seq + 2 after
  35. // finishing seq + 1. Finishing needs m+1's seq + 1 partial. m+1 publishes that
  36. // after finishing seq, and finishing seq needed the relayed data. The relay
  37. // takes the partial's size from this rank's own publish of the same seq. The
  38. // same chain keeps this rank from publishing seq + 2 before the relay.
  39. //
  40. // Two-shot (mesh only), for partials too big to send whole to every peer:
  41. // allreduce2() runs two collectives in a row, each its own seq. Reduce-scatter:
  42. // the proxy writes chunk j of this rank's partial into rank j's recv slot, and
  43. // the GPU sums its own chunk from every rank (fp32, rank order, one rounding,
  44. // so the bits match the one-shot sum). All-gather: the proxy writes the summed
  45. // chunk into every peer's slot, and the GPU assembles the chunks in rank order.
  46. // Each rank sends 2 (world - 1) / world partials' worth instead of world - 1.
  47. // Ctl::mode tells the proxy which pattern a seq uses.
  48. //
  49. // vLLM form: prepare() returns this rank's connection details, the caller
  50. // all-gathers them over its own group, connect() wires the QPs and starts the
  51. // proxy, then allreduce(in, out) runs on the current stream and is
  52. // CUDA-graph capturable. in/out are bf16 device tensors.
  53. #include <arpa/inet.h>
  54. #include <cuda_bf16.h>
  55. #include <cuda_runtime.h>
  56. #include <infiniband/verbs.h>
  57. #include <netinet/in.h>
  58. #include <netinet/tcp.h>
  59. #include <sys/socket.h>
  60. #include <unistd.h>
  61. #include <algorithm>
  62. #include <atomic>
  63. #include <chrono>
  64. #include <cstdint>
  65. #include <cstdio>
  66. #include <cstdlib>
  67. #include <cstring>
  68. #include <thread>
  69. #include <vector>
  70. #include <ATen/cuda/CUDAContext.h>
  71. #include <torch/extension.h>
  72. #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)
  73. #define IBCK(x) do { if (!(x)) { fprintf(stderr, "%s:%d %s failed: %s\n", __FILE__, __LINE__, #x, strerror(errno)); exit(1); } } while (0)
  74. constexpr int kMaxWorld = 8;
  75. constexpr int kMaxDev = 4; // ring mode: 2 ports x 2 roots
  76. constexpr size_t kMaxBytes = 512 << 10; // one-shot partial: bf16 [32, 4096] is 256 KB
  77. constexpr size_t kSendBytes = 4 << 20; // per parity: a two-shot partial
  78. constexpr size_t kSlotBytes = 1 << 20; // per peer per parity: a one-shot partial or a two-shot chunk
  79. enum : uint64_t { kFull = 0, kScatter = 1, kGather = 2 };
  80. struct Ctl { // pinned; written by the GPU, read by the proxy
  81. volatile uint64_t seq; // last published partial
  82. volatile uint64_t bytes[2]; // size of the partial in send[parity]
  83. volatile uint64_t mode[2]; // kFull, kScatter or kGather for send[parity]
  84. volatile uint64_t stop;
  85. volatile uint64_t done; // last seq the GPU finished summing (for the watchdog)
  86. };
  87. struct PeerInfo { // what rank r tells everyone about its QPs
  88. uint32_t qpn[kMaxWorld][2]; // per channel (see State), per root
  89. uint8_t gid[kMaxDev][16]; // per RDMA device
  90. uint32_t mtu[kMaxDev]; // per device: the port's active MTU (enum ibv_mtu)
  91. uint64_t recv_addr, flag_addr;
  92. uint32_t recv_rkey[kMaxDev], flag_rkey[kMaxDev];
  93. };
  94. // ---- GPU side ---------------------------------------------------------------
  95. __device__ __forceinline__ uint64_t ld_acquire_sys(const volatile uint64_t* p) {
  96. uint64_t v;
  97. asm volatile("ld.acquire.sys.global.u64 %0, [%1];" : "=l"(v) : "l"(p) : "memory");
  98. return v;
  99. }
  100. __device__ __forceinline__ uint4 ld_cv(const void* p) { // bypass caches: the NIC wrote it
  101. uint4 v;
  102. 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));
  103. return v;
  104. }
  105. struct Dev {
  106. __nv_bfloat16* send; // [2][kSendBytes / 2]
  107. const __nv_bfloat16* recv; // [2][world][kSlotBytes / 2]
  108. const volatile uint64_t* flag; // [2 * world]
  109. Ctl* ctl;
  110. unsigned* seq_dev; // device: last completed seq
  111. unsigned* blocks; // device [2]: blocks past the copy, blocks finished
  112. unsigned* go; // device: seq whose peer data has all arrived
  113. int rank, world;
  114. };
  115. // Weight ranges the next kernels read; the kernel asks L2 for them before it waits.
  116. constexpr int kMaxPrefetch = 4;
  117. struct Prefetch {
  118. const uint8_t* ptr[kMaxPrefetch];
  119. long long bytes[kMaxPrefetch];
  120. int count;
  121. };
  122. // out = sum over ranks of in. in and out may alias.
  123. __global__ void arx_kernel(Dev d, const __nv_bfloat16* __restrict__ in, __nv_bfloat16* out, int n, Prefetch pf) {
  124. const uint64_t seq = *d.seq_dev + 1;
  125. const int par = seq & 1;
  126. __nv_bfloat16* mine = d.send + (size_t)par * (kSendBytes / 2);
  127. const int stride = gridDim.x * blockDim.x * 8;
  128. for (int i = (blockIdx.x * blockDim.x + threadIdx.x) * 8; i < n; i += stride)
  129. *reinterpret_cast<uint4*>(mine + i) = *reinterpret_cast<const uint4*>(in + i);
  130. __threadfence_system();
  131. __syncthreads();
  132. // Separate counters per phase: blocks that start late would otherwise add
  133. // their first count after early blocks added their second, and the
  134. // publish would be skipped or would go out before every block copied.
  135. if (threadIdx.x == 0 && atomicAdd(&d.blocks[0], 1) == gridDim.x - 1) { // last block publishes
  136. d.blocks[0] = 0;
  137. d.ctl->mode[par] = kFull;
  138. d.ctl->bytes[par] = (uint64_t)n * 2;
  139. __threadfence_system();
  140. d.ctl->seq = seq;
  141. }
  142. // DRAM is idle while the peers' data is in flight: spread one bulk L2
  143. // prefetch per thread over each range.
  144. if (pf.count > 0) {
  145. const long long nthr = (long long)gridDim.x * blockDim.x, t = (long long)blockIdx.x * blockDim.x + threadIdx.x;
  146. for (int r = 0; r < pf.count; ++r) {
  147. const long long chunk = ((pf.bytes[r] + nthr - 1) / nthr + 15) & ~15LL, off = t * chunk;
  148. const long long len = min(chunk, pf.bytes[r] - off) & ~15LL;
  149. if (len > 0)
  150. asm volatile("cp.async.bulk.prefetch.L2.global [%0], %1;\n" ::"l"(pf.ptr[r] + off), "r"((unsigned)len));
  151. }
  152. }
  153. // Block 0 polls the pinned flags the NIC writes; the others wait on a word in
  154. // device memory, so only a few threads touch lines the NIC is filling.
  155. if (blockIdx.x == 0) {
  156. if (threadIdx.x < 2 * d.world && threadIdx.x / 2 != d.rank)
  157. while (ld_acquire_sys(d.flag + threadIdx.x) < seq) {}
  158. __syncthreads();
  159. if (threadIdx.x == 0) { __threadfence(); atomicExch(d.go, (unsigned)seq); }
  160. } else {
  161. if (threadIdx.x == 0)
  162. while (true) {
  163. unsigned g;
  164. asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(g) : "l"(d.go) : "memory");
  165. if (g >= (unsigned)seq) break;
  166. }
  167. __syncthreads();
  168. }
  169. for (int i = (blockIdx.x * blockDim.x + threadIdx.x) * 8; i < n; i += stride) {
  170. uint4 v[kMaxWorld];
  171. #pragma unroll
  172. for (int src = 0; src < kMaxWorld; ++src)
  173. if (src < d.world)
  174. v[src] = src == d.rank ? *reinterpret_cast<const uint4*>(mine + i)
  175. : ld_cv(d.recv + ((size_t)par * d.world + src) * (kSlotBytes / 2) + i);
  176. float acc[8] = {};
  177. #pragma unroll
  178. for (int src = 0; src < kMaxWorld; ++src) // rank order: every rank gets the same bits
  179. if (src < d.world) {
  180. const __nv_bfloat16* b = reinterpret_cast<const __nv_bfloat16*>(&v[src]);
  181. #pragma unroll
  182. for (int k = 0; k < 8; ++k) acc[k] += __bfloat162float(b[k]);
  183. }
  184. __align__(16) __nv_bfloat16 o[8];
  185. #pragma unroll
  186. for (int k = 0; k < 8; ++k) o[k] = __float2bfloat16(acc[k]);
  187. *reinterpret_cast<uint4*>(out + i) = *reinterpret_cast<const uint4*>(o);
  188. }
  189. __syncthreads();
  190. if (threadIdx.x == 0 && atomicAdd(&d.blocks[1], 1) == gridDim.x - 1) { // last block out
  191. d.blocks[1] = 0;
  192. *d.seq_dev = (unsigned)seq;
  193. d.ctl->done = seq;
  194. }
  195. }
  196. // The two-shot kernels: copy n bf16 into send[par], publish them as `mode`,
  197. // wait for every peer's flag, then leave the rest to the caller.
  198. __device__ __forceinline__ uint64_t publish_and_wait(Dev d, const __nv_bfloat16* __restrict__ in, int n,
  199. uint64_t mode) {
  200. const uint64_t seq = *d.seq_dev + 1;
  201. const int par = seq & 1;
  202. __nv_bfloat16* mine = d.send + (size_t)par * (kSendBytes / 2);
  203. const int stride = gridDim.x * blockDim.x * 8;
  204. for (int i = (blockIdx.x * blockDim.x + threadIdx.x) * 8; i < n; i += stride)
  205. *reinterpret_cast<uint4*>(mine + i) = *reinterpret_cast<const uint4*>(in + i);
  206. __threadfence_system();
  207. __syncthreads();
  208. if (threadIdx.x == 0 && atomicAdd(&d.blocks[0], 1) == gridDim.x - 1) {
  209. d.blocks[0] = 0;
  210. d.ctl->mode[par] = mode;
  211. d.ctl->bytes[par] = (uint64_t)n * 2;
  212. __threadfence_system();
  213. d.ctl->seq = seq;
  214. }
  215. if (blockIdx.x == 0) {
  216. if (threadIdx.x < 2 * d.world && threadIdx.x / 2 != d.rank)
  217. while (ld_acquire_sys(d.flag + threadIdx.x) < seq) {}
  218. __syncthreads();
  219. if (threadIdx.x == 0) { __threadfence(); atomicExch(d.go, (unsigned)seq); }
  220. } else {
  221. if (threadIdx.x == 0)
  222. while (true) {
  223. unsigned g;
  224. asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(g) : "l"(d.go) : "memory");
  225. if (g >= (unsigned)seq) break;
  226. }
  227. __syncthreads();
  228. }
  229. return seq;
  230. }
  231. __device__ __forceinline__ void finish(Dev d, uint64_t seq) {
  232. __syncthreads();
  233. if (threadIdx.x == 0 && atomicAdd(&d.blocks[1], 1) == gridDim.x - 1) {
  234. d.blocks[1] = 0;
  235. *d.seq_dev = (unsigned)seq;
  236. d.ctl->done = seq;
  237. }
  238. }
  239. // out[chunk] = this rank's chunk of the sum of every rank's in[n].
  240. __global__ void arx_scatter_kernel(Dev d, const __nv_bfloat16* __restrict__ in, __nv_bfloat16* out, int n) {
  241. const uint64_t seq = publish_and_wait(d, in, n, kScatter);
  242. const int par = seq & 1, chunk = n / d.world;
  243. const __nv_bfloat16* mine = d.send + (size_t)par * (kSendBytes / 2) + (size_t)d.rank * chunk;
  244. const int stride = gridDim.x * blockDim.x * 8;
  245. for (int i = (blockIdx.x * blockDim.x + threadIdx.x) * 8; i < chunk; i += stride) {
  246. uint4 v[kMaxWorld];
  247. #pragma unroll
  248. for (int src = 0; src < kMaxWorld; ++src)
  249. if (src < d.world)
  250. v[src] = src == d.rank ? *reinterpret_cast<const uint4*>(mine + i)
  251. : ld_cv(d.recv + ((size_t)par * d.world + src) * (kSlotBytes / 2) + i);
  252. float acc[8] = {};
  253. #pragma unroll
  254. for (int src = 0; src < kMaxWorld; ++src) // rank order, as the one-shot sum
  255. if (src < d.world) {
  256. const __nv_bfloat16* b = reinterpret_cast<const __nv_bfloat16*>(&v[src]);
  257. #pragma unroll
  258. for (int k = 0; k < 8; ++k) acc[k] += __bfloat162float(b[k]);
  259. }
  260. __align__(16) __nv_bfloat16 o[8];
  261. #pragma unroll
  262. for (int k = 0; k < 8; ++k) o[k] = __float2bfloat16(acc[k]);
  263. *reinterpret_cast<uint4*>(out + i) = *reinterpret_cast<const uint4*>(o);
  264. }
  265. finish(d, seq);
  266. }
  267. // out[world * chunk] = every rank's chunk, in rank order.
  268. __global__ void arx_gather_kernel(Dev d, const __nv_bfloat16* __restrict__ chunk_in, __nv_bfloat16* out, int chunk) {
  269. const uint64_t seq = publish_and_wait(d, chunk_in, chunk, kGather);
  270. const int par = seq & 1, n = chunk * d.world;
  271. const int stride = gridDim.x * blockDim.x * 8;
  272. for (int i = (blockIdx.x * blockDim.x + threadIdx.x) * 8; i < n; i += stride) {
  273. const int src = i / chunk, j = i - src * chunk;
  274. *reinterpret_cast<uint4*>(out + i) =
  275. src == d.rank ? *reinterpret_cast<const uint4*>(chunk_in + j)
  276. : ld_cv(d.recv + ((size_t)par * d.world + src) * (kSlotBytes / 2) + j);
  277. }
  278. finish(d, seq);
  279. }
  280. // ---- host side ---------------------------------------------------------------
  281. namespace {
  282. // A channel is one QP per root to one peer. Mesh: channel j goes to rank j
  283. // over device r. Ring: channel 0 goes to prev over devices 0 and 1, channel 1
  284. // to next over devices 2 and 3, and meets the peer's channel on the other side.
  285. struct State {
  286. int rank = -1, world = 0, ndev = 0;
  287. bool ring = false;
  288. int prev = 0, next = 0;
  289. int gid_idx[kMaxDev] = {};
  290. uint8_t *send_h = nullptr, *recv_h = nullptr;
  291. uint64_t* flag_h = nullptr;
  292. Ctl* ctl = nullptr;
  293. ibv_context* ctx[kMaxDev] = {};
  294. ibv_pd* pd[kMaxDev] = {};
  295. ibv_mr *send_mr[kMaxDev] = {}, *recv_mr[kMaxDev] = {}, *flag_mr[kMaxDev] = {};
  296. ibv_cq* cq[kMaxDev] = {};
  297. ibv_qp* qp[kMaxWorld][2] = {};
  298. PeerInfo mine{};
  299. std::vector<PeerInfo> all;
  300. Dev dev{};
  301. bool connected = false;
  302. };
  303. State S;
  304. std::atomic<bool> g_proxy_err{false};
  305. // Idle backoff (#55): keep the exact busy spin while there is work and for ARX_IDLE_SPIN_MS after it (default
  306. // 200 ms), then nap ARX_IDLE_NAP_US (default 200 us) per loop turn. ARX_IDLE_NAP_US=0 keeps the old spin.
  307. struct IdleNap {
  308. std::chrono::steady_clock::duration spin;
  309. std::chrono::microseconds nap;
  310. std::chrono::steady_clock::time_point last;
  311. IdleNap() {
  312. const char* s = std::getenv("ARX_IDLE_SPIN_MS");
  313. const char* n = std::getenv("ARX_IDLE_NAP_US");
  314. spin = std::chrono::milliseconds(s ? std::atol(s) : 200);
  315. nap = std::chrono::microseconds(n ? std::atol(n) : 200);
  316. last = std::chrono::steady_clock::now();
  317. fprintf(stderr, "arx idle nap: spin %ld ms, nap %ld us%s\n", (long)(s ? std::atol(s) : 200), (long)nap.count(),
  318. nap.count() <= 0 ? " (disabled = old spin)" : "");
  319. }
  320. void busy() { last = std::chrono::steady_clock::now(); }
  321. void idle() {
  322. if (nap.count() <= 0) return;
  323. if (std::chrono::steady_clock::now() - last > spin) std::this_thread::sleep_for(nap);
  324. }
  325. };
  326. int chan_peer(int c) { return S.ring ? (c ? S.next : S.prev) : c; }
  327. int chan_dev(int c, int r) { return S.ring ? c * 2 + r : r; } // device here
  328. int chan_rdev(int c, int r) { return S.ring ? (1 - c) * 2 + r : r; } // device at the peer
  329. // One write of [laddr, laddr + len) to the peer's recv at roff, then seq into
  330. // its flag slot, on channel c's root r QP.
  331. bool post(int c, int r, uint64_t laddr, uint32_t lkey, uint32_t len, uint64_t roff, int slot, uint64_t seq,
  332. uint64_t& posted) {
  333. const PeerInfo& p = S.all[chan_peer(c)];
  334. const int rd = chan_rdev(c, r);
  335. ibv_sge sg{laddr, len, lkey};
  336. ibv_send_wr w{}, f{}, *bad;
  337. w.opcode = IBV_WR_RDMA_WRITE; w.sg_list = &sg; w.num_sge = 1;
  338. w.wr.rdma.remote_addr = p.recv_addr + roff;
  339. w.wr.rdma.rkey = p.recv_rkey[rd];
  340. ibv_sge fs{(uint64_t)&seq, 8, 0};
  341. f.opcode = IBV_WR_RDMA_WRITE; f.sg_list = &fs; f.num_sge = 1; f.send_flags = IBV_SEND_INLINE;
  342. f.wr.rdma.remote_addr = p.flag_addr + slot * 8; f.wr.rdma.rkey = p.flag_rkey[rd];
  343. // Unsignalled WRs are reclaimed only by a later signalled one on the
  344. // same QP, so the count is per QP.
  345. if ((++posted & 31) == 0) f.send_flags |= IBV_SEND_SIGNALED;
  346. w.next = &f;
  347. if (int e = ibv_post_send(S.qp[c][r], &w, &bad)) {
  348. fprintf(stderr, "arx rank %d: post to %d root %d failed: %s\n", S.rank, chan_peer(c), r, strerror(e));
  349. return false;
  350. }
  351. return true;
  352. }
  353. void proxy_loop() {
  354. uint64_t seq = 0, posted[kMaxWorld][2] = {};
  355. const int rank = S.rank, world = S.world;
  356. auto last_move = std::chrono::steady_clock::now();
  357. uint64_t last_done = 0;
  358. bool reported = false;
  359. // Ring at world 4: the last seq relayed from prev to next, per root, and
  360. // the size of this rank's own partial for each parity.
  361. uint64_t relayed[2] = {}, own_bytes[2] = {};
  362. const bool relay = S.ring && world == 4;
  363. const volatile uint64_t* flags = S.flag_h;
  364. IdleNap nap;
  365. while (!S.ctl->stop) {
  366. for (int r = 0; relay && r < 2; ++r)
  367. while (relayed[r] < seq && flags[S.prev * 2 + r] > relayed[r]) {
  368. std::atomic_thread_fence(std::memory_order_acquire);
  369. const uint64_t s = ++relayed[r];
  370. const int par = s & 1;
  371. const uint32_t bytes = (uint32_t)own_bytes[par], half = (bytes / 2 + 15) & ~15u;
  372. const uint32_t off = r ? half : 0, len = r ? bytes - half : half;
  373. const uint64_t at = ((uint64_t)par * world + S.prev) * kSlotBytes + off;
  374. if (!post(1, r, (uint64_t)(S.recv_h + at), S.recv_mr[chan_dev(1, r)]->lkey, len, at, S.prev * 2 + r, s,
  375. posted[1][r])) {
  376. g_proxy_err = true;
  377. return;
  378. }
  379. }
  380. if (S.ctl->seq <= seq) {
  381. // Watchdog: published work that has not completed for 2 s is a stall,
  382. // and so is a peer whose flags are ahead of what this rank posted.
  383. // Print what this rank sent and what it has heard from each peer.
  384. const uint64_t done = S.ctl->done;
  385. const auto now = std::chrono::steady_clock::now();
  386. bool behind = false;
  387. for (int j = 0; j < world; ++j)
  388. if (j != rank) behind |= S.flag_h[j * 2] > seq;
  389. if (done != last_done || (done >= seq && !behind)) {
  390. last_done = done; last_move = now; reported = false;
  391. }
  392. else if (!reported && now - last_move > std::chrono::seconds(2)) {
  393. reported = true;
  394. char buf[512];
  395. int o = snprintf(buf, sizeof buf, "arx rank %d STALL: published %lu posted %lu done %lu; flags from peers:",
  396. rank, (unsigned long)S.ctl->seq, (unsigned long)seq, (unsigned long)done);
  397. for (int j = 0; j < world; ++j)
  398. if (j != rank)
  399. o += snprintf(buf + o, sizeof buf - o, " r%d=%lu/%lu", j, (unsigned long)S.flag_h[j * 2],
  400. (unsigned long)S.flag_h[j * 2 + 1]);
  401. if (relay)
  402. o += snprintf(buf + o, sizeof buf - o, "; relayed %lu/%lu", (unsigned long)relayed[0],
  403. (unsigned long)relayed[1]);
  404. fprintf(stderr, "%s\n", buf);
  405. }
  406. if (relay && (relayed[0] < seq || relayed[1] < seq)) nap.busy(); else nap.idle();
  407. continue;
  408. }
  409. nap.busy();
  410. ++seq; // every seq, in order
  411. std::atomic_thread_fence(std::memory_order_acquire);
  412. const int par = seq & 1;
  413. const uint64_t mode = S.ctl->mode[par];
  414. const uint32_t total = (uint32_t)S.ctl->bytes[par];
  415. // A scatter sends each peer its own chunk; full and gather send everything.
  416. const uint32_t bytes = mode == kScatter ? total / world : total;
  417. const uint32_t half = (bytes / 2 + 15) & ~15u; // root 0 takes the first half
  418. own_bytes[par] = bytes;
  419. // Mesh: channel j is rank j. Ring: next, then prev unless prev is next.
  420. const int nchan = S.ring ? (world == 2 ? 1 : 2) : world - 1;
  421. for (int step = 1; step <= nchan; ++step) {
  422. const int c = S.ring ? 2 - step : (rank + step) % world;
  423. const uint64_t base = mode == kScatter ? (uint64_t)chan_peer(c) * bytes : 0;
  424. for (int r = 0; r < 2; ++r) {
  425. const uint32_t off = r ? half : 0, len = r ? bytes - half : half;
  426. if (!post(c, r, (uint64_t)(S.send_h + par * kSendBytes + base + off), S.send_mr[chan_dev(c, r)]->lkey, len,
  427. ((uint64_t)par * world + rank) * kSlotBytes + off, rank * 2 + r, seq, posted[c][r])) {
  428. g_proxy_err = true;
  429. return;
  430. }
  431. }
  432. }
  433. for (int r = 0; r < S.ndev; ++r) {
  434. ibv_wc wc[16];
  435. const int n = ibv_poll_cq(S.cq[r], 16, wc);
  436. for (int i = 0; i < n; ++i)
  437. if (wc[i].status != IBV_WC_SUCCESS) {
  438. fprintf(stderr, "arx rank %d: completion error %d\n", rank, wc[i].status);
  439. g_proxy_err = true;
  440. return;
  441. }
  442. }
  443. }
  444. }
  445. } // namespace
  446. // Allocates buffers, opens the RDMA devices and creates the QPs. Returns this
  447. // rank's PeerInfo for the caller to all-gather. Mesh: devs are root 0 and root
  448. // 1. Ring: the port facing prev (root 0, root 1), then the port facing next.
  449. // gids: each device's GID index.
  450. py::bytes arx_prepare(int64_t rank, int64_t world, std::vector<std::string> devs, std::vector<int64_t> gids,
  451. bool ring) {
  452. TORCH_CHECK(S.rank < 0, "arx is already prepared in this process");
  453. TORCH_CHECK(world >= 2 && world <= kMaxWorld);
  454. TORCH_CHECK(!ring || (world >= 2 && world <= 4), "arx ring mode supports world 2 to 4, not ", world);
  455. S.ndev = ring ? 4 : 2;
  456. TORCH_CHECK((int)devs.size() == S.ndev && (int)gids.size() == S.ndev, "arx: expected ", S.ndev, " devices");
  457. S.rank = rank; S.world = world; S.ring = ring;
  458. S.prev = (rank + world - 1) % world; S.next = (rank + 1) % world;
  459. CK(cudaHostAlloc(&S.send_h, 2 * kSendBytes, cudaHostAllocMapped));
  460. CK(cudaHostAlloc(&S.recv_h, 2 * world * kSlotBytes, cudaHostAllocMapped));
  461. CK(cudaHostAlloc(&S.flag_h, 2 * kMaxWorld * sizeof(uint64_t), cudaHostAllocMapped));
  462. CK(cudaHostAlloc(&S.ctl, sizeof(Ctl), cudaHostAllocMapped));
  463. memset(S.flag_h, 0, 2 * kMaxWorld * sizeof(uint64_t));
  464. memset((void*)S.ctl, 0, sizeof(Ctl));
  465. int ndev;
  466. ibv_device** list = ibv_get_device_list(&ndev);
  467. const int acc = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE;
  468. for (int d = 0; d < S.ndev; ++d) {
  469. S.gid_idx[d] = gids[d];
  470. for (int i = 0; i < ndev; ++i)
  471. if (devs[d] == ibv_get_device_name(list[i])) S.ctx[d] = ibv_open_device(list[i]);
  472. TORCH_CHECK(S.ctx[d], "arx: no RDMA device ", devs[d]);
  473. S.pd[d] = ibv_alloc_pd(S.ctx[d]); IBCK(S.pd[d]);
  474. S.send_mr[d] = ibv_reg_mr(S.pd[d], S.send_h, 2 * kSendBytes, acc); IBCK(S.send_mr[d]);
  475. S.recv_mr[d] = ibv_reg_mr(S.pd[d], S.recv_h, 2 * world * kSlotBytes, acc); IBCK(S.recv_mr[d]);
  476. S.flag_mr[d] = ibv_reg_mr(S.pd[d], S.flag_h, 2 * kMaxWorld * sizeof(uint64_t), acc); IBCK(S.flag_mr[d]);
  477. S.cq[d] = ibv_create_cq(S.ctx[d], 4096, nullptr, nullptr, 0); IBCK(S.cq[d]);
  478. ibv_gid gid;
  479. IBCK(ibv_query_gid(S.ctx[d], 1, S.gid_idx[d], &gid) == 0);
  480. memcpy(S.mine.gid[d], gid.raw, 16);
  481. ibv_port_attr port{};
  482. IBCK(ibv_query_port(S.ctx[d], 1, &port) == 0);
  483. S.mine.mtu[d] = port.active_mtu;
  484. S.mine.recv_rkey[d] = S.recv_mr[d]->rkey;
  485. S.mine.flag_rkey[d] = S.flag_mr[d]->rkey;
  486. }
  487. ibv_free_device_list(list);
  488. for (int c = 0; c < (ring ? 2 : world); ++c)
  489. for (int r = 0; r < 2; ++r) {
  490. if (!ring && c == rank) continue;
  491. const int d = chan_dev(c, r);
  492. ibv_qp_init_attr ia{};
  493. ia.send_cq = S.cq[d]; ia.recv_cq = S.cq[d]; ia.qp_type = IBV_QPT_RC;
  494. ia.cap.max_send_wr = 1024; ia.cap.max_recv_wr = 1; ia.cap.max_send_sge = 1; ia.cap.max_recv_sge = 1;
  495. ia.cap.max_inline_data = 64;
  496. S.qp[c][r] = ibv_create_qp(S.pd[d], &ia); IBCK(S.qp[c][r]);
  497. ibv_qp_attr a{};
  498. a.qp_state = IBV_QPS_INIT; a.pkey_index = 0; a.port_num = 1; a.qp_access_flags = acc;
  499. IBCK(ibv_modify_qp(S.qp[c][r], &a, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS) == 0);
  500. S.mine.qpn[c][r] = S.qp[c][r]->qp_num;
  501. }
  502. S.mine.recv_addr = (uint64_t)S.recv_h;
  503. S.mine.flag_addr = (uint64_t)S.flag_h;
  504. return py::bytes(reinterpret_cast<const char*>(&S.mine), sizeof(PeerInfo));
  505. }
  506. // Takes every rank's PeerInfo in rank order, brings the QPs up, starts the
  507. // proxy. The caller must barrier its group before the first allreduce.
  508. void arx_connect(std::vector<std::string> infos) {
  509. TORCH_CHECK(S.rank >= 0 && !S.connected && (int)infos.size() == S.world);
  510. S.all.resize(S.world);
  511. for (int j = 0; j < S.world; ++j) {
  512. TORCH_CHECK(infos[j].size() == sizeof(PeerInfo), "arx: PeerInfo size mismatch");
  513. memcpy(&S.all[j], infos[j].data(), sizeof(PeerInfo));
  514. }
  515. for (int c = 0; c < (S.ring ? 2 : S.world); ++c)
  516. for (int r = 0; r < 2; ++r) {
  517. if (!S.ring && c == S.rank) continue;
  518. const PeerInfo& p = S.all[chan_peer(c)];
  519. const int d = chan_dev(c, r), rd = chan_rdev(c, r);
  520. ibv_qp_attr a{};
  521. a.qp_state = IBV_QPS_RTR; a.path_mtu = (ibv_mtu)std::min(S.mine.mtu[d], p.mtu[rd]);
  522. a.dest_qp_num = S.ring ? p.qpn[1 - c][r] : p.qpn[S.rank][r]; a.rq_psn = 0;
  523. a.max_dest_rd_atomic = 1; a.min_rnr_timer = 12;
  524. a.ah_attr.is_global = 1; a.ah_attr.port_num = 1; a.ah_attr.grh.hop_limit = 1;
  525. a.ah_attr.grh.sgid_index = S.gid_idx[d];
  526. memcpy(a.ah_attr.grh.dgid.raw, p.gid[rd], 16);
  527. IBCK(ibv_modify_qp(S.qp[c][r], &a, IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN |
  528. IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER) == 0);
  529. if (S.ring || c == (S.rank + 1) % S.world)
  530. fprintf(stderr, "arx rank %d%s root %d: path MTU %d bytes (ours %d, peer %d)\n", S.rank,
  531. S.ring ? (c ? " next" : " prev") : "", r, 128 << a.path_mtu, 128 << S.mine.mtu[d], 128 << p.mtu[rd]);
  532. a = {};
  533. 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;
  534. IBCK(ibv_modify_qp(S.qp[c][r], &a, IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY |
  535. IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC) == 0);
  536. }
  537. Dev& d = S.dev;
  538. d.send = (__nv_bfloat16*)S.send_h; d.recv = (const __nv_bfloat16*)S.recv_h; d.flag = S.flag_h; d.ctl = S.ctl;
  539. d.rank = S.rank; d.world = S.world;
  540. CK(cudaMalloc(&d.seq_dev, 4)); CK(cudaMalloc(&d.blocks, 8)); CK(cudaMalloc(&d.go, 4));
  541. CK(cudaMemset(d.seq_dev, 0, 4)); CK(cudaMemset(d.blocks, 0, 8)); CK(cudaMemset(d.go, 0, 4));
  542. CK(cudaDeviceSynchronize());
  543. std::thread(proxy_loop).detach();
  544. S.connected = true;
  545. }
  546. void arx_allreduce(torch::Tensor in, torch::Tensor out, std::vector<int64_t> pf_ptrs, std::vector<int64_t> pf_bytes) {
  547. TORCH_CHECK(S.connected, "arx is not connected");
  548. TORCH_CHECK(!g_proxy_err, "arx proxy failed");
  549. TORCH_CHECK(in.scalar_type() == at::kBFloat16 && out.scalar_type() == at::kBFloat16 && in.is_contiguous() &&
  550. out.is_contiguous() && in.numel() == out.numel());
  551. const int n = in.numel();
  552. TORCH_CHECK(n % 8 == 0 && (size_t)n * 2 <= kMaxBytes);
  553. const int threads = 256, blocks = std::max(1, std::min(8, n / (threads * 8)));
  554. Prefetch pf{};
  555. pf.count = (int)std::min<size_t>(pf_ptrs.size(), kMaxPrefetch);
  556. for (int r = 0; r < pf.count; ++r) {
  557. pf.ptr[r] = reinterpret_cast<const uint8_t*>(pf_ptrs[r]);
  558. pf.bytes[r] = pf_bytes[r];
  559. }
  560. arx_kernel<<<blocks, threads, 0, at::cuda::getCurrentCUDAStream()>>>(
  561. S.dev, (const __nv_bfloat16*)in.data_ptr(), (__nv_bfloat16*)out.data_ptr(), n, pf);
  562. }
  563. int64_t arx_max_bytes() { return kMaxBytes; }
  564. // Two-shot all-reduce (mesh only): a reduce-scatter then an all-gather. n must
  565. // split into world chunks of whole 16-byte vectors.
  566. void arx_allreduce2(torch::Tensor in, torch::Tensor out) {
  567. TORCH_CHECK(S.connected, "arx is not connected");
  568. TORCH_CHECK(!g_proxy_err, "arx proxy failed");
  569. TORCH_CHECK(!S.ring, "arx two-shot is mesh only");
  570. TORCH_CHECK(in.scalar_type() == at::kBFloat16 && out.scalar_type() == at::kBFloat16 && in.is_contiguous() &&
  571. out.is_contiguous() && in.numel() == out.numel());
  572. const int n = in.numel(), chunk = n / S.world;
  573. TORCH_CHECK(n % (8 * S.world) == 0 && (size_t)n * 2 <= kSendBytes && (size_t)chunk * 2 <= kSlotBytes);
  574. auto mid = torch::empty({chunk}, in.options());
  575. const int threads = 256;
  576. auto stream = at::cuda::getCurrentCUDAStream();
  577. arx_scatter_kernel<<<std::max(1, std::min(8, chunk / (threads * 8))), threads, 0, stream>>>(
  578. S.dev, (const __nv_bfloat16*)in.data_ptr(), (__nv_bfloat16*)mid.data_ptr(), n);
  579. arx_gather_kernel<<<std::max(1, std::min(8, n / (threads * 8))), threads, 0, stream>>>(
  580. S.dev, (const __nv_bfloat16*)mid.data_ptr(), (__nv_bfloat16*)out.data_ptr(), chunk);
  581. }
  582. int64_t arx_max_bytes2() { return S.ring ? 0 : (int64_t)std::min(kSendBytes, kSlotBytes * S.world); }
  583. PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
  584. m.def("prepare", &arx_prepare);
  585. m.def("connect", &arx_connect);
  586. m.def("allreduce", &arx_allreduce);
  587. m.def("max_bytes", &arx_max_bytes);
  588. m.def("allreduce2", &arx_allreduce2);
  589. m.def("max_bytes2", &arx_max_bytes2);
  590. }