Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / dtensor / layout.py: 86%

478 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-21 04:29 +0800

1# Copyright 2025 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"""layout""" 

16 

17import copy 

18import functools 

19from typing import NamedTuple, Optional, Sequence 

20 

21import numpy as np 

22 

23 

24from hyper_parallel.core.dtensor.placement_types import ( 

25 Partial, 

26 Placement, 

27 RaggedShard, 

28 Replicate, 

29 Shard, 

30 StridedShard, 

31) 

32from hyper_parallel.core.dtensor.device_mesh import DeviceMesh, _create_device_mesh 

33from hyper_parallel.platform import get_platform 

34 

35platform = get_platform() 

36 

37 

38class RaggedShardInfo(NamedTuple): 

39 """RaggedShard placement and its corresponding mesh dimension.""" 

40 

41 mesh_dim: int 

42 placement: RaggedShard 

43 

44 

45def _extract_ragged_shard(placements: Sequence[Placement]) -> Optional[RaggedShardInfo]: 

46 """Extract the single RaggedShard placement from a placement sequence.""" 

47 ragged_shard = None 

48 for mesh_dim, placement in enumerate(placements): 

49 if not placement.is_ragged_shard(): 

50 continue 

51 if ragged_shard is not None: 

52 raise ValueError( 

53 "Layout supports at most one RaggedShard placement, " 

54 f"but got placements={tuple(placements)!r}" 

55 ) 

56 ragged_shard = RaggedShardInfo(mesh_dim, placement) 

57 return ragged_shard 

58 

59 

60def _replace_ragged_with_replicate(placements: Sequence[Placement]) -> tuple[Placement, ...]: 

61 """Return placements with RaggedShard represented as Replicate.""" 

62 return tuple( 

63 Replicate() if placement.is_ragged_shard() else placement 

64 for placement in placements 

65 ) 

66 

67 

68def _infer_slice_area_by_rank(mesh_shape, tensor_map, rank_id: int, full_shape: tuple): # -> tuple[tuple[int]]: 

69 """Return the range of each axis from full tensor for slice in current rank.""" 

70 

71 def _get_dev_num_alone_dim(mesh_shape, dim): 

72 """_get_dev_num_alone_dim.""" 

73 return mesh_shape[-dim - 1] if dim != -1 else 1 

74 

75 def _rank_id_to_dev_id_list(mesh_shape, rank_id): 

76 """Infer dev id list by rank_id and mesh_shape""" 

77 dims = len(mesh_shape) 

78 dev_id_list = [0] * dims 

79 for i in range(dims - 1, -1, -1): 

80 dev_id_list[i] = rank_id % mesh_shape[i] 

81 rank_id = rank_id // mesh_shape[i] 

82 return dev_id_list 

83 

84 dev_id_list = _rank_id_to_dev_id_list(mesh_shape, rank_id) 

85 

86 dims = len(full_shape) 

87 area = [] 

88 for axis in range(dims): 

89 mapping = tensor_map[axis] 

90 if isinstance(mapping, int): 

91 mapping = (mapping,) 

92 split_num = 1 

93 for dim in mapping: 

94 split_num *= _get_dev_num_alone_dim(mesh_shape, dim) 

95 

96 slice_id = 0 

97 coef = 1 

98 for dim in reversed(mapping): 

99 if dim == -1: 

100 continue 

101 slice_id += dev_id_list[-dim - 1] * coef 

102 coef *= _get_dev_num_alone_dim(mesh_shape, dim) 

103 slice_size = full_shape[axis] // split_num 

104 start = slice_id * slice_size 

105 end = start + slice_size 

106 area.append((start, end)) 

107 return area 

108 

109 

110def _get_slice_tensor_by_layout(global_tensor, layout): 

111 """Transfer global tensor to local tensor by layout""" 

112 inner_rank_id = layout.rank_list.index(layout.mesh.rank) 

113 slice_area = _infer_slice_area_by_rank(layout.mesh_shape, layout.tensor_map, inner_rank_id, global_tensor.shape) 

114 

115 def get_slice_data(full_data, offset): 

116 area = () 

117 for begin, end in offset: 

118 area += (slice(begin, end),) 

119 return full_data[area].clone() 

120 

121 local_tensor = get_slice_data(global_tensor, slice_area) 

122 return local_tensor 

123 

124 

125def _infer_slice_shape_by_layout(global_shape, layout): 

126 """Infer slice shape from global_shape and layout""" 

127 slice_shape = list(global_shape) 

128 alias_tensor_map = layout.alias_tensor_map 

129 for i in range(len(global_shape)): 

130 axis_name = alias_tensor_map[i] 

