Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / expert_parallel / collectives.py: 96%

194 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +0800

1# Copyright 2025-2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15 

16"""expert_parallel.collectives: backend-dispatched EP all_to_all. 

17 

18NCCL/HCCL use the ragged a2a (``_EPAllToAllUneven``, zero-padding); gloo and 

19other backends that do not support ragged a2a use pad-to-max + 

20``all_to_all_single`` (``_EPAllToAllPadded``). Both paths are numerically 

21equivalent (padding only adds filler rows that do not participate in 

22computation). 

23 

24When every per-peer count is equal -- which is exactly what a balanced routing 

25plan produces -- the split sizes carry no information, so ``HP_EP_EQUAL_A2A`` 

26swaps the ragged exchange for the plain equal-length ``all_to_all_single``, 

27whose backend kernel is cheaper than alltoallv. The rows, their order and the 

28values are identical on both paths. On top of the kernel, the exchange has to 

29stay *lazy*: the champion issues its token exchange through the async entry and 

30hides it behind the shared-expert MLP, and a plain c10d collective orders the 

31stream it is called on at issue time, which closes that window. ``=1`` 

32therefore means split-free *and* lazy (``_EPAllToAllEqualLazy``: the exchange is 

33issued on a stream of its own and the caller waits on an event), while ``=eager`` 

34forces the split-free call back onto the caller's stream for an A/B of the two. 

35 

36Split out of components/distributed/ep_utils.py in stage 4e. 

37""" 

38 

39import os 

40import threading 

41from typing import Any, Optional 

42import torch 

43import torch.distributed as dist 

44from torch import _check, _check_is_size 

45from torch.distributed import _functional_collectives 

46 

47from hyper_parallel.core.dtensor._utils import ( 

48 differentiable_all_to_all_single_async, 

49 get_device_handle, 

50) 

51from hyper_parallel.core.utils.communication import _is_tracing_or_compiling 

52 

53_UNEVEN_A2A_BACKENDS = ("nccl", "hccl") 

54 

55# A ragged exchange reaches NCCL/HCCL as alltoallv, which takes a per-peer count 

56# vector and runs a count-driven kernel. Under a balanced routing plan every 

57# per-peer count is the same, so the counts add nothing and the plain 

58# equal-length all_to_all_single (alltoall) can be issued instead. Opt-in while 

59# it is being measured (HP_EP_EQUAL_A2A). 

60_EQUAL_A2A_OFF = "off" 

61_EQUAL_A2A_LAZY = "lazy" 

62_EQUAL_A2A_EAGER = "eager" 

63_EQUAL_A2A_MODES = { 

64 "0": _EQUAL_A2A_OFF, 

65 "1": _EQUAL_A2A_LAZY, 

66 "eager": _EQUAL_A2A_EAGER, 

67} 

68_EQUAL_A2A_RAW = os.environ.get("HP_EP_EQUAL_A2A", "0") 

69 

70# Ragged (uneven) exchange kernel: the list form issues one 

71# ``dist.all_to_all`` with per-peer tensors, which HCCL serves through 

72# alltoallv; ``HP_EP_A2A_SINGLE=1`` issues the identical exchange as one 

73# ``all_to_all_single`` with split sizes, the form the micro benchmark 

74# measures at ~2x the bandwidth (106.4 vs 52.9 GB/s). Default off. Scope: this 

75# only affects THIS synchronous entry; the training MoE path calls 

76# ``ep_all_to_all_async``, whose ``_AsyncA2ALazyBwd`` already issues the split-sized 

77# ``all_to_all_single``, so the knob is a no-op there (measured: neutral). 

78_A2A_SINGLE = os.environ.get("HP_EP_A2A_SINGLE", "0") == "1" 

79 

80# Lazily created, then reused for every lazy exchange (see 

81# :func:`_lazy_a2a_resources`): the stream the split-free collective is issued 

82# on, the event that hands it the payload, and the event that hands the result 

83# back to the consumer. 

84_LAZY_A2A_STREAM = None 

