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

174 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +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 

44 

45from hyper_parallel.core.dtensor._utils import ( 

46 differentiable_all_to_all_single_async, 

47 get_device_handle, 

48) 

49 

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

51 

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

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

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

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

56# it is being measured (HP_EP_EQUAL_A2A). 

57_EQUAL_A2A_OFF = "off" 

58_EQUAL_A2A_LAZY = "lazy" 

59_EQUAL_A2A_EAGER = "eager" 

60_EQUAL_A2A_MODES = { 

61 "0": _EQUAL_A2A_OFF, 

62 "1": _EQUAL_A2A_LAZY, 

63 "eager": _EQUAL_A2A_EAGER, 

64} 

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

66 

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

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

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

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

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

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

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

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

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

76 

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

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

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

80# back to the consumer. 

81_LAZY_A2A_STREAM = None 

82_LAZY_A2A_READY_EVENT = None 

83_LAZY_A2A_DONE_EVENT = None 

84_LAZY_A2A_LOCK = threading.Lock() 

85 

86 

87def _backend_supports_uneven_a2a(group) -> bool: 

88 return dist.get_backend(group) in _UNEVEN_A2A_BACKENDS 

89 

90 

91def _equal_a2a_mode() -> str: 

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

93 

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

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

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

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

98 lost overlap can be measured apart. 

99 

100 Returns: 

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

102 

103 Raises: 

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

105 loudly instead of silently leaving the fast path off. 

106 """ 

107 raw = _EQUAL_A2A_RAW.strip() 

108 try: 

109 return _EQUAL_A2A_MODES[raw] 

110 except KeyError: 

111 raise ValueError( 

112 f"HP_EP_EQUAL_A2A must be one of " 

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

114 ) from None 

115 

116 

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

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

119 

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

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

122 (a2a is self-inverse). 

123 """ 

124 

125 @staticmethod 