131 if isinstance(axis_name, str): 

132 axis_name = (axis_name,) 

133 for sub_axis_name in axis_name: 

134 if sub_axis_name != "None": 

135 slice_shape[i] = slice_shape[i] // layout.mesh.get_device_num_along_axis(sub_axis_name) 

136 return slice_shape 

137 

138 

139class Layout: 

140 """ 

141 Topological abstraction describing cluster devices for tensor slice placement on the cluster. 

142 

143 Note: 

144 - It is valid only in semi auto parallel or auto parallel mode. 

145 - The multiplication result of the `mesh_shape` must be equal to the device count in a pipeline stage. 

146 - When the layout function is invoked to constructs a sharding strategy, each alias name is only allowed to be 

147 used once to shard a tensor. 

148 

149 Args: 

150 mesh_shape (tuple): Describe the shape of devices arrangement, its element type is int. 

151 alias_name (tuple): The alias name for each axis of mesh_shape, its length shoits element type is string. 

152 When using "interleaved_parallel" as an alias name, the tensor would be split into multiple 

153 copies on the corresponding partition dimension on a single card. 

154 rank_list (tuple, optional): Data is allocated to the device according to rank_list. Default: ``None``. 

155 

156 Raises: 

157 TypeError: `mesh_shape` is not a tuple type. 

158 TypeError: `alias_name` is not a tuple type. 

159 TypeError: 'rank_list' is not a list type. 

160 ValueError: `mesh_shape` length is not equal to `alias_name` length. 

161 TypeError: The element of `mesh_shape` is not int type. 

162 TypeError: The element of `alias_name` is not a str type. 

163 TypeError: The element of `rank_list` is not int type. 

164 ValueError: The element of `alias_name` is an empty str. 

165 ValueError: The element of `alias_name` is "None". 

166 ValueError: `alias_name` contains repeated element. 

167 

168 Supported Platforms: 

169 ``Ascend`` 

170 

171 Examples: 

172 >>> from mindspore.parallel import Layout 

173 >>> layout = Layout((2, 2, 2), ("dp", "sp", "mp")) 

174 >>> layout0 = layout("dp", "mp") 

175 >>> print(layout0.to_dict()) 

176 {"mesh_shape": (2, 2, 2), "tensor_map": (2, 0), "interleaved_parallel": False, 

177 'alias_name': {'dp', 'sp', 'mp'}, "rank_list": [0, 1, 2, 3, 4, 5, 6, 7]} 

178 >>> layout = Layout((2, 2, 2), ("dp", "sp", "interleaved_parallel")) 

179 >>> layout1 = layout(("dp", "interleaved_parallel"), "sp") 

180 """ 

181 

182 def __init__(self, mesh_shape, alias_name, rank_list=None, init_backend=True): 

183 self._alias_name = alias_name 

184 self._tensor_map = None 

185 if not rank_list: 

186 self._rank_list = tuple(range(np.prod(np.array(mesh_shape)))) 

187 else: 

188 self._rank_list = tuple(rank_list) 

189 self._partial = [None] * len(mesh_shape) # partial status for each dev dim 

190 self._support_partial_op = ['sum', 'max', 'min', 'avg', 'prod', 'all', None] 

191 self._alias_tensor_map = None 

192 self._mesh = _create_device_mesh("npu", mesh_shape, mesh_dim_names=alias_name, rank_list=self._rank_list, 

193 init_backend=init_backend) 

194 self._compact_str = self._to_compact_string() 

195 self._placements = None 

196 self.partial_ops = {} # Initialized in _build_dim_map_from_placements() 

197 self._ragged_shard = None 

198 

199 @classmethod 

200 def from_device_mesh(cls, device_mesh: DeviceMesh) -> 'Layout': 

201 """ 

202 Create a Layout from an existing DeviceMesh. 

203 

204 Args: 

205 device_mesh (DeviceMesh): The device mesh to create layout from. 

206 

207 Returns: 

208 Layout: A new Layout instance initialized with the properties of the provided device mesh. 

209 

210 Examples: 

211 >>> from hyper_parallel.core.dtensor.layout import Layout, DeviceMesh 

212 >>> device_mesh = DeviceMesh("npu", (2, 2), mesh_dim_names=("dp", "mp")) 

213 >>> layout = Layout.from_device_mesh(device_mesh) 

214 """ 

215 obj = cls.__new__(cls) 

216 obj._mesh = device_mesh 

217 obj._alias_name = device_mesh.mesh_dim_names 

218 obj._rank_list = device_mesh.rank_list 

219 obj._tensor_map = None 