85_LAZY_A2A_READY_EVENT = None 

86_LAZY_A2A_DONE_EVENT = None 

87_LAZY_A2A_LOCK = threading.Lock() 

88 

89 

90def ep_exchange_counts(counts: torch.Tensor, group: Any) -> torch.Tensor: 

91 """Exchange one integer count per peer, waiting before reading the result. 

92 

93 Args: 

94 counts: Non-differentiable send counts in process-group rank order. 

95 group: Expert-parallel process group. 

96 

97 Returns: 

98 Counts received from each peer in the same rank order. 

99 """ 

100 if _is_tracing_or_compiling(): 

101 return _functional_collectives.wait_tensor( 

102 _functional_collectives.all_to_all_single(counts, None, None, group) 

103 ) 

104 received = torch.empty_like(counts) 

105 dist.all_to_all_single(received, counts, group=group) 

106 return received 

107 

108 

109def ep_split_sizes(counts: torch.Tensor, total: Optional[int] = None) -> list[int]: 

110 """Read per-call segment sizes, retaining data dependencies during capture. 

111 

112 Args: 

113 counts: One-dimensional integer tensor of segment lengths. 

114 total: Expected number of rows, when known from a payload's shape. 

115 

116 Returns: 

117 Concrete eager sizes or symbolic sizes evaluated by the captured graph. 

118 """ 

119 if not _is_tracing_or_compiling(): 

120 return counts.tolist() 

121 sizes = counts.cpu().tolist() 

122 for size in sizes: 

123 _check_is_size(size) 

124 if total is not None: 

125 _check(sum(sizes) == total) 

126 return sizes 

127 

128 

129def _backend_supports_uneven_a2a(group) -> bool: 

130 return dist.get_backend(group) in _UNEVEN_A2A_BACKENDS 

131 

132 

133def _equal_a2a_mode() -> str: 

134 """Resolve ``HP_EP_EQUAL_A2A`` to the split-free exchange's mode. 

135 

136 ``"0"`` (the default) keeps the ragged exchange, ``"1"`` takes the 

137 split-free exchange and defers its wait, and ``"eager"`` takes the 

138 split-free exchange while ordering the caller's stream on it at issue time 

139 -- the behaviour ``"1"`` used to have, kept so the kernel's gain and the 

140 lost overlap can be measured apart. 

141 

142 Returns: 

143 One of ``"off"``, ``"lazy"``, ``"eager"``. 

144 

145 Raises: 

146 ValueError: If the knob is none of the accepted values; a typo must fail 

147 loudly instead of silently leaving the fast path off. 

148 """ 

149 raw = _EQUAL_A2A_RAW.strip() 

150 try: 

151 return _EQUAL_A2A_MODES[raw] 

152 except KeyError: 

153 raise ValueError( 

154 f"HP_EP_EQUAL_A2A must be one of " 

155 f"{sorted(_EQUAL_A2A_MODES)}, but got {raw!r}" 

156 ) from None 

157 

158 

159class _EPAllToAllUneven(torch.autograd.Function): # pylint: disable=abstract-method 

160 """Ragged all_to_all (NCCL/HCCL production path): split by send/recv counts. 

161 

162 forward: split(x, send_counts) -> dist.all_to_all(out_list, in_list) -> cat 

163 backward: swap send/recv counts and run the ragged all_to_all again 

164 (a2a is self-inverse). 

165 """ 

166 

167 @staticmethod 

168 def forward( 

169 ctx: Any, 

170 x: torch.Tensor, 

171 send_counts: list[int], 

172 recv_counts: list[int], 

173 group: Any, 

174 ) -> torch.Tensor: # pylint: disable=arguments-differ 

175 """Run the ragged all_to_all and retain the counts for backward.""" 

176 ctx.send_counts = send_counts 

177 ctx.recv_counts = recv_counts 

178 ctx.group = group 

179 out = x.new_empty((sum(recv_counts),) + tuple(x.shape[1:])) 

180 if _A2A_SINGLE: 

