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

269 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"""context_parallel.collectives: CP collective primitives (05 §4.4.2 / §6.3.4). 

17 

18- ``flex_cp_allgather``: K/V all-gather along the CP dim (reuses 

19 cp_mesh.get_group(); new_group is forbidden); 

20- ``ulysses_seq_to_head`` / ``ulysses_head_to_seq``: differentiable Pure 

21 Ulysses all-to-all layout transforms; 

22- async collective launches (``AsyncCPCollective`` / 

23 ``async_cp_allgather_launch`` / ``async_ulysses_seq_to_head_launch``); 

24- hybrid (Ulysses + all-gather) CP sub-mesh construction and attention 

25 orchestration (``_build_hybrid_cp_submeshes`` / ``hybrid_cp_attention``). 

26 

27Note (G5): the seq_len % (2*cp) padding constraint originates from the 

28zigzag/ring load-balancing scheme; this design uses all-gather K/V + 

29contiguous chunk (D-01'' rejected ring), so each rank's Q chunk is 

30equal-length and FLOPs are naturally balanced -- the constraint is redundant 

31but harmless: the implementation is kept and documented here. 

32 

33Split out of components/distributed/cp_utils.py in stage 4e; the attention 

34adaptations live in context_parallel/attention.py and the batch sharding in 

35hyper_parallel/data/parallel/batch_parallel.py. 

36""" 

37 

38# The legacy header checker does not recognize the existing multi-year copyright. 

39# pylint: disable=missing-apache-license-header 

40 

41from dataclasses import dataclass 

42from typing import Any, Callable, Optional 

43import torch # pylint: disable=forbidden-backend-import 

44import torch.distributed as dist # pylint: disable=forbidden-backend-import 

45from torch import Tensor # pylint: disable=forbidden-backend-import 

46from torch.distributed.nn.functional import all_gather as differentiable_all_gather # pylint: disable=forbidden-backend-import 

47from hyper_parallel.core.dtensor.device_mesh import DeviceMesh 

48from hyper_parallel.core.utils import communication 

49 

50 

51_ULYSSES_WRAPPED_FLAG = "_hyper_ulysses_wrapped" 

52 

53 

54_HYBRID_MESH_CACHE = {} 

55 

56 

57class _SeqAllToAll(torch.autograd.Function): 

58 """Autograd-aware exchange from one tensor dimension to another.""" 

59 

60 @staticmethod 

61 def forward( 

62 ctx: Any, 

63 group: Any, 

64 tensor: Tensor, 

65 scatter_dim: int, 

66 gather_dim: int, 

67 ) -> Tensor: 

68 """Exchange tensor shards between the sequence and head dimensions.""" 

69 ctx.group = group 

70 ctx.scatter_dim = scatter_dim 

71 ctx.gather_dim = gather_dim 

72 world_size = dist.get_world_size(group) 

73 if tensor.size(scatter_dim) % world_size: 

74 raise ValueError( 

75 f"dimension {scatter_dim} size {tensor.size(scatter_dim)} " 

76 f"is not divisible by CP size {world_size}") 

77 inputs = [ 

78 part.contiguous() 

79 for part in tensor.chunk(world_size, dim=scatter_dim) 

80 ] 

81 outputs = [torch.empty_like(inputs[0]) for _ in range(world_size)] 

82 dist.all_to_all(outputs, inputs, group=group) 

83 return torch.cat(outputs, dim=gather_dim).contiguous() 

84 

85 @staticmethod 

86 def backward( 

87 ctx: Any, 

88 grad_output: Tensor, 

89 ) -> tuple[None, Tensor, None, None]: 

90 """Apply the inverse all-to-all to the output gradient.""" 

91 grad_input = _SeqAllToAll.apply( 

92 ctx.group, grad_output, ctx.gather_dim, ctx.scatter_dim) 

93 return None, grad_input, None, None 

94 

95 

96@dataclass(frozen=True) 

97class _UlyssesContext: 

98 """Communication context derived from the framework-owned CP mesh.""" 