220 obj._partial = [None] * len(device_mesh.mesh_shape) 

221 obj._support_partial_op = ['sum', 'max', 'min', 'avg', 'prod', 'all', None] 

222 obj._alias_tensor_map = None 

223 obj._placements = None 

224 obj._ragged_shard = None 

225 obj._compact_str = obj._to_compact_string() 

226 return obj 

227 

228 def __call__(self, *alias_tensor_map): 

229 obj = copy.deepcopy(self) 

230 

231 # Clear the inherited partial status. 

232 # When creating a new layout mapping configuration via __call__, 

233 # it should not inherit the dynamic execution state (Partial) of the original layout. 

234 # If the user intends to create a Partial placement, it will be parsed from alias_tensor_map. 

235 obj._partial = [None] * len(obj.mesh_shape) 

236 

237 if len(alias_tensor_map) == 1 and isinstance(alias_tensor_map[0], (list, tuple)): 

238 if len(alias_tensor_map[0]) > 0 and isinstance(alias_tensor_map[0][0], Placement): 

239 return self._process_placement_layout(obj, alias_tensor_map[0]) 

240 

241 if len(alias_tensor_map) > 0 and isinstance(alias_tensor_map[0], Placement): 

242 return self._process_placement_layout(obj, alias_tensor_map) 

243 

244 return self._process_alias_layout(obj, alias_tensor_map) 

245 

246 def __deepcopy__(self, memo): 

247 """Deep copy layout without rebuilding the underlying device mesh.""" 

248 cls = self.__class__ 

249 result = cls.__new__(cls) 

250 memo[id(self)] = result 

251 for k, v in self.__dict__.items(): 

252 setattr(result, k, copy.deepcopy(v, memo)) 

253 return result 

254 

255 @staticmethod 

256 def _process_placement_layout(obj, placements): 

257 """Process layout defined by Placement types.""" 

258 obj.set_placements(placements) 

259 return copy.deepcopy(obj) 

260 

261 @staticmethod 

262 def _process_alias_layout(obj, alias_tensor_map): 

263 """Process layout defined by alias strings.""" 

264 obj.set_alias_tensor_map(alias_tensor_map) 

265 tensor_map = () 

266 writed_map = () 

267 for ele in alias_tensor_map: 

268 if isinstance(ele, tuple): 

269 ele_map = () 

270 for item in ele: 

271 if item == "None": 

272 ele_map += (-1,) 

273 continue 

274 if item not in obj.alias_name: 

275 raise ValueError(f'The axis {item} is not found in {obj.alias_name}') 

276 if item in writed_map: 

277 raise ValueError(f'The axis {item} has been set more than one in {obj.alias_name}') 

278 ele_map += (len(obj.alias_name) - 1 - obj.alias_name.index(item),) 

279 writed_map += (item,) 

280 tensor_map += (ele_map,) 

281 continue 

282 if ele == "None": 

283 tensor_map += (-1,) 

284 continue 

285 if ele not in obj.alias_name: 

286 raise ValueError(f'The axis {ele} is not found in {obj.alias_name}') 

287 if ele in writed_map: 

288 raise ValueError(f'The axis {ele} has been set more than one in {obj.alias_name}') 

289 tensor_map += (len(obj.alias_name) - 1 - obj.alias_name.index(ele),) 

290 writed_map += (ele,) 

291 obj.set_tensor_map(tensor_map) 

292 obj.tensor_map_to_placement() 

293 obj.update_compact_str() 

294 return copy.deepcopy(obj) 

295 

296 def to_dict(self): 

297 """ 

298 Transform layout to a dictionary. 

299 """ 

300 if self._mesh.mesh_shape is None: 

301 raise ValueError("The device_shape of layout is None") 

302 if self._tensor_map is None: 

303 raise ValueError("The tensor_map of layout is None") 

304 interleaved_parallel = "interleaved_parallel" in self._mesh.mesh_dim_names 

305 return {"mesh_shape": self._mesh.mesh_shape, "tensor_map": self._tensor_map, 

306 "interleaved_parallel": interleaved_parallel, "alias_name": self._mesh.mesh_dim_names, 

307 "rank_list": self._rank_list} 

308 

309 def placement_to_tensor_map(self, dim): 