181 dist.all_to_all_single( 

182 out, 

183 x.contiguous(), 

184 output_split_sizes=[int(count) for count in recv_counts], 

185 input_split_sizes=[int(count) for count in send_counts], 

186 group=group, 

187 ) 

188 else: 

189 dist.all_to_all(list(out.split(recv_counts)), 

190 list(x.split(send_counts)), group=group) 

191 return out 

192 

193 @staticmethod 

194 def backward( 

195 ctx: Any, 

196 grad_output: torch.Tensor, 

197 ) -> tuple[torch.Tensor, None, None, None]: # pylint: disable=arguments-differ 

198 """Swap send/recv counts and re-run the self-inverse ragged all_to_all.""" 

199 grad = _EPAllToAllUneven.apply( 

200 grad_output.contiguous(), ctx.recv_counts, ctx.send_counts, ctx.group) 

201 return grad, None, None, None 

202 

203 

204def _equal_a2a_exchange( 

205 x: torch.Tensor, 

206 rows_per_peer: int, 

207 ep_size: int, 

208 group: Any, 

209) -> torch.Tensor: 

210 """Move ``rows_per_peer`` rows to each peer and return the peer-major result. 

211 

212 The split-free call: the backend derives the chunking from the tensor shape 

213 alone, and the received chunks are concatenated in peer order. Both the 

214 eager and the lazy equal-length exchange issue this, so a change here cannot 

215 make the two disagree about the layout. 

216 """ 

217 out = x.new_empty((rows_per_peer * ep_size,) + tuple(x.shape[1:])) 

218 dist.all_to_all_single(out, x, group=group) 

219 return out 

220 

221 

222class _EPAllToAllEqual(torch.autograd.Function): # pylint: disable=abstract-method 

223 """Equal-length ``all_to_all_single`` (NCCL/HCCL path when counts are uniform). 

224 

225 Without split sizes every peer gets ``rows_per_peer`` rows -- the backend 

226 derives the chunking from the tensor shape alone -- and the received chunks 

227 are concatenated in peer order. That is exactly the layout 

228 :class:`_EPAllToAllUneven` produces when all counts are equal, so this path 

229 is interchangeable with it (see :func:`_equal_a2a_rows` for when it applies). 

230 

231 forward: all_to_all_single(no splits) -> [ep_size * rows_per_peer, ...] 

232 backward: the same exchange; an equal-length a2a is its own inverse, and 

233 with a uniform plan the reverse chunks are the same size. 

234 

235 ``forward`` issues the exchange on the caller's stream, which orders that 

236 stream on the transfer before it returns; :class:`_EPAllToAllEqualLazy` is 

237 the same exchange with the ordering left to the caller. 

238 """ 

239 

240 @staticmethod 

241 def forward( 

242 ctx: Any, 

243 x: torch.Tensor, 

244 rows_per_peer: int, 

245 ep_size: int, 

246 group: Any, 

247 ) -> torch.Tensor: # pylint: disable=arguments-differ 

248 """Run the equal-length all_to_all and retain its geometry for backward. 

249 

250 Args: 

251 ctx: Autograd context of this exchange. 

252 x: Payload to exchange; it holds ``ep_size * rows_per_peer`` rows. 

253 rows_per_peer: Rows handed to each peer. 

254 ep_size: Number of ranks in ``group``. 

255 group: Process group to exchange over. 

256 

257 Returns: 

258 The received rows, concatenated in peer order. 

259 """ 

260 ctx.rows_per_peer = rows_per_peer 

261 ctx.ep_size = ep_size 

262 ctx.group = group 

263 return _equal_a2a_exchange(x, rows_per_peer, ep_size, group) 

264 

265 @staticmethod 

266 def backward( 

267 ctx: Any, 

268 grad_output: torch.Tensor, 

269 ) -> tuple[torch.Tensor, None, None, None]: # pylint: disable=arguments-differ 