126 def forward( 

127 ctx: Any, 

128 x: torch.Tensor, 

129 send_counts: list[int], 

130 recv_counts: list[int], 

131 group: Any, 

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

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

134 ctx.send_counts = send_counts 

135 ctx.recv_counts = recv_counts 

136 ctx.group = group 

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

138 if _A2A_SINGLE: 

139 dist.all_to_all_single( 

140 out, 

141 x.contiguous(), 

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

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

144 group=group, 

145 ) 

146 else: 

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

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

149 return out 

150 

151 @staticmethod 

152 def backward( 

153 ctx: Any, 

154 grad_output: torch.Tensor, 

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

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

157 grad = _EPAllToAllUneven.apply( 

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

159 return grad, None, None, None 

160 

161 

162def _equal_a2a_exchange( 

163 x: torch.Tensor, 

164 rows_per_peer: int, 

165 ep_size: int, 

166 group: Any, 

167) -> torch.Tensor: 

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

169 

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

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

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

173 make the two disagree about the layout. 

174 """ 

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

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

177 return out 

178 

179 

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

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

182 

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

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

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

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

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

188 

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

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

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

192 

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

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

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

196 """ 

197 

198 @staticmethod 

199 def forward( 

200 ctx: Any, 

201 x: torch.Tensor, 

202 rows_per_peer: int, 

203 ep_size: int, 

204 group: Any, 

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

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

207 

208 Args: 

209 ctx: Autograd context of this exchange. 

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

211 rows_per_peer: Rows handed to each peer. 

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

213 group: Process group to exchange over. 

214 

215 Returns: 

216 The received rows, concatenated in peer order. 

217 """ 

218 ctx.rows_per_peer = rows_per_peer 

219 ctx.ep_size = ep_size 

220 ctx.group = group 

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

222 

223 @staticmethod 

224 def backward( 

225 ctx: Any, 

226 grad_output: torch.Tensor, 

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

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

229 

230 Args: 

231 ctx: Autograd context of the forward exchange. 

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

233 

234 Returns: 

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

236 """ 

237 grad = _equal_a2a_exchange( 

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

239 return grad, None, None, None 

240 

241 

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

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

244 

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

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

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

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

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

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

251 

252 Returns: 

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

254 the event that hands the result back. 

255 """ 

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

257 with _LAZY_A2A_LOCK: 

258 if _LAZY_A2A_DONE_EVENT is None: 

259 _LAZY_A2A_STREAM = get_device_handle().Stream() 

260 _LAZY_A2A_READY_EVENT = get_device_handle().Event() 

261 _LAZY_A2A_DONE_EVENT = get_device_handle().Event() 

262 return _LAZY_A2A_STREAM, _LAZY_A2A_READY_EVENT, _LAZY_A2A_DONE_EVENT 

263 

264 

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

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

267 

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

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

270 real device stream. 

271 """ 

272 tensor.record_stream(stream) 

273 

274 

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

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

277 

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

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

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

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

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

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

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

285 event. 

286 

287 Args: 

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

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

290 group: Process group to exchange over. 

291 

292 Returns: 

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

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

295 """ 

296 compute_stream = get_device_handle().current_stream() 

297 stream, ready_event, done_event = _lazy_a2a_resources() 

298 stream_context = get_device_handle().stream 

299 ready_event.record(compute_stream) 

300 with stream_context(stream): 

301 ready_event.wait(stream) 

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

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

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

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

306 # this stream is still reading it. 

307 _record_stream(out, stream) 

308 _record_stream(x, stream) 

309 done_event.record(stream) 

310 return done_event 

311 

312 

313class _PendingEqualA2A: 

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

315 

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

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

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

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

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

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

322 

323 Attributes: 

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

325 """ 

326 

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

328 

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

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

331 self._tensor = tensor 

332 self._event = event 

333 self.completed = completed 

334 

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

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

337 if not self.completed: 

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

339 self.completed = True 

340 return self._tensor 

341 

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

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

344 

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

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

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

348 caller reaches its independent work. 

349 

350 Args: 

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

352 

353 Returns: 

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

355 as pending as this one does. 

356 """ 

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

358 

359 def __repr__(self) -> str: 

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

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

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

363 

364 

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

366 """Materialize a pending EP exchange. 

367 

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

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

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

371 

372 Args: 

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

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

375 

376 Returns: 

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

378 not a pending handle. 

379 """ 

380 if isinstance(value, _PendingEqualA2A): 

381 return value.wait() 

382 return value 

383 

384 

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

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

387 

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

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

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

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

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

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

394 

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

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

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

398 """ 

399 

400 @staticmethod 

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

402 ctx: Any, 

403 x: torch.Tensor, 

404 rows_per_peer: int, 

405 ep_size: int, 

406 group: Any, 

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

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

409 

410 Args: 

411 ctx: Autograd context of this exchange. 

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

413 rows_per_peer: Rows handed to each peer. 

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

415 group: Process group to exchange over. 

416 

417 Returns: 

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

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

420 """ 

421 ctx.rows_per_peer = rows_per_peer 

422 ctx.ep_size = ep_size 

423 ctx.group = group 

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

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

426 

427 @staticmethod 

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

429 ctx: Any, 

430 grad_output: torch.Tensor, 

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

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

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

434 

435 Args: 

436 ctx: Autograd context of the forward exchange. 

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

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

439 

440 Returns: 

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

442 """ 

443 grad = _equal_a2a_exchange( 

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

445 return grad, None, None, None 

446 

447 

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

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

450 

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

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

453 globally consistent, obtained via all_reduce MAX) 

454 -> a2a_single -> unpad by recv_counts; 

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

456 -> unpad by send_counts. 

457 """ 

458 

459 @staticmethod 

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

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

462 chunks = [] 

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

464 if n < pad_to: 

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

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

467 chunks.append(chunk) 

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

469 recv = torch.empty_like(send) 

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

471 return recv 

472 

473 @staticmethod 

474 def _unpad(recv, counts, pad_to): 

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

476 pieces = [] 

477 for i, n in enumerate(counts): 

478 if n > 0: 

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

480 if not pieces: 

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

482 return torch.cat(pieces) 

483 

484 @staticmethod 

485 def forward( 

486 ctx: Any, 

487 x: torch.Tensor, 

488 send_counts: list[int], 

489 recv_counts: list[int], 

490 group: Any, 

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

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

493 ctx.send_counts = send_counts 

494 ctx.recv_counts = recv_counts 

495 ctx.group = group 

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

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

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

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

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

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

502 

503 @staticmethod 

504 def backward( 

505 ctx: Any, 

506 grad_output: torch.Tensor, 

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

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

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

510 recv = _EPAllToAllPadded._pad_and_exchange( 

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

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

513 return grad, None, None, None 

514 

515 

516def _equal_a2a_rows( 

517 x: torch.Tensor, 

518 send_counts: list[int], 

519 recv_counts: list[int], 

520 group: Any, 

521) -> Optional[int]: 

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

523 

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

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

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

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

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

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

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

531 runs today. 

532 

533 Args: 

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

535 send_counts: Rows sent to each EP rank. 

536 recv_counts: Rows received from each EP rank. 

537 group: EP process group. 

538 

539 Returns: 

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

541 apply. 

542 """ 

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

544 return None 

545 ep_size = len(send_counts) 

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

547 return None 

548 rows_per_peer = send_counts[0] 

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

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

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

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

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

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

555 return None 

556 return rows_per_peer 

557 

558 

559def ep_all_to_all( 

560 x: torch.Tensor, 

561 send_counts: list[int], 

562 recv_counts: list[int], 

563 group: Any, 

564) -> torch.Tensor: 

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

566 

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

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

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

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

571 returns a materialized tensor; :func:`ep_all_to_all_async` is the lazy one. 

572 """ 

573 if not _backend_supports_uneven_a2a(group): 

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

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

576 if rows_per_peer is not None: 

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

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

579 

580 

581def ep_all_to_all_async( 

582 x: torch.Tensor, 

583 send_counts: list[int], 

584 recv_counts: list[int], 

585 group: Any, 

586 *, 

587 allow_pending: bool = True, 

588) -> Any: 

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

590 

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

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

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

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

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

596 (forward and backward alike). 

597 

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

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

600 

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

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

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

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

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

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

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

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

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

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

611 the lost overlap apart. 

612 

613 Args: 

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

615 send_counts: Rows sent to each EP rank. 

616 recv_counts: Rows received from each EP rank. 

617 group: EP process group. 

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

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

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

621 eager split-free tensor instead. 

622 

623 Returns: 

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

625 """ 

626 if not _backend_supports_uneven_a2a(group): 

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

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

629 if rows_per_peer is not None: 

630 ep_size = len(send_counts) 

631 if _equal_a2a_mode() == _EQUAL_A2A_LAZY and allow_pending: 

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

633 return _PendingEqualA2A(tensor, event) 

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

635 return differentiable_all_to_all_single_async( 

636 x, send_counts, recv_counts, group, 

637 )