310 """ 

311 Transform placement to tensor map. 

312 

313 This method converts the `placements` configuration (consisting of Shard, StridedShard, 

314 Replicate, Partial) 

315 into a `tensor_map` representation used for distributed tensor operations. 

316 

317 Args: 

318 dim (int): The dimension of the tensor. Must be a positive integer. 

319 

320 Returns: 

321 tuple: A tuple representing the tensor map, where each element corresponds to a tensor dimension. 

322 A value of -1 indicates the dimension is not sharded, an integer indicates the mesh 

323 dimension index along which the tensor dimension is sharded, and a tuple indicates 

324 that the same tensor dimension is sharded multiple times in order. 

325 

326 Raises: 

327 ValueError: If `dim` is negative. 

328 ValueError: If a shard dimension in `placements` is out of bounds for the given tensor dimension. 

329 """ 

330 if dim < 0: 

331 raise ValueError(f"Tensor dimension must be positive, but got {dim}") 

332 if dim == 0: 

333 return self._handle_zero_dim_placement() 

334 

335 dim_map = self._build_dim_map_from_placements(dim) 

336 tensor_map = self._convert_dim_map_to_tensor_map(dim_map) 

337 self.set_tensor_map(tuple(tensor_map)) 

338 self._alias_tensor_map = self._build_readable_tensor_map() 

339 self.update_compact_str() 

340 return tensor_map 

341 

342 def _handle_zero_dim_placement(self): 

343 """Handle the special case of zero-dimensional tensor.""" 

344 self.set_tensor_map(()) 

345 self._alias_tensor_map = () 

346 for mesh_idx, placement in enumerate(self.normal_placements): 

347 if isinstance(placement, Partial): 

348 self._partial[mesh_idx] = self._extract_reduce_op(placement) 

349 return [] 

350 

351 def _build_dim_map_from_placements(self, dim): 

352 """Build dimension map from placements.""" 

353 dim_map = [-1] * dim 

354 self.partial_ops = {} 

355 for mesh_idx, placement in enumerate(self.normal_placements): 

356 if isinstance(placement, Shard): 

357 shard_dim = placement.dim 

358 if shard_dim < -dim or shard_dim >= dim: 

359 raise ValueError(f"Shard dimension {shard_dim} is out of bounds for tensor of dimension {dim}") 

360 if shard_dim < 0: 

361 shard_dim += dim 

362 if dim_map[shard_dim] == -1: 

363 dim_map[shard_dim] = [mesh_idx] 

364 else: 

365 dim_map[shard_dim].append(mesh_idx) 

366 elif isinstance(placement, Partial): 

367 self._partial[mesh_idx] = self._extract_reduce_op(placement) 

368 self._validate_strided_shard_split_factor(dim_map) 

369 self._reorder_dim_map_for_strided_shard(dim_map) 

370 return dim_map 

371 

372 @staticmethod 

373 def _placement_split_factor(placement): 

374 """Return the effective split factor carried by a placement.""" 

375 return placement.split_factor if isinstance(placement, StridedShard) else 1 

376 

377 @staticmethod 

378 def _build_order_positions(shard_order): 

379 """Build a mesh axis to order position mapping.""" 

380 return {mesh_idx: order_idx for order_idx, mesh_idx in enumerate(shard_order)} 

381 

382 def _compute_expected_split_factors(self, shard_axes, shard_order): 

383 """Infer the split_factor each mesh axis should carry for the given sharding order.""" 

384 order_positions = self._build_order_positions(shard_order) 

385 expected_split_factors = {} 

386 for mesh_idx in shard_axes: 

387 split_factor = 1 

388 for right_mesh_idx in shard_axes: 

389 if right_mesh_idx <= mesh_idx: 

390 continue 

391 if order_positions[right_mesh_idx] < order_positions[mesh_idx]: 

392 split_factor *= self.mesh_shape[right_mesh_idx] 

393 expected_split_factors[mesh_idx] = split_factor 

394 return expected_split_factors 

395 

396 def _get_effective_shard_axes(self, shard_axes): 

397 """Return shard axes ordered by their effective sharding order.""" 

398 return sorted( 

399 shard_axes, 

400 key=lambda mesh_idx: self._placement_split_factor(self.placements[mesh_idx]), 

401 ) 

402 

403 def _reorder_dim_map_for_strided_shard(self, dim_map): 

404 """Reorder dim_map entries to reflect the effective sharding order.""" 

405 for i, shard_axes in enumerate(dim_map): 

406 if shard_axes == -1 or len(shard_axes) <= 1: 

407 continue 

408 dim_map[i] = self._get_effective_shard_axes(shard_axes) 

409 

410 def _validate_strided_shard_split_factor(self, dim_map): 

411 """Validate that split factors match the effective sharding order.""" 

412 for shard_axes in dim_map: 

413 if shard_axes == -1: 

414 continue 

415 shard_order = self._get_effective_shard_axes(shard_axes) 

416 expected_split_factors = self._compute_expected_split_factors( 

417 shard_axes, shard_order 

418 ) 