270 """Re-run the self-inverse equal-length exchange on the output gradient. 

271 

272 Args: 

273 ctx: Autograd context of the forward exchange. 

274 grad_output: Output gradient, one row per exchanged row. 

275 

276 Returns: 

277 The input gradient, followed by ``None`` for the non-tensor arguments. 

278 """ 

279 grad = _equal_a2a_exchange( 

280 grad_output.contiguous(), ctx.rows_per_peer, ctx.ep_size, ctx.group) 

281 return grad, None, None, None 

282 

283 

284def _lazy_a2a_resources() -> tuple[Any, Any, Any]: 

285 """Return the ``(stream, ready_event, done_event)`` the lazy exchange reuses. 

286 

287 One stream and two events for the whole process, which is deliberate: a 

288 fresh event per exchange drains the runtime's event pool after a few hundred 

289 exchanges (the run then dies in ``AclQueryEventRecordedStatus``), and one 

290 stream is enough because every record and wait below is enqueued in host 

291 order -- a wait therefore never observes a *stale* record, at worst a later 

292 one, which only makes it wait longer than it strictly had to. 

293 

294 Returns: 

295 The shared comm stream, the event that hands the payload over to it, and 

296 the event that hands the result back. 

297 """ 

298 global _LAZY_A2A_STREAM, _LAZY_A2A_READY_EVENT, _LAZY_A2A_DONE_EVENT # pylint: disable=global-statement 

299 with _LAZY_A2A_LOCK: 

300 if _LAZY_A2A_DONE_EVENT is None: 

301 _LAZY_A2A_STREAM = get_device_handle().Stream() 

302 _LAZY_A2A_READY_EVENT = get_device_handle().Event() 

303 _LAZY_A2A_DONE_EVENT = get_device_handle().Event() 

304 return _LAZY_A2A_STREAM, _LAZY_A2A_READY_EVENT, _LAZY_A2A_DONE_EVENT 

305 

306 

307def _record_stream(tensor: torch.Tensor, stream: Any) -> None: 

308 """Tell the caching allocator that ``stream`` touches ``tensor``'s storage. 

309 

310 Wrapped rather than called inline so the exchange can be driven with a fake 

311 stream in unit tests: ``Tensor.record_stream`` rejects anything that is not a 

312 real device stream. 

313 """ 

314 tensor.record_stream(stream) 

315 

316 

317def _issue_split_free_a2a(out: torch.Tensor, x: torch.Tensor, group: Any) -> Any: 

318 """Issue the split-free exchange on the shared comm stream. 

319 

320 ``dist.all_to_all_single`` without split sizes is the one call shape whose 

321 backend kernel is the plain alltoall, and it is a *blocking* collective: 

322 c10d orders the stream it is called on after the transfer before returning, 

323 which is what would close the caller's overlap window. Issuing it inside a 

324 stream context moves that ordering onto the comm stream. c10d only orders 

325 the collective against the stream it is called on, so the payload's 

326 producers are re-connected to that stream explicitly through the ready 

327 event. 

328 

329 Args: 

330 out: Receive buffer, ``[ep_size * rows_per_peer, ...]``; written here. 

331 x: Contiguous payload, ``[ep_size * rows_per_peer, ...]``; read here. 

332 group: Process group to exchange over. 

333 

334 Returns: 

335 The event recorded when the exchange has completed; the consumer's 

336 stream waits on it (see :class:`_PendingEqualA2A`). 

337 """ 

338 compute_stream = get_device_handle().current_stream() 

339 stream, ready_event, done_event = _lazy_a2a_resources() 

340 stream_context = get_device_handle().stream 

341 ready_event.record(compute_stream) 

342 with stream_context(stream): 

343 ready_event.wait(stream) 

344 dist.all_to_all_single(out, x, group=group) 

345 # Both buffers outlive the call by different means: the receive buffer 

346 # was allocated on the compute stream, and the payload is a temporary of 

347 # the caller's dispatch, so its storage may be freed and reused while 

348 # this stream is still reading it. 

349 _record_stream(out, stream) 

350 _record_stream(x, stream) 