99 

100 cp_mesh: Any 

101 

102 @property 

103 def group(self) -> Any: 

104 """Process group of the CP mesh.""" 

105 return self.cp_mesh.get_group() 

106 

107 @property 

108 def size(self) -> int: 

109 """Number of ranks in the CP mesh.""" 

110 return self.cp_mesh.size() 

111 

112 @property 

113 def rank(self) -> int: 

114 """Local rank of this process within the CP mesh.""" 

115 return self.cp_mesh.get_local_rank() 

116 

117 

118def _sequence_to_head(tensor, context): 

119 """[B, S/CP, H, D] -> [B, S, H/CP, D].""" 

120 return _SeqAllToAll.apply(context.group, tensor, 2, 1) 

121 

122 

123def _head_to_sequence(tensor, context): 

124 """[B, S, H/CP, D] -> [B, S/CP, H, D].""" 

125 return _SeqAllToAll.apply(context.group, tensor, 1, 2) 

126 

127 

128def _gather_sequence(tensor, context): 

129 """Gather sequence shards and sum their gradients during backward.""" 

130 return torch.cat( 

131 differentiable_all_gather( 

132 tensor.contiguous(), group=context.group), dim=1) 

133 

134 

135def _slice_sequence(tensor, dim, context): 

136 if tensor.size(dim) % context.size: 

137 raise ValueError( 

138 f"sequence length {tensor.size(dim)} is not divisible by " 

139 f"CP size {context.size}") 

140 length = tensor.size(dim) // context.size 

141 return tensor.narrow( 

142 dim, context.rank * length, length).contiguous() 

143 

144 

145def _global_seq_len(actual, length, device): 

146 if actual is None: 

147 return [length] 

148 if isinstance(actual, Tensor): 

149 return actual.to(device=device, dtype=torch.int32) 

150 return actual 

151 

152 

153def _slice_sink(tensor, context): 

154 if tensor is None or tensor.size(2) == 1: 

155 return tensor 

156 if tensor.size(2) % context.size: 

157 raise ValueError( 

158 f"parameter-sink heads {tensor.size(2)} are not divisible by " 

159 f"CP size {context.size}") 

160 count = tensor.size(2) // context.size 

161 start = context.rank * count 

162 return tensor[:, :, start:start + count].contiguous() 

163 

164 

165def _normalize_dim(dim: int, ndim: int) -> int: 

166 """Normalize and validate a tensor dimension.""" 

167 normalized = dim + ndim if dim < 0 else dim 

168 if normalized < 0 or normalized >= ndim: 

169 raise ValueError(f"dimension {dim} is out of range for a {ndim}D tensor") 

170 return normalized 

171 

172 

173def _reconstruct_all_to_all(output: Tensor, concat_dim: int) -> Tensor: 

174 """Move the source-rank axis next to ``concat_dim`` and merge it.""" 

175 rank_axis_position = concat_dim + 1 

176 permutation = ( 

177 list(range(1, rank_axis_position)) 

178 + [0] 

179 + list(range(rank_axis_position, output.dim())) 

180 ) 

181 reconstructed = output.permute(permutation).contiguous() 

182 shape = list(reconstructed.shape) 

183 merged = shape[concat_dim] * shape[concat_dim + 1] 

184 return reconstructed.reshape(shape[:concat_dim] + [merged] + shape[concat_dim + 2:]) 

185 

186 

187def _move_dim_to_front(tensor: Tensor, dim: int) -> Tensor: 

188 """Move one dimension to the leading communication dimension.""" 

189 dim = _normalize_dim(dim, tensor.dim()) 

190 if dim == 0: 

191 return tensor.contiguous() 

192 return tensor.movedim(dim, 0).contiguous() 

193 

194 

195def _move_dim_from_front(tensor: Tensor, dim: int) -> Tensor: 

196 """Restore a leading communication dimension to its original position.""" 

197 dim = _normalize_dim(dim, tensor.dim()) 