419 for mesh_idx in shard_axes: 

420 placement = self.placements[mesh_idx] 

421 actual_split_factor = self._placement_split_factor(placement) 

422 expected_split_factor = expected_split_factors[mesh_idx] 

423 if actual_split_factor != expected_split_factor: 

424 raise ValueError( 

425 f"StridedShard split_factor mismatch on mesh axis {mesh_idx}: " 

426 f"expected {expected_split_factor}, got {actual_split_factor}." 

427 ) 

428 

429 @staticmethod 

430 def _extract_reduce_op(placement): 

431 """Extract reduce operation name from Partial placement.""" 

432 op_name = getattr(placement, "reduce_op", "sum") 

433 if isinstance(op_name, str): 

434 op_name = op_name.lower() 

435 return op_name 

436 

437 def _convert_dim_map_to_tensor_map(self, dim_map): 

438 """Convert dimension map to tensor map format.""" 

439 device_dim_count = len(self.mesh_shape) 

440 tensor_map = [] 

441 for mesh_idx in dim_map: 

442 if mesh_idx == -1: 

443 tensor_map.append(-1) 

444 continue 

445 mapped_axes = tuple(device_dim_count - 1 - axis for axis in mesh_idx) 

446 tensor_map.append(mapped_axes[0] if len(mapped_axes) == 1 else mapped_axes) 

447 return tensor_map 

448 

449 def _build_readable_tensor_map(self): 

450 """Build human-readable alias tensor map from tensor_map.""" 

451 mesh_dim_names = self._mesh.mesh_dim_names 

452 has_names = mesh_dim_names is not None 

453 

454 def _map_dim(dim): 

455 """convert dimension index to dimension name.""" 

456 if dim == -1: 

457 return "None" 

458 if not has_names: 

459 return f"dim_{dim}" 

460 return mesh_dim_names[len(mesh_dim_names) - 1 - dim] 

461 

462 readable_map = [] 

463 for item in self._tensor_map: 

464 if isinstance(item, tuple): 

465 mapped_tuple = tuple(_map_dim(dim) for dim in item) 

466 readable_map.append(mapped_tuple) 

467 else: 

468 readable_map.append(_map_dim(item)) 

469 return tuple(readable_map) 

470 

471 def tensor_map_to_placement(self): 

472 """ 

473 Transform tensor map to placement. 

474 

475 This method converts the existing `tensor_map` and `partial` status into a list of `Placement` objects 

476 (Shard, StridedShard, Replicate, Partial). This is the inverse operation of 

477 `placement_to_tensor_map`. 

478 

479 Returns: 

480 list[Placement]: A list of Placement objects describing the distribution strategy for each 

481 dimension of the device mesh. 

482 

483 Raises: 

484 ValueError: If `tensor_map` is not configured (None). 

485 """ 

486 if self._tensor_map is None: 

487 raise ValueError("The tensor_map is None, cannot transform to placements.") 

488 mesh_ndim = len(self.mesh_shape) 

489 placements = [Replicate()] * mesh_ndim 

490 for tensor_dim, mapping in enumerate(self._tensor_map): 

491 mapping_list = mapping if isinstance(mapping, tuple) else (mapping,) 

492 valid_mapping = [map_val for map_val in mapping_list if map_val != -1] 

493 mesh_indices = [mesh_ndim - 1 - map_val for map_val in valid_mapping] 

494 shard_axes = sorted(mesh_indices) 

495 expected_split_factors = self._compute_expected_split_factors( 

496 shard_axes, mesh_indices 

497 ) 

498 for mesh_idx in shard_axes: 

499 split_factor = expected_split_factors[mesh_idx] 

500 placement = ( 

501 StridedShard(dim=tensor_dim, split_factor=split_factor) 

502 if split_factor > 1 

503 else Shard(dim=tensor_dim) 

504 ) 

505 placements[mesh_idx] = placement 

506 for mesh_idx, op in enumerate(self.partial): 

507 if op is not None: 

508 placements[mesh_idx] = Partial(reduce_op=op) 

509 if self._ragged_shard is not None: 

510 placements[self._ragged_shard.mesh_dim] = self._ragged_shard.placement 

511 self.set_placements(placements) 

512 self._alias_tensor_map = self._build_readable_tensor_map() 

513 self.update_compact_str() 

514 return placements 

515 

516 def __setstate__(self, state): 

517 self.__dict__.update(state) 

518 self.update_mesh(init_backend=False) 

519 

520 @property 

521 def mesh(self): 

522 """ 

523 Get the device mesh associated with this layout. 

524 

525 Returns: 

526 DeviceMesh: The device mesh describing the device topology. 

527 """ 