351 done_event.record(stream) 

352 return done_event 

353 

354 

355class _PendingEqualA2A: 

356 """Handle to a split-free exchange that is still in flight. 

357 

358 ``wait()`` is the only way to read the result, and *where* it is called is 

359 the caller's decision: the exchange is issued while the dispatch runs, but 

360 the consumer's stream is only ordered on it at ``wait()``, so independent 

361 work issued in between (the shared-expert MLP an ``overlap_fn`` runs) works 

362 against the transfer instead of after it. Reading the exchange without 

363 waiting is not an option -- the handle is not a tensor. 

364 

365 Attributes: 

366 completed: Whether a stream has already been ordered on the exchange. 

367 """ 

368 

369 __slots__ = ("completed", "_event", "_tensor") 

370 

371 def __init__(self, tensor: torch.Tensor, event: Any, completed: bool = False) -> None: 

372 """Wrap an issued-but-unwaited exchange result.""" 

373 self._tensor = tensor 

374 self._event = event 

375 self.completed = completed 

376 

377 def wait(self) -> torch.Tensor: 

378 """Order the current stream after the exchange and return its result.""" 

379 if not self.completed: 

380 self._event.wait(get_device_handle().current_stream()) 

381 self.completed = True 

382 return self._tensor 

383 

384 def squeeze(self, dim: int) -> "_PendingEqualA2A": 

385 """Return a handle to the squeezed result (a view, so the wait is pending). 

386 

387 The dispatched expert indices arrive as ``[rows, 1]`` and their consumers 

388 want ``[rows]``. Squeezing is metadata-only, so it can be taken here 

389 without materializing: a read here would enqueue the wait before the 

390 caller reaches its independent work. 

391 

392 Args: 

393 dim: Dimension to squeeze, as ``Tensor.squeeze`` takes it. 

394 

395 Returns: 

396 A handle to the squeezed result; it still holds the exchange's result 

397 as pending as this one does. 

398 """ 

399 return _PendingEqualA2A(self._tensor.squeeze(dim), self._event, self.completed) 

400 

401 def __repr__(self) -> str: 

402 """Name the pending exchange and what is known about it without reading it.""" 

403 return (f"PendingEqualA2A(shape={tuple(self._tensor.shape)}, " 

404 f"completed={self.completed})") 

405 

406 

407def wait_ep_all_to_all(value: Any) -> Any: 

408 """Materialize a pending EP exchange. 

409 

410 Every consumer of an :func:`ep_all_to_all_async` result must pass it through 

411 here before reading it: with ``HP_EP_EQUAL_A2A=1`` and a uniform plan that 

412 entry returns a :class:`_PendingEqualA2A` instead of a tensor. 

413 

414 Args: 

415 value: The pending handle, an already-materialized tensor from any other 

416 path of :func:`ep_all_to_all_async`, or ``None``. 

417 

418 Returns: 

419 The exchange result as a regular tensor; ``value`` unchanged when it is 

420 not a pending handle. 

421 """ 

422 if isinstance(value, _PendingEqualA2A): 

423 return value.wait() 

424 return value 

425 

426 

427class _EPAllToAllEqualLazy(torch.autograd.Function): # pylint: disable=abstract-method 

428 """Split-free equal-length exchange issued on the comm stream, waited lazily. 

429 

430 Same exchange as :class:`_EPAllToAllEqual` and just as differentiable, but 

431 the forward leaves the caller's stream alone: it issues on the shared comm 

432 stream and returns the buffer together with the event that says when the 

433 buffer is final, so the caller can synchronize when it needs the values 

434 rather than when it issues the exchange. Both travel out of ``forward`` -- 

435 the buffer as the autograd output, the event as a second, non-tensor output. 

436 

437 The backward is the plain split-free exchange on the caller's stream: the 

438 engine consumes its result immediately (it accumulates it onto the input's 

439 ``grad``), so there is nothing to defer. 

440 """ 

441 

442 @staticmethod 