198 if dim == 0: 

199 return tensor.contiguous() 

200 return tensor.movedim(0, dim).contiguous() 

201 

202 

203def _prepare_ulysses_send( 

204 tensor: Tensor, 

205 scatter_dim: int, 

206 world_size: int, 

207) -> Tensor: 

208 """Split one tensor dimension and move the destination-rank axis first.""" 

209 ndim = tensor.dim() 

210 scatter_dim = _normalize_dim(scatter_dim, ndim) 

211 shape = list(tensor.shape) 

212 scatter_size = shape[scatter_dim] 

213 if scatter_size % world_size: 

214 raise ValueError( 

215 f"tensor dimension {scatter_dim} size ({scatter_size}) must be " 

216 f"divisible by Ulysses degree ({world_size})" 

217 ) 

218 split_shape = ( 

219 shape[:scatter_dim] 

220 + [world_size, scatter_size // world_size] 

221 + shape[scatter_dim + 1:] 

222 ) 

223 permutation = ( 

224 [scatter_dim] 

225 + list(range(scatter_dim)) 

226 + list(range(scatter_dim + 1, ndim + 1)) 

227 ) 

228 return tensor.contiguous().reshape(split_shape).permute(permutation).contiguous() 

229 

230 

231class _AsyncAllGatherWait(torch.autograd.Function): 

232 """Attach a pre-launched all-gather to autograd at its wait point.""" 

233 

234 @staticmethod 

235 def forward( 

236 ctx: Any, 

237 tensor: Tensor, 

238 work: Any, 

239 output: Tensor, 

240 group: Any, 

241 world_size: int, 

242 gather_dim: int, 

243 ) -> Tensor: 

244 """Wait for a launched all-gather and expose its autograd edge.""" 

245 del tensor 

246 ctx.group = group 

247 ctx.world_size = world_size 

248 ctx.gather_dim = gather_dim 

249 work.wait() 

250 return _move_dim_from_front(output, gather_dim) 

251 

252 @staticmethod 

253 def backward( 

254 ctx: Any, 

255 grad_output: Tensor, 

256 ) -> tuple[Tensor, None, None, None, None, None]: 

257 """SUM consumer gradients directly into each owner's sequence shard.""" 

258 send = _move_dim_to_front(grad_output, ctx.gather_dim) 

259 output = grad_output.new_empty((send.shape[0] // ctx.world_size, *send.shape[1:])) 

260 work = dist.reduce_scatter_tensor( 

261 output, send, op=dist.ReduceOp.SUM, group=ctx.group, async_op=True, 

262 ) 

263 work.wait() 

264 local = _move_dim_from_front(output, ctx.gather_dim) 

265 return ( 

266 local, 

267 None, 

268 None, 

269 None, 

270 None, 

271 None, 

272 ) 

273 

274 

275class _AsyncUlyssesWait(torch.autograd.Function): 

276 """Attach a pre-launched sequence-to-head A2A to autograd.""" 

277 

278 @staticmethod 

279 def forward( 

280 ctx: Any, 

281 tensor: Tensor, 

282 work: Any, 

283 output: Tensor, 

284 group: Any, 

285 world_size: int, 

286 seq_dim: int, 

287 head_dim: int, 

288 ) -> Tensor: 

289 """Wait for a launched A2A and expose its autograd edge.""" 

290 del tensor 

291 ctx.group = group 

292 ctx.world_size = world_size 

293 ctx.seq_dim = seq_dim 

294 ctx.head_dim = head_dim 

295 work.wait() 

296 return _reconstruct_all_to_all(output, seq_dim) 

297 

298 @staticmethod 

299 def backward( 

300 ctx: Any, 

301 grad_output: Tensor, 

302 ) -> tuple[Tensor, None, None, None, None, None, None]: 

303 """Apply the inverse A2A to restore the input-gradient layout.""" 

304 send = _prepare_ulysses_send( 

305 grad_output, 

306 scatter_dim=ctx.seq_dim, 

307 world_size=ctx.world_size, 

308 ) 

309 output = torch.empty( 

310 list(send.shape), device=send.device, dtype=send.dtype 

311 ) 

312 work = dist.all_to_all_single( 

313 output, send, group=ctx.group, async_op=True 

314 ) 

315 work.wait() 

316 grad_input = _reconstruct_all_to_all(output, ctx.head_dim) 

317 return grad_input, None, None, None, None, None, None 

318 

319 

320@dataclass 

321class AsyncCPCollective: 

322 """A local-tensor CP collective launched before its consumer.""" 

323 

324 tensor: Tensor 

325 work: Any 

326 output: Tensor 

327 group: Any 

328 world_size: int 

329 kind: str 

330 seq_dim: int 

331 head_dim: Optional[int] = None 

332 waited: bool = False 

333 

334 def wait(self) -> Tensor: 

335 """Materialize the collective result exactly once.""" 

336 if self.waited: 

337 raise RuntimeError("an async CP collective handle cannot be waited twice") 

338 self.waited = True 

339 if self.work is None: 

340 return self.tensor 

341 if self.kind == "allgather": 

342 return _AsyncAllGatherWait.apply( 

343 self.tensor, 

344 self.work, 

345 self.output, 

346 self.group, 

347 self.world_size, 

348 self.seq_dim, 

349 ) 

350 if self.kind == "ulysses": 

351 return _AsyncUlyssesWait.apply( 

352 self.tensor, 

353 self.work, 

354 self.output, 

355 self.group, 

356 self.world_size, 

357 self.seq_dim, 

358 self.head_dim, 

359 ) 

360 raise RuntimeError(f"unknown async CP collective kind {self.kind!r}") 

361 

362 

363def async_cp_allgather_launch( 

364 tensor: Tensor, 

365 gather_dim: int, 

366 cp_mesh: DeviceMesh, 

367) -> AsyncCPCollective: 

368 """Launch an asynchronous all-gather along gather_dim.""" 

369 world_size = cp_mesh.size() 

370 if world_size <= 1: 

371 return AsyncCPCollective( 

372 tensor, None, tensor, None, world_size, "allgather", gather_dim 

373 ) 

374 gather_dim = _normalize_dim(gather_dim, tensor.dim()) 

375 send = _move_dim_to_front(tensor.detach(), gather_dim) 

376 output_shape = list(send.shape) 

377 output_shape[0] *= world_size 

378 group = cp_mesh.get_group() 

379 output = torch.empty( 

380 output_shape, device=send.device, dtype=send.dtype 

381 ) 

382 work = dist.all_gather_into_tensor( 

383 output, send, group=group, async_op=True 

384 ) 

385 return AsyncCPCollective( 

386 tensor, work, output, group, world_size, "allgather", gather_dim 

387 ) 

388 

389 

390def async_ulysses_seq_to_head_launch( 

391 tensor: Tensor, 

392 seq_dim: int, 

393 head_dim: int, 

394 cp_mesh: DeviceMesh, 

395) -> AsyncCPCollective: 

396 """Launch an asynchronous Ulysses sequence-to-head A2A.""" 

397 world_size = cp_mesh.size() 

398 if world_size <= 1: 

399 return AsyncCPCollective( 

400 tensor, 

401 None, 

402 tensor, 

403 None, 

404 world_size, 

405 "ulysses", 

406 seq_dim, 

407 head_dim, 

408 ) 

409 seq_dim = _normalize_dim(seq_dim, tensor.dim()) 

410 head_dim = _normalize_dim(head_dim, tensor.dim()) 

411 if seq_dim == head_dim: 

412 raise ValueError("Ulysses seq_dim and head_dim must be different") 

413 send = _prepare_ulysses_send( 

414 tensor.detach(), scatter_dim=head_dim, world_size=world_size 

415 ) 

416 group = cp_mesh.get_group() 

417 output = torch.empty( 

418 list(send.shape), device=send.device, dtype=send.dtype 

419 ) 

420 work = dist.all_to_all_single( 

421 output, send, group=group, async_op=True 

422 ) 

423 return AsyncCPCollective( 

424 tensor, 

425 work, 

426 output, 

427 group, 

428 world_size, 

429 "ulysses", 

430 seq_dim, 

431 head_dim, 

432 ) 

433 

434 

435def _ulysses_all_to_all( 

436 tensor: Tensor, 

437 *, 

438 scatter_dim: int, 

439 gather_dim: int, 

440 cp_mesh: DeviceMesh, 

441) -> Tensor: 

442 """Split one tensor dimension and gather another through all-to-all.""" 

443 cp_size = cp_mesh.size() 

444 if cp_size <= 1: 

445 return tensor 

446 

447 ndim = tensor.dim() 

448 scatter_dim = _normalize_dim(scatter_dim, ndim) 

449 gather_dim = _normalize_dim(gather_dim, ndim) 

450 if scatter_dim == gather_dim: 

451 raise ValueError("Ulysses scatter_dim and gather_dim must be different") 

452 

453 shape = list(tensor.shape) 

454 scatter_size = shape[scatter_dim] 

455 if scatter_size % cp_size != 0: 

456 raise ValueError( 

457 f"tensor dimension {scatter_dim} size ({scatter_size}) must be " 

458 f"divisible by Ulysses degree ({cp_size})" 

459 ) 

460 

461 split_shape = ( 

462 shape[:scatter_dim] 

463 + [cp_size, scatter_size // cp_size] 

464 + shape[scatter_dim + 1:] 

465 ) 

466 split_ndim = ndim + 1 

467 permutation = ( 

468 [scatter_dim] 

469 + list(range(scatter_dim)) 

470 + list(range(scatter_dim + 1, split_ndim)) 

471 ) 

472 send = tensor.contiguous().reshape(split_shape).permute(permutation).contiguous() 

473 received = communication.differentiable_all_to_all( 

474 send, list(send.shape), cp_mesh.get_group()) 

475 return _reconstruct_all_to_all(received, gather_dim) 

476 

477 

478def ulysses_seq_to_head( 

479 tensor: Tensor, 

480 seq_dim: int, 

481 head_dim: int, 

482 cp_mesh: DeviceMesh, 

483) -> Tensor: 

484 """Convert a sequence shard into a full-sequence head shard. 

485 

486 Args: 

487 tensor: Local Q, K, or V tensor. 

488 seq_dim: Sequence dimension to gather across the CP group. 

489 head_dim: Head dimension to split across the CP group. 

490 cp_mesh: One-dimensional CP DeviceMesh. 

491 

492 Returns: 

493 Tensor with the sequence dimension gathered and head dimension sharded. 

494 """ 

495 return _ulysses_all_to_all( 

496 tensor, 

497 scatter_dim=head_dim, 

498 gather_dim=seq_dim, 

499 cp_mesh=cp_mesh, 

500 ) 

501 

502 

503def ulysses_head_to_seq( 

504 tensor: Tensor, 

505 seq_dim: int, 

506 head_dim: int, 

507 cp_mesh: DeviceMesh, 

508) -> Tensor: 

509 """Convert a full-sequence head shard back into a sequence shard. 

510 

511 Args: 

512 tensor: Local attention output in Ulysses head-sharded layout. 

513 seq_dim: Full sequence dimension to split across the CP group. 

514 head_dim: Head dimension to gather across the CP group. 

515 cp_mesh: One-dimensional CP DeviceMesh. 

516 

517 Returns: 

518 Tensor restored to the original local sequence-sharded layout. 

519 """ 

520 return _ulysses_all_to_all( 

521 tensor, 

522 scatter_dim=seq_dim, 

523 gather_dim=head_dim, 

524 cp_mesh=cp_mesh, 

525 ) 

526 

527 

528def _validate_ulysses_head_counts( 

529 query: Tensor, key: Tensor, value: Tensor, 

530 ulysses_degree: int, context: str) -> None: 

531 """Require every Q/K/V head dimension to be shardable by Ulysses.""" 

532 head_counts = { 

533 "query": query.shape[1], 

534 "key": key.shape[1], 

535 "value": value.shape[1], 

536 } 

537 invalid = { 

538 name: count 

539 for name, count in head_counts.items() 

540 if count % ulysses_degree 

541 } 

542 if invalid: 

543 details = ", ".join( 

544 f"{name}_heads={count}" for name, count in invalid.items() 

545 ) 

546 raise ValueError( 

547 f"{context} requires every Q/K/V head count to be divisible by " 

548 f"ulysses_degree ({ulysses_degree}), got {details}" 

549 ) 

550 

551 

552class _AllGatherAlongDim(torch.autograd.Function): 

553 """all-gather along cp_dim + backward reduce-scatter semantics (sum across 

554 ranks, then take this rank's chunk).""" 

555 

556 @staticmethod 

557 def forward( 

558 ctx: Any, 

559 t: Tensor, 

560 cp_dim: int, 

561 group: Any, 

562 cp_size: int, 

563 ) -> Tensor: 

564 """All-gather the tensor along cp_dim in CP-rank order.""" 

565 ctx.cp_dim = cp_dim 

566 ctx.group = group 

567 ctx.cp_size = cp_size 

568 world_t = [torch.empty_like(t) for _ in range(cp_size)] 

569 dist.all_gather(world_t, t.contiguous(), group=group) 

570 # cat in cp_rank order: [chunk_rank0, chunk_rank1, ...] 

571 return torch.cat(world_t, dim=cp_dim) 

572 

573 @staticmethod 

574 def backward( 

575 ctx: Any, 

576 grad_output: Tensor, 

577 ) -> tuple[Tensor, None, None, None]: 

578 """Sum the gradient across ranks and take this rank's chunk.""" 

579 # reduce-scatter: sum the gradient across ranks, take this rank's chunk 

580 grad = grad_output.contiguous().clone() 

581 dist.all_reduce(grad, group=ctx.group) 

582 rank = dist.get_rank(ctx.group) 

583 local = torch.chunk(grad, ctx.cp_size, dim=ctx.cp_dim)[rank] 

584 return local.contiguous(), None, None, None 

585 

586 

587def flex_cp_allgather( 

588 k: Tensor, 

589 v: Tensor, 

590 cp_dim: int, 

591 cp_mesh: DeviceMesh, 

592) -> tuple[Tensor, Tensor]: 

593 """All-gather K/V along CP dimension for context parallel attention. 

594 

595 Forward: all-gather K/V along cp_dim (each rank ends up holding the full K/V). 

596 Backward: reduce-scatter semantics (gradients summed across ranks, then this 

597 rank's chunk is taken -- implemented explicitly by the _AllGatherAlongDim 

598 autograd.Function, since plain ``dist.all_gather`` has no autograd kernel). 

599 

600 Args: 

601 k, v: [B, N, S_local, H] (cp_dim=2 is the sequence dim). 

602 cp_dim: the gather dimension. 

603 cp_mesh: DeviceMesh of the CP dim. The communication group is taken from 

604 ``cp_mesh.get_group()`` -- already created and cached at DeviceMesh 

605 construction; **dist.new_group must NOT be called here** (otherwise 

606 every forward leaks one process group, and the semantics would be 

607 misaligned). 

608 """ 

609 cp_size = cp_mesh.size() 

610 if cp_size <= 1: 

611 return k, v 

612 group = cp_mesh.get_group() 

613 return (_AllGatherAlongDim.apply(k, cp_dim, group, cp_size), 

614 _AllGatherAlongDim.apply(v, cp_dim, group, cp_size)) 

615 

616 

617def _build_hybrid_cp_submeshes(cp_mesh, ulysses_degree: int): 

618 """Build cached communication submeshes for local-tensor Hybrid CP.""" 

619 if cp_mesh is None or cp_mesh.size() <= 1: 

620 raise ValueError("Hybrid CP requires an active CP mesh") 

621 if isinstance(ulysses_degree, bool) or not isinstance(ulysses_degree, int): 

622 raise TypeError( 

623 "Hybrid CP requires integer ulysses_degree, got " 

624 f"{type(ulysses_degree).__name__}" 

625 ) 

626 cp_size = cp_mesh.size() 

627 if not 1 < ulysses_degree < cp_size: 

628 raise ValueError( 

629 "Hybrid CP requires 1 < ulysses_degree < cp_size, got " 

630 f"ulysses_degree={ulysses_degree}, cp_size={cp_size}" 

631 ) 

632 if cp_size % ulysses_degree: 

633 raise ValueError( 

634 f"cp_size ({cp_size}) must be divisible by ulysses_degree " 

635 f"({ulysses_degree})" 

636 ) 

637 

638 cache_key = (cp_mesh.to_hash(), ulysses_degree) 

639 cached = _HYBRID_MESH_CACHE.get(cache_key) 

640 if cached is not None: 

641 return cached 

642 

643 colossal_degree = cp_size // ulysses_degree 

644 if cp_mesh.ndim == 2: 

645 hybrid_mesh = cp_mesh 

646 elif (cp_mesh.root_mesh is not None 

647 and cp_mesh.mesh_dim_names == ("cp",)): 

648 hybrid_mesh = cp_mesh._unflatten( # pylint: disable=protected-access 

649 "cp", 

650 (colossal_degree, ulysses_degree), 

651 ("colossal", "ulysses"), 

652 ) 

653 else: 

654 ranks = list(cp_mesh.rank_list) 

655 hybrid_mesh = DeviceMesh( 

656 cp_mesh.device_type, 

657 [ 

658 ranks[index * ulysses_degree:(index + 1) * ulysses_degree] 

659 for index in range(colossal_degree) 

660 ], 

661 mesh_dim_names=("colossal", "ulysses"), 

662 ) 

663 dim_names = hybrid_mesh.mesh_dim_names 

664 if not dim_names or len(dim_names) != 2: 

665 raise ValueError("Hybrid CP requires a named two-dimensional submesh") 

666 colossal_mesh = hybrid_mesh[dim_names[0]] 

667 ulysses_mesh = hybrid_mesh[dim_names[1]] 

668 result = (ulysses_mesh, colossal_mesh) 

669 _HYBRID_MESH_CACHE[cache_key] = result 

670 return result 

671 

672 

673def hybrid_cp_attention( 

674 attention_fn: Callable[[Tensor, Tensor, Tensor, dict[str, Any]], Any], 

675 query: Tensor, key: Tensor, value: Tensor, 

676 attention_kwargs: dict[str, Any], cp_mesh: Any, 

677 ulysses_degree: int) -> Any: 

678 """Run local-tensor Hybrid communication around an attention callable. 

679 

680 ``attention_kwargs`` are forwarded unchanged. Mask construction and other 

681 model-input semantics belong to the caller's input-preparation contract. 

682 """ 

683 _validate_ulysses_head_counts( 

684 query, key, value, ulysses_degree, "Hybrid CP" 

685 ) 

686 ulysses_mesh, colossal_mesh = ( 

687 _build_hybrid_cp_submeshes(cp_mesh, ulysses_degree) 

688 ) 

689 local_shape = tuple(query.shape) 

690 query, key, value = ( 

691 ulysses_seq_to_head(tensor, 2, 1, ulysses_mesh) 

692 for tensor in (query, key, value) 

693 ) 

694 key, value = flex_cp_allgather(key, value, 2, colossal_mesh) 

695 output = attention_fn(query, key, value, attention_kwargs) 

696 output = ulysses_head_to_seq(output, 2, 1, ulysses_mesh) 

697 if tuple(output.shape) != local_shape: 

698 raise RuntimeError( 

699 f"Hybrid CP output shape {tuple(output.shape)} does not restore " 

700 f"the local query shape {local_shape}" 

701 ) 

702 return output