528 return self._mesh 

529 

530 def update_mesh(self, init_backend: bool = True): 

531 """Recreate the internal DeviceMesh from current layout properties. 

532 

533 Args: 

534 init_backend (bool): Whether to initialize communication backend 

535 (process groups). Set to ``False`` during deserialization to 

536 avoid creating process groups with a stale rank_list from the 

537 sender side. Default ``True``. 

538 """ 

539 self._mesh = _create_device_mesh("npu", self.mesh_shape, mesh_dim_names=self.alias_name, 

540 rank_list=self.rank_list, init_backend=init_backend) 

541 

542 @property 

543 def rank_list(self): 

544 """ 

545 Get the list of ranks participating in this layout. 

546 

547 Returns: 

548 tuple[int]: The rank list. 

549 """ 

550 return self._rank_list 

551 

552 @rank_list.setter 

553 def rank_list(self, val): 

554 self._rank_list = val 

555 

556 @property 

557 def mesh_shape(self): 

558 """mesh shape""" 

559 return self._mesh.mesh_shape 

560 

561 @property 

562 def alias_name(self): 

563 """alias name""" 

564 return self._mesh.mesh_dim_names 

565 

566 @property 

567 def alias_tensor_map(self): 

568 """Return the human-readable alias tensor map for this layout.""" 

569 return self._alias_tensor_map 

570 

571 @property 

572 def alias_placements(self): 

573 """Return alias_tensor_map when it contains multi-axis tuples, otherwise placements. 

574 

575 alias_tensor_map preserves multi-axis ordering information 

576 (e.g., (("dp", "tp"), "None") vs (("tp", "dp"), "None")) 

577 that Placement objects cannot represent, since both map to 

578 [Shard(0), Shard(0)]. 

579 

580 For single-axis layouts, Placement objects are preferred because they 

581 also carry Partial status which alias_tensor_map cannot encode. 

582 

583 Use this property when constructing DTensors from an existing Layout 

584 to avoid the lossy Placement round-trip for multi-axis cases. 

585 """ 

586 if self._ragged_shard is not None: 

587 return self._placements 

588 if self._alias_tensor_map is not None and any( 

589 isinstance(item, tuple) for item in self._alias_tensor_map 

590 ): 

591 return self._alias_tensor_map 

592 return self._placements 

593 

594 def set_alias_tensor_map(self, alias_tensor_map): 

595 """Set alias_tensor_map""" 

596 self._alias_tensor_map = alias_tensor_map 

597 

598 @property 

599 def placements(self): 

600 """placements""" 

601 return self._placements 

602 

603 def set_placements(self, placements: Optional[Sequence[Placement]]) -> None: 

604 """Set placements and retain the RaggedShard omitted from tensor_map.""" 

605 self._placements = placements 

606 self._ragged_shard = ( 

607 None if placements is None else _extract_ragged_shard(placements) 

608 ) 

609 

610 @property 

611 def normal_placements(self) -> Optional[tuple[Placement, ...]]: 

612 """Return placements with RaggedShard represented as Replicate.""" 

613 if self._placements is None: 

614 return None 

615 return _replace_ragged_with_replicate(self._placements) 

616 

617 @property 

618 def ragged_shard(self) -> Optional[RaggedShardInfo]: 

619 """Return the RaggedShard placement and its mesh dimension, if present.""" 

620 return self._ragged_shard 

621 

622 @property 

623 def tensor_map(self): 

624 """tensor map""" 

625 return self._tensor_map 

626 

627 def set_tensor_map(self, tensor_map): 

628 """Set tensor_map.""" 

629 self._tensor_map = tensor_map 

630 

631 @property 

632 def partial(self): 

633 """partial status""" 

634 return self._partial 

635 

636 def set_partial_by_dev_axis(self, axis, op): 

637 """Set the partial status for the specified dev ID, means pending to do reduce by op.""" 

638 if op not in self._support_partial_op: 

639 raise ValueError(f"Partial op must be one of {self._support_partial_op}, but got {op}") 

640 if self.is_dev_axis_apply_shard(axis): 

641 raise ValueError("Partial dim must be replicate.") 

642 self._partial[self._mesh.axis_index(axis)] = op 

643 self.tensor_map_to_placement() 

644 self.update_compact_str() 

645 

646 def get_partial_by_dev_id(self, axis): 

647 """Get the partial status for the specified dev id""" 

648 return self.partial[self._mesh.axis_index(axis)] 

649 

650 def is_dev_axis_apply_shard(self, axis): 

651 """Return true if device axis is applying shard""" 