443 def forward( # pylint: disable=arguments-differ 

444 ctx: Any, 

445 x: torch.Tensor, 

446 rows_per_peer: int, 

447 ep_size: int, 

448 group: Any, 

449 ) -> tuple[torch.Tensor, Any]: 

450 """Issue the split-free exchange and return ``(buffer, done event)``. 

451 

452 Args: 

453 ctx: Autograd context of this exchange. 

454 x: Contiguous payload, ``[ep_size * rows_per_peer, ...]``. 

455 rows_per_peer: Rows handed to each peer. 

456 ep_size: Number of ranks in ``group``. 

457 group: Process group to exchange over. 

458 

459 Returns: 

460 The receive buffer the exchange writes -- the output the autograd 

461 graph is built on -- and the event that says when it is final. 

462 """ 

463 ctx.rows_per_peer = rows_per_peer 

464 ctx.ep_size = ep_size 

465 ctx.group = group 

466 out = x.new_empty((rows_per_peer * ep_size,) + tuple(x.shape[1:])) 

467 return out, _issue_split_free_a2a(out, x, group) 

468 

469 @staticmethod 

470 def backward( # pylint: disable=arguments-differ 

471 ctx: Any, 

472 grad_output: torch.Tensor, 

473 _event_grad: Any, # pylint: disable=invalid-name 

474 ) -> tuple[torch.Tensor, None, None, None]: 

475 """Re-run the self-inverse exchange, on the caller's stream this time. 

476 

477 Args: 

478 ctx: Autograd context of the forward exchange. 

479 grad_output: Output gradient, one row per exchanged row. 

480 _event_grad: Gradient slot of the forward's event output; unused. 

481 

482 Returns: 

483 The input gradient, followed by ``None`` for the non-tensor arguments. 

484 """ 

485 grad = _equal_a2a_exchange( 

486 grad_output.contiguous(), ctx.rows_per_peer, ctx.ep_size, ctx.group) 

487 return grad, None, None, None 

488 

489 

490class _EPAllToAllPadded(torch.autograd.Function): # pylint: disable=abstract-method 

491 """pad-to-max + all_to_all_single (gloo test path). 

492 

493 forward: pad each dest chunk to the global max(counts) (a2a_single 

494 requires equal-length chunks per rank -> pad_to must be 

495 globally consistent, obtained via all_reduce MAX) 

496 -> a2a_single -> unpad by recv_counts; 

497 backward: pad by recv_counts -> a2a_single (equal-length self-inverse) 

498 -> unpad by send_counts. 

499 """ 

500 

501 @staticmethod 

502 def _pad_and_exchange(x, counts, pad_to, group): 

503 """Pad each chunk to pad_to, run equal-length a2a_single, return [ep*pad_to, ...].""" 

504 chunks = [] 

505 for chunk, n in zip(x.split(counts), counts): 

506 if n < pad_to: 

507 pad = x.new_zeros((pad_to - n,) + tuple(x.shape[1:])) 

508 chunk = torch.cat([chunk, pad]) 

509 chunks.append(chunk) 

510 send = torch.cat(chunks).contiguous() 

511 recv = torch.empty_like(send) 

512 dist.all_to_all_single(recv, send, group=group) 

513 return recv 

514 

515 @staticmethod 

516 def _unpad(recv, counts, pad_to): 

517 """Take the valid rows from the equal-length buffer per counts and cat.""" 

518 pieces = [] 

519 for i, n in enumerate(counts): 

520 if n > 0: 

521 pieces.append(recv[i * pad_to: i * pad_to + n]) 

522 if not pieces: 

523 return recv.new_zeros((0,) + tuple(recv.shape[1:])) 

524 return torch.cat(pieces) 

525 

526 @staticmethod 

527 def forward( 

528 ctx: Any, 

529 x: torch.Tensor, 

530 send_counts: list[int], 

531 recv_counts: list[int], 

532 group: Any, 

533 ) -> torch.Tensor: # pylint: disable=arguments-differ 

534 """Exchange padded expert-token chunks and retain counts for backward.""" 