652 axis_id = self._mesh.axis_id(axis) 

653 

654 def flatten(input_x): 

655 flatten_res = [] 

656 for item in input_x: 

657 if isinstance(item, tuple): 

658 flatten_res.extend(flatten(item)) 

659 else: 

660 flatten_res.append(item) 

661 return flatten_res 

662 

663 flatten_tensor_map = flatten(self.tensor_map) 

664 return axis_id in flatten_tensor_map 

665 

666 def get_dev_axis_apply_shard_axis(self, axis): 

667 """Return the axis which be split by axis. If axis not be apply to shard, return None.""" 

668 for dim, dim_map in enumerate(self.alias_tensor_map): 

669 if (isinstance(dim_map, tuple) and axis in dim_map) or axis == dim_map: 

670 return dim 

671 return None 

672 

673 def reset_partial(self): 

674 """Clear all partial statuses and regenerate placements from the tensor map.""" 

675 self._partial = [None] * len(self.mesh_shape) 

676 self.tensor_map_to_placement() 

677 self.update_compact_str() 

678 

679 def is_partial(self): 

680 """Return true if any dim in mesh_shape is partial""" 

681 return any(self.partial) 

682 

683 def get_dim_split_num(self, tensor_dim: int) -> int: 

684 """Return the total shard count for ``tensor_dim`` via alias_tensor_map. 

685 

686 Args: 

687 tensor_dim: Tensor dimension index to check. 

688 

689 Returns: 

690 Number of shards (1 if not sharded or no alias_tensor_map set). 

691 """ 

692 alias_tm = self.alias_tensor_map 

693 if alias_tm is None or tensor_dim >= len(alias_tm): 

694 return 1 

695 dim_entry = alias_tm[tensor_dim] 

696 if dim_entry == 'None': 

697 return 1 

698 if isinstance(dim_entry, str): 

699 return self.mesh.get_device_num_along_axis(dim_entry) 

700 if isinstance(dim_entry, tuple): 

701 total = 1 

702 for axis in dim_entry: 

703 if axis != 'None': 

704 total *= self.mesh.get_device_num_along_axis(axis) 

705 return total 

706 return 1 

707 

708 def get_split_id(self, tensor_dim: int) -> int: 

709 """Return this rank's global position among all shards of ``tensor_dim``. 

710 

711 For a single sharding axis, returns the rank's position within that axis group. 

712 For multiple sharding axes (e.g. dp+cp both sharding T1), computes the combined 

713 global position as a mixed-radix number ordered by the axis tuple: 

714 global_id = ax0_pos * ax1_size * ... + ax1_pos * ax2_size * ... + axN_pos 

715 This matches MindFormers' ``offset_id = dp_rank * (cp*tp) + cp_rank * tp + tp_rank`` 

716 for combined sequence-parallel sharding across dp, cp, and tp dimensions. 

717 

718 Args: 

719 tensor_dim: Tensor dimension index to query. 

720 

721 Returns: 

722 Split index for this rank (0 if not sharded or rank not in rank list). 

723 """ 

724 alias_tm = self.alias_tensor_map 

725 if alias_tm is None or tensor_dim >= len(alias_tm): 

726 return 0 

727 dim_entry = alias_tm[tensor_dim] 

728 if dim_entry == 'None': 

729 return 0 

730 rank = platform.get_rank() 

731 if isinstance(dim_entry, tuple): 

732 non_none = [ax for ax in dim_entry if ax != 'None'] 

733 if not non_none: 

734 return 0 

735 global_id = 0 

736 for ax in non_none: 

737 rank_list = self.mesh.get_rank_list_along_axis(ax) 

738 local_id = rank_list.index(rank) if rank in rank_list else 0 

739 ax_size = self.mesh.get_device_num_along_axis(ax) 

740 global_id = global_id * ax_size + local_id 

741 return global_id 

742 if isinstance(dim_entry, str): 

743 rank_list = self.mesh.get_rank_list_along_axis(dim_entry) 

744 return rank_list.index(rank) if rank in rank_list else 0 

745 return 0 

746 

747 def get_global_shape(self, slice_shape): 

748 """get global shape""" 

749 return self._mesh.get_global_shape(slice_shape, self._tensor_map) 

750 

751 def get_devices_for_axis(self, axis, rank): 

752 """ 

753 Get the repeat rank list when the axis is not shard. 

754 

755 Args: 

756 layout (Layout): Layout 

757 axis (str): Axis name. 

758 rank (int): Global rank 

759 

760 Returns: 

761 list: reduce rank list 

762 """ 

763 return self._mesh.get_devices_for_axis(axis, rank) 