535 ctx.send_counts = send_counts 

536 ctx.recv_counts = recv_counts 

537 ctx.group = group 

538 local_max = max([*send_counts, *recv_counts, 1]) 

539 pad_to = x.new_tensor([local_max], dtype=torch.int64) 

540 dist.all_reduce(pad_to, op=dist.ReduceOp.MAX, group=group) 

541 ctx.pad_to = pad_to = int(pad_to.item()) 

542 recv = _EPAllToAllPadded._pad_and_exchange(x, send_counts, pad_to, group) 

543 return _EPAllToAllPadded._unpad(recv, recv_counts, pad_to) 

544 

545 @staticmethod 

546 def backward( 

547 ctx: Any, 

548 grad_output: torch.Tensor, 

549 ) -> tuple[torch.Tensor, None, None, None]: # pylint: disable=arguments-differ 

550 """Reverse the exchange: pad by recv_counts, a2a_single, unpad by send_counts.""" 

551 # backward = reversed a2a: pad by recv_counts -> a2a_single -> unpad by send_counts 

552 recv = _EPAllToAllPadded._pad_and_exchange( 

553 grad_output.contiguous(), ctx.recv_counts, ctx.pad_to, ctx.group) 

554 grad = _EPAllToAllPadded._unpad(recv, ctx.send_counts, ctx.pad_to) 

555 return grad, None, None, None 

556 

557 

558def _equal_a2a_rows( 

559 x: torch.Tensor, 

560 send_counts: list[int], 

561 recv_counts: list[int], 

562 group: Any, 

563) -> Optional[int]: 

564 """Rows per peer when the exchange may take the equal-length path, else None. 

565 

566 The equal-length path is only equivalent to the ragged one when the plan is 

567 uniform *and* the payload really holds one such chunk per peer: the split 

568 sizes then carry no information, and the plain all-to-all -- which derives 

569 its chunking from the tensor shape alone -- moves exactly the rows the 

570 ragged call would. Anything else (unequal counts, a payload whose row count 

571 disagrees with the counts, a non-contiguous payload, the knob off, a backend 

572 without a ragged all-to-all) yields ``None`` so the caller keeps the path it 

573 runs today. 

574 

575 Args: 

576 x: Payload to exchange, split along dim 0. 

577 send_counts: Rows sent to each EP rank. 

578 recv_counts: Rows received from each EP rank. 

579 group: EP process group. 

580 

581 Returns: 

582 The uniform per-peer row count, or ``None`` when the fast path does not 

583 apply. 

584 """ 

585 if _equal_a2a_mode() == _EQUAL_A2A_OFF or not _backend_supports_uneven_a2a(group): 

586 return None 

587 ep_size = len(send_counts) 

588 if ep_size == 0 or len(recv_counts) != ep_size or not x.is_contiguous(): 

589 return None 

590 rows_per_peer = send_counts[0] 

591 # The split-free call takes the chunk size from the tensor shape and needs 

592 # the receive buffer to be the input's size, so a uniform plan is only 

593 # equivalent when both count lists hold that same count. 

594 uniform = (all(count == rows_per_peer for count in send_counts) 

595 and all(count == rows_per_peer for count in recv_counts)) 

596 if not uniform or x.shape[0] != rows_per_peer * ep_size: 

597 return None 

598 return rows_per_peer 

599 

600 

601def ep_all_to_all( 

602 x: torch.Tensor, 

603 send_counts: list[int], 

604 recv_counts: list[int], 

605 group: Any, 

606) -> torch.Tensor: 

607 """Unified entry for EP token exchange (autograd-differentiable). 

608 

609 send_counts/recv_counts: list[int], length ep_size, row counts per dest/src rank. 

610 NCCL/HCCL -> ragged a2a (zero-padding); other backends (gloo test path) -> pad-to-max. 

611 With ``HP_EP_EQUAL_A2A`` on, a uniform plan is exchanged with the equal-length 

612 a2a instead (same rows, same order, cheaper kernel). This entry always 

613 returns a materialized tensor in eager mode; graph capture records the 

614 functional exchange and its consumer wait. :func:`ep_all_to_all_async` 

615 provides the eager lazy entry. 

616 """ 