764 

765 def get_comm_group_by_axis(self, axis): 

766 """Return the communication group for the specified mesh axis via the underlying DeviceMesh.""" 

767 return self._mesh.get_comm_group_by_axis(axis) 

768 

769 def repeat_num(self): 

770 """ 

771 Number of repeated placements. 

772 For example: 

773 layout = Layout((2, 4), ("dp", "mp")) 

774 x_layout = layout("dp", "None") 

775 The repeat_num is equal to all device num 8 divided by device num corresponding to used axis 2, that is 4. 

776 """ 

777 if self._tensor_map is None: 

778 raise ValueError(f"The tensor_map is None, the mesh_shape is {self._mesh.mesh_shape}," 

779 f" alias_name is {self._mesh.mesh_dim_names}") 

780 

781 all_device_num = functools.reduce(lambda x, y: x * y, self._mesh.mesh_shape) 

782 used_dev_num = 1 

783 for ele in self._tensor_map: 

784 if isinstance(ele, tuple): 

785 for item in ele: 

786 if item >= 0: 

787 used_dev_num *= self._mesh.mesh_shape[len(self._mesh.mesh_shape) - item - 1] 

788 continue 

789 if ele >= 0: 

790 used_dev_num *= self._mesh.mesh_shape[len(self._mesh.mesh_shape) - ele - 1] 

791 

792 return all_device_num // used_dev_num 

793 

794 def _to_compact_string(self): 

795 """ 

796 generate dict key 

797 

798 Returns: 

799 str: string for compact 

800 """ 

801 mesh_key = self._mesh.to_hash() 

802 hash_key = (self._tensor_map, self.partial) 

803 hash_key += mesh_key 

804 return str(hash_key) 

805 

806 @property 

807 def compact_str(self): 

808 """Return the cached compact string representation of this layout.""" 

809 return self._compact_str 

810 

811 def update_compact_str(self): 

812 """Recompute and store the compact string representation of this layout.""" 

813 self._compact_str = self._to_compact_string() 

814 

815 def to_string(self): 

816 """ 

817 layout dump 

818 

819 Returns: 

820 str: layout string 

821 """ 

822 device_info = f"Mesh shape: {self._mesh.mesh_shape}" 

823 alias_info = f"Alias Names: {self._mesh.mesh_dim_names}" 

824 rank_info = f"Rank List: {self._rank_list}" 

825 partial_info = f"Partial: {self.partial}" 

826 

827 if self._tensor_map is None: 

828 tensor_info = "Tensor Map: Not configured" 

829 else: 

830 readable_map = [] 

831 for item in self._tensor_map: 

832 if isinstance(item, tuple): 

833 # handle nested tuple 

834 mapped_tuple = tuple( 

835 self._mesh.mesh_dim_names[len(self._mesh.mesh_dim_names) - 1 - dim] if dim != -1 else "None" 

836 for dim in item 

837 ) 

838 readable_map.append(mapped_tuple) 

839 else: 

840 readable_map.append( 

841 self._mesh.mesh_dim_names[len(self._mesh.mesh_dim_names) - 1 - item] if item != -1 else "None" 

842 ) 

843 

844 tensor_info = f"Tensor Map: {tuple(readable_map)}" 

845 

846 interleaved = "Yes" if "interleaved_parallel" in self._mesh.mesh_dim_names else "No" 

847 interleaved_info = f"Interleaved Parallel: {interleaved}" 

848 

849 return ( 

850 f"Layout Configuration:\n" 

851 f" {device_info}\n" 

852 f" {alias_info}\n" 

853 f" {partial_info}\n" 

854 f" {tensor_info}\n" 

855 f" {interleaved_info}\n" 

856 f" {rank_info}" 

857 ) 

858 

859 def __str__(self): 

860 """__str__""" 

861 return self.to_string() 

862 

863 def __repr__(self): 

864 """__repr__""" 

865 return f"<Layout at {hex(id(self))}>" 

866 

867 def __eq__(self, other): 

868 """ 

869 __eq__ 

870 """ 

871 if not isinstance(other, Layout): 

872 return False 

873 

874 same_layout_attrs = ( 

875 self.mesh_shape, 

876 self.alias_name, 

877 self.partial, 

878 self.rank_list, 

879 ) == ( 

880 other.mesh_shape, 

881 other.alias_name, 

882 other.partial, 

883 other.rank_list, 

884 ) 

885 if not same_layout_attrs: 

886 return False 

887 

888 if self._tensor_map is None or other.tensor_map is None: 

889 return self._tensor_map is other.tensor_map 

890 return self._tensor_map == other.tensor_map