617 if _is_tracing_or_compiling(): 

618 # Keep the autograd wrapper; its first consumer inserts the stream wait. 

619 return _functional_collectives.all_to_all_single_autograd(x, recv_counts, send_counts, group) 

620 if not _backend_supports_uneven_a2a(group): 

621 return _EPAllToAllPadded.apply(x, send_counts, recv_counts, group) 

622 rows_per_peer = _equal_a2a_rows(x, send_counts, recv_counts, group) 

623 if rows_per_peer is not None: 

624 return _EPAllToAllEqual.apply(x, rows_per_peer, len(send_counts), group) 

625 return _EPAllToAllUneven.apply(x, send_counts, recv_counts, group) 

626 

627 

628def ep_all_to_all_async( 

629 x: torch.Tensor, 

630 send_counts: list[int], 

631 recv_counts: list[int], 

632 group: Any, 

633 *, 

634 allow_pending: bool = True, 

635) -> Any: 

636 """Non-blocking variant of :func:`ep_all_to_all` (lazy wait). 

637 

638 On backends whose ``all_to_all_single`` accepts unequal splits the exchange 

639 is issued through ``differentiable_all_to_all_single_async``, which returns 

640 an ``AsyncCollectiveTensor``: the ``wait_tensor`` op is only enqueued when a 

641 non-view op first consumes the result. Issuing independent work between the 

642 exchange and that first read therefore overlaps with the in-flight transfer 

643 (forward and backward alike). 

644 

645 Backends without that support fall back to the blocking path, so the result 

646 always carries the same values — only the schedule differs. 

647 

648 With ``HP_EP_EQUAL_A2A=1`` and a uniform plan the exchange is the split-free 

649 equal-length one instead -- the cheaper backend kernel -- and it stays lazy: 

650 :class:`_EPAllToAllEqualLazy` issues it on a stream of its own, so the two 

651 properties are no longer a trade. The result is then a 

652 :class:`_PendingEqualA2A` *handle* rather than a tensor, and every consumer 

653 must read it through :func:`wait_ep_all_to_all`; that call is where the 

654 consumer's stream is ordered on the transfer, which is what the caller wants 

655 to place after its overlapped work. With ``=eager`` the same split-free 

656 exchange is issued on the caller's stream and a plain tensor comes back -- 

657 the behaviour ``=1`` used to have, kept for measuring the kernel's gain and 

658 the lost overlap apart. 

659 

660 Args: 

661 x: Input tensor, split along dim 0 by ``send_counts``. 

662 send_counts: Rows sent to each EP rank. 

663 recv_counts: Rows received from each EP rank. 

664 group: EP process group. 

665 allow_pending: Whether a uniform plan may return a pending handle. A 

666 caller whose next step is a view chain over the exchanged buffer 

667 (the fused states+indices dispatch) passes ``False`` and gets the 

668 eager split-free tensor instead. 

669 

670 Returns: 

671 The exchanged rows, materialized lazily on the async path. 

672 """ 

673 if not _backend_supports_uneven_a2a(group): 

674 return _EPAllToAllPadded.apply(x, send_counts, recv_counts, group) 

675 rows_per_peer = _equal_a2a_rows(x, send_counts, recv_counts, group) 

676 if rows_per_peer is not None: 

677 ep_size = len(send_counts) 

678 if _equal_a2a_mode() == _EQUAL_A2A_LAZY and allow_pending: 

679 tensor, event = _EPAllToAllEqualLazy.apply(x, rows_per_peer, ep_size, group) 

680 return _PendingEqualA2A(tensor, event) 

681 return _EPAllToAllEqual.apply(x, rows_per_peer, ep_size, group) 

682 return differentiable_all_to_all_single_async( 

683 x, send_counts, recv_counts, group, 

684 )