Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / dtensor / layout.py: 96%
504 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-26 05:43 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-26 05:43 +0800
1# Copyright 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"""layout"""
16# pylint: disable=C9006,C9007,C9008
18import copy
19import functools
20from typing import Any, NamedTuple, Optional, Sequence
22import numpy as np
23import torch.distributed as dist
25from hyper_parallel.core.dtensor.placement_types import (
26 Partial,
27 Placement,
28 RaggedShard,
29 Replicate,
30 Shard,
31 StridedShard,
32)
33from hyper_parallel.core.dtensor.device_mesh import DeviceMesh, _create_device_mesh
36class RaggedShardInfo(NamedTuple):
37 """RaggedShard placement and its corresponding mesh dimension."""
39 mesh_dim: int
40 placement: RaggedShard
43def _extract_ragged_shard(placements: Sequence[Placement]) -> Optional[RaggedShardInfo]:
44 """Extract the single RaggedShard placement from a placement sequence."""
45 ragged_shard = None
46 for mesh_dim, placement in enumerate(placements):
47 if not placement.is_ragged_shard():
48 continue
49 if ragged_shard is not None:
50 raise ValueError(
51 "Layout supports at most one RaggedShard placement, "
52 f"but got placements={tuple(placements)!r}"
53 )
54 ragged_shard = RaggedShardInfo(mesh_dim, placement)
55 return ragged_shard
58def _replace_ragged_with_replicate(placements: Sequence[Placement]) -> tuple[Placement, ...]:
59 """Return placements with RaggedShard represented as Replicate."""
60 return tuple(
61 Replicate() if placement.is_ragged_shard() else placement
62 for placement in placements
63 )
66def infer_balanced_chunk_range(
67 tensor_size: int,
68 shard_count: int,
69 shard_rank: int,
70) -> tuple[int, int]:
71 """Return one balanced half-open shard range."""
72 shard_size, remainder = divmod(tensor_size, shard_count)
73 chunk_start = shard_rank * shard_size + min(shard_rank, remainder)
74 chunk_end = chunk_start + shard_size + int(shard_rank < remainder)
75 return chunk_start, chunk_end
78def infer_ceil_chunk_range(
79 tensor_size: int,
80 shard_count: int,
81 shard_rank: int,
82) -> tuple[int, int]:
83 """Return one ceil-chunk range, including empty trailing ranks."""
84 chunk_size = (tensor_size + shard_count - 1) // shard_count
85 chunk_start = min(shard_rank * chunk_size, tensor_size)
86 chunk_end = min(chunk_start + chunk_size, tensor_size)
87 return chunk_start, chunk_end
90def _calculate_mesh_coordinate_for_rank(
91 mesh_shape: Sequence[int], rank_id: int
92) -> list[int]:
93 """Calculate the device-mesh coordinate for a linear rank."""
94 coordinate = [0] * len(mesh_shape)
95 remaining_rank = rank_id
96 for mesh_dim in range(len(mesh_shape) - 1, -1, -1):
97 coordinate[mesh_dim] = remaining_rank % mesh_shape[mesh_dim]
98 remaining_rank //= mesh_shape[mesh_dim]
99 return coordinate
102def _infer_tensor_dimension_slice(
103 global_size: int,
104 tensor_mapping: int | Sequence[int],
105 mesh_shape: Sequence[int],
106 mesh_coordinate: Sequence[int],
107 uneven_shard_mesh_dims: set[int],
108) -> tuple[int, int]:
109 """Infer the half-open slice for one tensor dimension."""
110 mapped_dims = (tensor_mapping,) if isinstance(tensor_mapping, int) else tensor_mapping
111 slice_start, slice_end = 0, global_size
112 for mapped_mesh_dim in mapped_dims:
113 if mapped_mesh_dim == -1:
114 continue
115 mesh_dim = len(mesh_shape) - mapped_mesh_dim - 1
116 infer_chunk_range = (
117 infer_ceil_chunk_range
118 if mesh_dim in uneven_shard_mesh_dims
119 else infer_balanced_chunk_range
120 )
121 chunk_start, chunk_end = infer_chunk_range(
122 slice_end - slice_start,
123 mesh_shape[mesh_dim],
124 mesh_coordinate[mesh_dim],
125 )
126 slice_start += chunk_start
127 slice_end = slice_start + chunk_end - chunk_start
128 return slice_start, slice_end
131def _infer_slice_area_by_rank(
132 mesh_shape: tuple[int, ...],
133 tensor_map: Sequence,
134 rank_id: int,
135 full_shape: Sequence[int],
136 uneven_shard_mesh_dims: set[int],
137) -> tuple[tuple[int, int], ...]:
138 """Return one rank's slice using placement-specific shard geometry."""
139 mesh_coordinate = _calculate_mesh_coordinate_for_rank(mesh_shape, rank_id)
140 return tuple(
141 _infer_tensor_dimension_slice(
142 global_size,
143 tensor_map[tensor_dim],
144 mesh_shape,
145 mesh_coordinate,
146 uneven_shard_mesh_dims,
147 )
148 for tensor_dim, global_size in enumerate(full_shape)
149 )
152def infer_slice_area_by_rank(
153 mesh_shape: tuple[int, ...],
154 tensor_map: Sequence,
155 rank_id: int,
156 full_shape: Sequence[int],
157 uneven_shard_mesh_dims: Optional[Sequence[int]] = None,
158) -> tuple[tuple[int, int], ...]:
159 """Return one rank's global slice using placement-specific shard semantics."""
160 return _infer_slice_area_by_rank(
161 mesh_shape,
162 tensor_map,
163 rank_id,
164 full_shape,
165 set(uneven_shard_mesh_dims or ()),
166 )
169def infer_slice_area_by_layout(
170 layout: "Layout",
171 rank_id: int,
172 full_shape: Sequence[int],
173) -> tuple[tuple[int, int], ...]:
174 """Return one rank's slice using ``uneven_shard`` placement markers."""
175 return infer_slice_area_by_rank(
176 layout.mesh_shape,
177 layout.tensor_map,
178 rank_id,
179 full_shape,
180 layout.uneven_shard_mesh_dims,
181 )
184def _get_slice_tensor_by_layout(global_tensor, layout):
185 """Transfer global tensor to local tensor by layout"""
186 inner_rank_id = layout.rank_list.index(layout.mesh.rank)
187 slice_area = infer_slice_area_by_layout(layout, inner_rank_id, global_tensor.shape)
189 def get_slice_data(full_data, offset):
190 area = ()
191 for begin, end in offset:
192 area += (slice(begin, end),)
193 return full_data[area].clone()
195 local_tensor = get_slice_data(global_tensor, slice_area)
196 return local_tensor
199def _infer_slice_shape_by_layout(global_shape, layout):
200 """Infer slice shape from global_shape and layout"""
201 inner_rank_id = layout.rank_list.index(layout.mesh.rank)
202 slice_area = infer_slice_area_by_layout(layout, inner_rank_id, global_shape)
203 return [end - start for start, end in slice_area]
206class Layout:
207 """
208 Topological abstraction describing cluster devices for tensor slice placement on the cluster.
210 Note:
211 - It is valid only in semi auto parallel or auto parallel mode.
212 - The multiplication result of the `mesh_shape` must be equal to the device count in a pipeline stage.
213 - When the layout function is invoked to constructs a sharding strategy, each alias name is only allowed to be
214 used once to shard a tensor.
216 Args:
217 mesh_shape (tuple): Describe the shape of devices arrangement, its element type is int.
218 alias_name (tuple): The alias name for each axis of mesh_shape, its length shoits element type is string.
219 When using "interleaved_parallel" as an alias name, the tensor would be split into multiple
220 copies on the corresponding partition dimension on a single card.
221 rank_list (tuple, optional): Data is allocated to the device according to rank_list. Default: ``None``.
223 Raises:
224 TypeError: `mesh_shape` is not a tuple type.
225 TypeError: `alias_name` is not a tuple type.
226 TypeError: 'rank_list' is not a list type.
227 ValueError: `mesh_shape` length is not equal to `alias_name` length.
228 TypeError: The element of `mesh_shape` is not int type.
229 TypeError: The element of `alias_name` is not a str type.
230 TypeError: The element of `rank_list` is not int type.
231 ValueError: The element of `alias_name` is an empty str.
232 ValueError: The element of `alias_name` is "None".
233 ValueError: `alias_name` contains repeated element.
235 Supported Platforms:
236 ``Ascend``
238 Examples:
239 >>> layout = Layout((2, 2, 2), ("dp", "sp", "mp"))
240 >>> layout0 = layout("dp", "mp")
241 >>> print(layout0.to_dict())
242 {"mesh_shape": (2, 2, 2), "tensor_map": (2, 0), "interleaved_parallel": False,
243 'alias_name': {'dp', 'sp', 'mp'}, "rank_list": [0, 1, 2, 3, 4, 5, 6, 7]}
244 >>> layout = Layout((2, 2, 2), ("dp", "sp", "interleaved_parallel"))
245 >>> layout1 = layout(("dp", "interleaved_parallel"), "sp")
246 """
248 def __init__(self, mesh_shape, alias_name, rank_list=None, init_backend=True):
249 self._alias_name = alias_name
250 self._tensor_map = None
251 if not rank_list:
252 self._rank_list = tuple(range(np.prod(np.array(mesh_shape))))
253 else:
254 self._rank_list = tuple(rank_list)
255 self._partial = [None] * len(mesh_shape) # partial status for each dev dim
256 self._support_partial_op = ['sum', 'max', 'min', 'avg', 'prod', 'all', None]
257 self._alias_tensor_map = None
258 self._tensor_shape = None
259 self._tensor_stride = None
260 self._tensor_dtype = None
261 self._mesh = _create_device_mesh("npu", mesh_shape, mesh_dim_names=alias_name, rank_list=self._rank_list,
262 init_backend=init_backend)
263 self._placements = None
264 self.partial_ops = {} # Initialized in _build_dim_map_from_placements()
265 self._ragged_shard = None
266 self._compact_str = self._to_compact_string()
268 @classmethod
269 def from_device_mesh(cls, device_mesh: DeviceMesh) -> 'Layout':
270 """
271 Create a Layout from an existing DeviceMesh.
273 Args:
274 device_mesh (DeviceMesh): The device mesh to create layout from.
276 Returns:
277 Layout: A new Layout instance initialized with the properties of the provided device mesh.
279 Examples:
280 >>> from hyper_parallel.core.dtensor.layout import Layout, DeviceMesh
281 >>> device_mesh = DeviceMesh("npu", (2, 2), mesh_dim_names=("dp", "mp"))
282 >>> layout = Layout.from_device_mesh(device_mesh)
283 """
284 obj = cls.__new__(cls)
285 obj._mesh = device_mesh
286 obj._alias_name = device_mesh.mesh_dim_names
287 obj._rank_list = device_mesh.rank_list
288 obj._tensor_map = None
289 obj._partial = [None] * len(device_mesh.mesh_shape)
290 obj._support_partial_op = ['sum', 'max', 'min', 'avg', 'prod', 'all', None]
291 obj._alias_tensor_map = None
292 obj._tensor_shape = None
293 obj._tensor_stride = None
294 obj._tensor_dtype = None
295 obj._placements = None
296 obj._ragged_shard = None
297 obj._compact_str = obj._to_compact_string()
298 return obj
300 def __call__(self, *alias_tensor_map):
301 obj = copy.deepcopy(self)
303 # Clear the inherited partial status.
304 # When creating a new layout mapping configuration via __call__,
305 # it should not inherit the dynamic execution state (Partial) of the original layout.
306 # If the user intends to create a Partial placement, it will be parsed from alias_tensor_map.
307 obj._partial = [None] * len(obj.mesh_shape)
309 if len(alias_tensor_map) == 1 and isinstance(alias_tensor_map[0], (list, tuple)):
310 if len(alias_tensor_map[0]) > 0 and isinstance(alias_tensor_map[0][0], Placement):
311 return self._process_placement_layout(obj, alias_tensor_map[0])
313 if len(alias_tensor_map) > 0 and isinstance(alias_tensor_map[0], Placement):
314 return self._process_placement_layout(obj, alias_tensor_map)
316 return self._process_alias_layout(obj, alias_tensor_map)
318 def __deepcopy__(self, memo):
319 """Deep copy layout without rebuilding the underlying device mesh."""
320 cls = self.__class__
321 result = cls.__new__(cls)
322 memo[id(self)] = result
323 for k, v in self.__dict__.items():
324 setattr(result, k, copy.deepcopy(v, memo))
325 return result
327 @staticmethod
328 def _process_placement_layout(obj, placements):
329 """Process layout defined by Placement types."""
330 obj.set_placements(placements)
331 return copy.deepcopy(obj)
333 @staticmethod
334 def _process_alias_layout(obj, alias_tensor_map):
335 """Process layout defined by alias strings."""
336 obj.set_alias_tensor_map(alias_tensor_map)
337 tensor_map = ()
338 writed_map = ()
339 for ele in alias_tensor_map:
340 if isinstance(ele, tuple):
341 ele_map = ()
342 for item in ele:
343 if item == "None":
344 ele_map += (-1,)
345 continue
346 if item not in obj.alias_name:
347 raise ValueError(f'The axis {item} is not found in {obj.alias_name}')
348 if item in writed_map:
349 raise ValueError(f'The axis {item} has been set more than one in {obj.alias_name}')
350 ele_map += (len(obj.alias_name) - 1 - obj.alias_name.index(item),)
351 writed_map += (item,)
352 tensor_map += (ele_map,)
353 continue
354 if ele == "None":
355 tensor_map += (-1,)
356 continue
357 if ele not in obj.alias_name:
358 raise ValueError(f'The axis {ele} is not found in {obj.alias_name}')
359 if ele in writed_map:
360 raise ValueError(f'The axis {ele} has been set more than one in {obj.alias_name}')
361 tensor_map += (len(obj.alias_name) - 1 - obj.alias_name.index(ele),)
362 writed_map += (ele,)
363 obj.set_tensor_map(tensor_map)
364 obj.tensor_map_to_placement()
365 obj.update_compact_str()
366 return copy.deepcopy(obj)
368 def to_dict(self):
369 """
370 Transform layout to a dictionary.
371 """
372 if self._mesh.mesh_shape is None:
373 raise ValueError("The device_shape of layout is None")
374 if self._tensor_map is None:
375 raise ValueError("The tensor_map of layout is None")
376 interleaved_parallel = "interleaved_parallel" in self._mesh.mesh_dim_names
377 return {"mesh_shape": self._mesh.mesh_shape, "tensor_map": self._tensor_map,
378 "interleaved_parallel": interleaved_parallel, "alias_name": self._mesh.mesh_dim_names,
379 "rank_list": self._rank_list}
381 def placement_to_tensor_map(self, dim):
382 """
383 Transform placement to tensor map.
385 This method converts the `placements` configuration (consisting of Shard, StridedShard,
386 Replicate, Partial)
387 into a `tensor_map` representation used for distributed tensor operations.
389 Args:
390 dim (int): The dimension of the tensor. Must be a positive integer.
392 Returns:
393 tuple: A tuple representing the tensor map, where each element corresponds to a tensor dimension.
394 A value of -1 indicates the dimension is not sharded, an integer indicates the mesh
395 dimension index along which the tensor dimension is sharded, and a tuple indicates
396 that the same tensor dimension is sharded multiple times in order.
398 Raises:
399 ValueError: If `dim` is negative.
400 ValueError: If a shard dimension in `placements` is out of bounds for the given tensor dimension.
401 """
402 if dim < 0:
403 raise ValueError(f"Tensor dimension must be positive, but got {dim}")
404 if dim == 0:
405 return self._handle_zero_dim_placement()
407 dim_map = self._build_dim_map_from_placements(dim)
408 tensor_map = self._convert_dim_map_to_tensor_map(dim_map)
409 self.set_tensor_map(tuple(tensor_map))
410 self._alias_tensor_map = self._build_readable_tensor_map()
411 self.update_compact_str()
412 return tensor_map
414 def _handle_zero_dim_placement(self):
415 """Handle the special case of zero-dimensional tensor."""
416 self.set_tensor_map(())
417 self._alias_tensor_map = ()
418 for mesh_idx, placement in enumerate(self.normal_placements):
419 if isinstance(placement, Partial):
420 self._partial[mesh_idx] = self._extract_reduce_op(placement)
421 return []
423 def _build_dim_map_from_placements(self, dim):
424 """Build dimension map from placements."""
425 dim_map = [-1] * dim
426 self.partial_ops = {}
427 for mesh_idx, placement in enumerate(self.normal_placements):
428 if isinstance(placement, Shard):
429 shard_dim = placement.dim
430 if shard_dim < -dim or shard_dim >= dim:
431 raise ValueError(f"Shard dimension {shard_dim} is out of bounds for tensor of dimension {dim}")
432 if shard_dim < 0:
433 shard_dim += dim
434 if dim_map[shard_dim] == -1:
435 dim_map[shard_dim] = [mesh_idx]
436 else:
437 dim_map[shard_dim].append(mesh_idx)
438 elif isinstance(placement, Partial):
439 self._partial[mesh_idx] = self._extract_reduce_op(placement)
440 self._validate_strided_shard_split_factor(dim_map)
441 self._reorder_dim_map_for_strided_shard(dim_map)
442 return dim_map
444 @staticmethod
445 def _placement_split_factor(placement):
446 """Return the effective split factor carried by a placement."""
447 return placement.split_factor if isinstance(placement, StridedShard) else 1
449 @staticmethod
450 def _build_order_positions(shard_order):
451 """Build a mesh axis to order position mapping."""
452 return {mesh_idx: order_idx for order_idx, mesh_idx in enumerate(shard_order)}
454 def _compute_expected_split_factors(self, shard_axes, shard_order):
455 """Infer the split_factor each mesh axis should carry for the given sharding order."""
456 order_positions = self._build_order_positions(shard_order)
457 expected_split_factors = {}
458 for mesh_idx in shard_axes:
459 split_factor = 1
460 for right_mesh_idx in shard_axes:
461 if right_mesh_idx <= mesh_idx:
462 continue
463 if order_positions[right_mesh_idx] < order_positions[mesh_idx]:
464 split_factor *= self.mesh_shape[right_mesh_idx]
465 expected_split_factors[mesh_idx] = split_factor
466 return expected_split_factors
468 def _get_effective_shard_axes(self, shard_axes):
469 """Return shard axes ordered by their effective sharding order."""
470 return sorted(
471 shard_axes,
472 key=lambda mesh_idx: self._placement_split_factor(self.placements[mesh_idx]),
473 )
475 def _reorder_dim_map_for_strided_shard(self, dim_map):
476 """Reorder dim_map entries to reflect the effective sharding order."""
477 for i, shard_axes in enumerate(dim_map):
478 if shard_axes == -1 or len(shard_axes) <= 1:
479 continue
480 dim_map[i] = self._get_effective_shard_axes(shard_axes)
482 def _validate_strided_shard_split_factor(self, dim_map):
483 """Validate that split factors match the effective sharding order."""
484 for shard_axes in dim_map:
485 if shard_axes == -1:
486 continue
487 shard_order = self._get_effective_shard_axes(shard_axes)
488 expected_split_factors = self._compute_expected_split_factors(
489 shard_axes, shard_order
490 )
491 for mesh_idx in shard_axes:
492 placement = self.placements[mesh_idx]
493 actual_split_factor = self._placement_split_factor(placement)
494 expected_split_factor = expected_split_factors[mesh_idx]
495 if actual_split_factor != expected_split_factor:
496 raise ValueError(
497 f"StridedShard split_factor mismatch on mesh axis {mesh_idx}: "
498 f"expected {expected_split_factor}, got {actual_split_factor}."
499 )
501 @staticmethod
502 def _extract_reduce_op(placement):
503 """Extract reduce operation name from Partial placement."""
504 op_name = getattr(placement, "reduce_op", "sum")
505 if isinstance(op_name, str):
506 op_name = op_name.lower()
507 return op_name
509 def _convert_dim_map_to_tensor_map(self, dim_map):
510 """Convert dimension map to tensor map format."""
511 device_dim_count = len(self.mesh_shape)
512 tensor_map = []
513 for mesh_idx in dim_map:
514 if mesh_idx == -1:
515 tensor_map.append(-1)
516 continue
517 mapped_axes = tuple(device_dim_count - 1 - axis for axis in mesh_idx)
518 tensor_map.append(mapped_axes[0] if len(mapped_axes) == 1 else mapped_axes)
519 return tensor_map
521 def _build_readable_tensor_map(self):
522 """Build human-readable alias tensor map from tensor_map."""
523 mesh_dim_names = self._mesh.mesh_dim_names
524 has_names = mesh_dim_names is not None
526 def _map_dim(dim):
527 """convert dimension index to dimension name."""
528 if dim == -1:
529 return "None"
530 if not has_names:
531 return f"dim_{dim}"
532 return mesh_dim_names[len(mesh_dim_names) - 1 - dim]
534 readable_map = []
535 for item in self._tensor_map:
536 if isinstance(item, tuple):
537 mapped_tuple = tuple(_map_dim(dim) for dim in item)
538 readable_map.append(mapped_tuple)
539 else:
540 readable_map.append(_map_dim(item))
541 return tuple(readable_map)
543 def tensor_map_to_placement(self):
544 """
545 Transform tensor map to placement.
547 This method converts the existing `tensor_map` and `partial` status into a list of `Placement` objects
548 (Shard, StridedShard, Replicate, Partial). This is the inverse operation of
549 `placement_to_tensor_map`.
551 Returns:
552 list[Placement]: A list of Placement objects describing the distribution strategy for each
553 dimension of the device mesh.
555 Raises:
556 ValueError: If `tensor_map` is not configured (None).
557 """
558 if self._tensor_map is None:
559 raise ValueError("The tensor_map is None, cannot transform to placements.")
560 uneven_shard_dims = {
561 mesh_idx: placement.dim
562 for mesh_idx, placement in enumerate(self.placements or ())
563 if isinstance(placement, Shard) and placement.uneven_shard
564 }
565 mesh_ndim = len(self.mesh_shape)
566 placements = [Replicate()] * mesh_ndim
567 for tensor_dim, mapping in enumerate(self._tensor_map):
568 mapping_list = mapping if isinstance(mapping, tuple) else (mapping,)
569 valid_mapping = [map_val for map_val in mapping_list if map_val != -1]
570 mesh_indices = [mesh_ndim - 1 - map_val for map_val in valid_mapping]
571 shard_axes = sorted(mesh_indices)
572 expected_split_factors = self._compute_expected_split_factors(
573 shard_axes, mesh_indices
574 )
575 for mesh_idx in shard_axes:
576 split_factor = expected_split_factors[mesh_idx]
577 uneven_shard = uneven_shard_dims.get(mesh_idx) == tensor_dim
578 placement = (
579 StridedShard(
580 dim=tensor_dim,
581 split_factor=split_factor,
582 uneven_shard=uneven_shard,
583 )
584 if split_factor > 1
585 else Shard(dim=tensor_dim, uneven_shard=uneven_shard)
586 )
587 placements[mesh_idx] = placement
588 for mesh_idx, op in enumerate(self.partial):
589 if op is not None:
590 placements[mesh_idx] = Partial(reduce_op=op)
591 if self._ragged_shard is not None:
592 placements[self._ragged_shard.mesh_dim] = self._ragged_shard.placement
593 self.set_placements(placements)
594 self._alias_tensor_map = self._build_readable_tensor_map()
595 self.update_compact_str()
596 return placements
598 def __setstate__(self, state):
599 self.__dict__.update(state)
600 self.update_mesh(init_backend=False)
602 @property
603 def mesh(self):
604 """
605 Get the device mesh associated with this layout.
607 Returns:
608 DeviceMesh: The device mesh describing the device topology.
609 """
610 return self._mesh
612 def update_mesh(self, init_backend: bool = True):
613 """Recreate the internal DeviceMesh from current layout properties.
615 Args:
616 init_backend (bool): Whether to initialize communication backend
617 (process groups). Set to ``False`` during deserialization to
618 avoid creating process groups with a stale rank_list from the
619 sender side. Default ``True``.
620 """
621 self._mesh = _create_device_mesh("npu", self.mesh_shape, mesh_dim_names=self.alias_name,
622 rank_list=self.rank_list, init_backend=init_backend)
624 @property
625 def rank_list(self):
626 """
627 Get the list of ranks participating in this layout.
629 Returns:
630 tuple[int]: The rank list.
631 """
632 return self._rank_list
634 @rank_list.setter
635 def rank_list(self, val):
636 self._rank_list = val
638 @property
639 def mesh_shape(self):
640 """mesh shape"""
641 return self._mesh.mesh_shape
643 @property
644 def alias_name(self):
645 """alias name"""
646 return self._mesh.mesh_dim_names
648 @property
649 def alias_tensor_map(self):
650 """Return the human-readable alias tensor map for this layout."""
651 return self._alias_tensor_map
653 @property
654 def alias_placements(self):
655 """Return alias_tensor_map when it contains multi-axis tuples, otherwise placements.
657 alias_tensor_map preserves multi-axis ordering information
658 (e.g., (("dp", "tp"), "None") vs (("tp", "dp"), "None"))
659 that Placement objects cannot represent, since both map to
660 [Shard(0), Shard(0)].
662 For single-axis layouts, Placement objects are preferred because they
663 also carry Partial status which alias_tensor_map cannot encode.
665 Use this property when constructing DTensors from an existing Layout
666 to avoid the lossy Placement round-trip for multi-axis cases.
667 """
668 if self._ragged_shard is not None:
669 return self._placements
670 if self._alias_tensor_map is not None and any(
671 isinstance(item, tuple) for item in self._alias_tensor_map
672 ):
673 return self._alias_tensor_map
674 return self._placements
676 def set_alias_tensor_map(self, alias_tensor_map):
677 """Set alias_tensor_map"""
678 self._alias_tensor_map = alias_tensor_map
680 @property
681 def placements(self):
682 """placements"""
683 return self._placements
685 def set_placements(self, placements: Optional[Sequence[Placement]]) -> None:
686 """Set placements and retain the RaggedShard omitted from tensor_map."""
687 self._placements = placements
688 self._ragged_shard = (
689 None if placements is None else _extract_ragged_shard(placements)
690 )
692 @property
693 def tensor_shape(self) -> Optional[tuple[int, ...]]:
694 """Return the explicit logical global shape, if present."""
695 return self._tensor_shape
697 @property
698 def tensor_stride(self) -> Optional[tuple[int, ...]]:
699 """Return the explicit logical global stride, if present."""
700 return self._tensor_stride
702 @property
703 def tensor_dtype(self) -> Optional[Any]:
704 """Return the explicit logical dtype, if present."""
705 return self._tensor_dtype
707 @property
708 def uneven_shard_mesh_dims(self) -> tuple[int, ...]:
709 """Return mesh dimensions carrying FSDP ceil-chunk placements."""
710 return tuple(
711 mesh_dim
712 for mesh_dim, placement in enumerate(self._placements or ())
713 if isinstance(placement, Shard) and placement.uneven_shard
714 )
716 @property
717 def has_uneven_shard(self) -> bool:
718 """Return whether any placement uses FSDP ceil-chunk geometry."""
719 return bool(self.uneven_shard_mesh_dims)
721 def set_tensor_meta(
722 self,
723 shape: Sequence[int],
724 stride: Sequence[int],
725 dtype: Any,
726 ) -> None:
727 """Set logical tensor metadata independently from local shard storage.
729 Args:
730 shape: Logical global tensor shape.
731 stride: Logical global tensor stride.
732 dtype: Logical tensor dtype.
733 """
734 self._tensor_shape = tuple(shape)
735 self._tensor_stride = tuple(stride)
736 self._tensor_dtype = dtype
737 self.update_compact_str()
739 @property
740 def normal_placements(self) -> Optional[tuple[Placement, ...]]:
741 """Return placements with RaggedShard represented as Replicate."""
742 if self._placements is None:
743 return None
744 return _replace_ragged_with_replicate(self._placements)
746 @property
747 def ragged_shard(self) -> Optional[RaggedShardInfo]:
748 """Return the RaggedShard placement and its mesh dimension, if present."""
749 return self._ragged_shard
751 @property
752 def tensor_map(self):
753 """tensor map"""
754 return self._tensor_map
756 def set_tensor_map(self, tensor_map):
757 """Set tensor_map."""
758 self._tensor_map = tensor_map
760 @property
761 def partial(self):
762 """partial status"""
763 return self._partial
765 def set_partial_by_dev_axis(self, axis, op):
766 """Set the partial status for the specified dev ID, means pending to do reduce by op."""
767 if op not in self._support_partial_op:
768 raise ValueError(f"Partial op must be one of {self._support_partial_op}, but got {op}")
769 if self.is_dev_axis_apply_shard(axis):
770 raise ValueError("Partial dim must be replicate.")
771 self._partial[self._mesh.axis_index(axis)] = op
772 self.tensor_map_to_placement()
773 self.update_compact_str()
775 def get_partial_by_dev_id(self, axis):
776 """Get the partial status for the specified dev id"""
777 return self.partial[self._mesh.axis_index(axis)]
779 def is_dev_axis_apply_shard(self, axis):
780 """Return true if device axis is applying shard"""
781 axis_id = self._mesh.axis_id(axis)
783 def flatten(input_x):
784 flatten_res = []
785 for item in input_x:
786 if isinstance(item, tuple):
787 flatten_res.extend(flatten(item))
788 else:
789 flatten_res.append(item)
790 return flatten_res
792 flatten_tensor_map = flatten(self.tensor_map)
793 return axis_id in flatten_tensor_map
795 def get_dev_axis_apply_shard_axis(self, axis):
796 """Return the axis which be split by axis. If axis not be apply to shard, return None."""
797 for dim, dim_map in enumerate(self.alias_tensor_map):
798 if (isinstance(dim_map, tuple) and axis in dim_map) or axis == dim_map:
799 return dim
800 return None
802 def reset_partial(self):
803 """Clear all partial statuses and regenerate placements from the tensor map."""
804 self._partial = [None] * len(self.mesh_shape)
805 self.tensor_map_to_placement()
806 self.update_compact_str()
808 def is_partial(self):
809 """Return true if any dim in mesh_shape is partial"""
810 return any(self.partial)
812 def get_dim_split_num(self, tensor_dim: int) -> int:
813 """Return the total shard count for ``tensor_dim`` via alias_tensor_map.
815 Args:
816 tensor_dim: Tensor dimension index to check.
818 Returns:
819 Number of shards (1 if not sharded or no alias_tensor_map set).
820 """
821 alias_tm = self.alias_tensor_map
822 if alias_tm is None or tensor_dim >= len(alias_tm):
823 return 1
824 dim_entry = alias_tm[tensor_dim]
825 if dim_entry == 'None':
826 return 1
827 if isinstance(dim_entry, str):
828 return self.mesh.get_device_num_along_axis(dim_entry)
829 if isinstance(dim_entry, tuple):
830 total = 1
831 for axis in dim_entry:
832 if axis != 'None':
833 total *= self.mesh.get_device_num_along_axis(axis)
834 return total
835 return 1
837 def get_split_id(self, tensor_dim: int) -> int:
838 """Return this rank's global position among all shards of ``tensor_dim``.
840 For a single sharding axis, returns the rank's position within that axis group.
841 For multiple sharding axes (e.g. dp+cp both sharding T1), computes the combined
842 global position as a mixed-radix number ordered by the axis tuple:
843 global_id = ax0_pos * ax1_size * ... + ax1_pos * ax2_size * ... + axN_pos
844 This matches MindFormers' ``offset_id = dp_rank * (cp*tp) + cp_rank * tp + tp_rank``
845 for combined sequence-parallel sharding across dp, cp, and tp dimensions.
847 Args:
848 tensor_dim: Tensor dimension index to query.
850 Returns:
851 Split index for this rank (0 if not sharded or rank not in rank list).
852 """
853 alias_tm = self.alias_tensor_map
854 if alias_tm is None or tensor_dim >= len(alias_tm):
855 return 0
856 dim_entry = alias_tm[tensor_dim]
857 if dim_entry == 'None':
858 return 0
859 rank = dist.get_rank()
860 if isinstance(dim_entry, tuple):
861 non_none = [ax for ax in dim_entry if ax != 'None']
862 if not non_none:
863 return 0
864 global_id = 0
865 for ax in non_none:
866 rank_list = self.mesh.get_rank_list_along_axis(ax)
867 local_id = rank_list.index(rank) if rank in rank_list else 0
868 ax_size = self.mesh.get_device_num_along_axis(ax)
869 global_id = global_id * ax_size + local_id
870 return global_id
871 if isinstance(dim_entry, str):
872 rank_list = self.mesh.get_rank_list_along_axis(dim_entry)
873 return rank_list.index(rank) if rank in rank_list else 0
874 return 0
876 def get_global_shape(self, slice_shape):
877 """get global shape"""
878 if self._tensor_shape is not None:
879 return self._tensor_shape
880 return self._mesh.get_global_shape(slice_shape, self._tensor_map)
882 def get_devices_for_axis(self, axis, rank):
883 """
884 Get the repeat rank list when the axis is not shard.
886 Args:
887 layout (Layout): Layout
888 axis (str): Axis name.
889 rank (int): Global rank
891 Returns:
892 list: reduce rank list
893 """
894 return self._mesh.get_devices_for_axis(axis, rank)
896 def get_comm_group_by_axis(self, axis):
897 """Return the communication group for the specified mesh axis via the underlying DeviceMesh."""
898 return self._mesh.get_comm_group_by_axis(axis)
900 def repeat_num(self):
901 """
902 Number of repeated placements.
903 For example:
904 layout = Layout((2, 4), ("dp", "mp"))
905 x_layout = layout("dp", "None")
906 The repeat_num is equal to all device num 8 divided by device num corresponding to used axis 2, that is 4.
907 """
908 if self._tensor_map is None:
909 raise ValueError(f"The tensor_map is None, the mesh_shape is {self._mesh.mesh_shape},"
910 f" alias_name is {self._mesh.mesh_dim_names}")
912 all_device_num = functools.reduce(lambda x, y: x * y, self._mesh.mesh_shape)
913 used_dev_num = 1
914 for ele in self._tensor_map:
915 if isinstance(ele, tuple):
916 for item in ele:
917 if item >= 0:
918 used_dev_num *= self._mesh.mesh_shape[len(self._mesh.mesh_shape) - item - 1]
919 continue
920 if ele >= 0:
921 used_dev_num *= self._mesh.mesh_shape[len(self._mesh.mesh_shape) - ele - 1]
923 return all_device_num // used_dev_num
925 def _to_compact_string(self):
926 """
927 generate dict key
929 Returns:
930 str: string for compact
931 """
932 mesh_key = self._mesh.to_hash()
933 hash_key = (
934 self._tensor_map,
935 self.partial,
936 self.uneven_shard_mesh_dims,
937 self._tensor_shape,
938 self._tensor_stride,
939 str(self._tensor_dtype),
940 )
941 hash_key += mesh_key
942 return str(hash_key)
944 @property
945 def compact_str(self):
946 """Return the cached compact string representation of this layout."""
947 return self._compact_str
949 def update_compact_str(self):
950 """Recompute and store the compact string representation of this layout."""
951 self._compact_str = self._to_compact_string()
953 def to_string(self):
954 """
955 layout dump
957 Returns:
958 str: layout string
959 """
960 device_info = f"Mesh shape: {self._mesh.mesh_shape}"
961 alias_info = f"Alias Names: {self._mesh.mesh_dim_names}"
962 rank_info = f"Rank List: {self._rank_list}"
963 partial_info = f"Partial: {self.partial}"
965 if self._tensor_map is None:
966 tensor_info = "Tensor Map: Not configured"
967 else:
968 readable_map = []
969 for item in self._tensor_map:
970 if isinstance(item, tuple):
971 # handle nested tuple
972 mapped_tuple = tuple(
973 self._mesh.mesh_dim_names[len(self._mesh.mesh_dim_names) - 1 - dim] if dim != -1 else "None"
974 for dim in item
975 )
976 readable_map.append(mapped_tuple)
977 else:
978 readable_map.append(
979 self._mesh.mesh_dim_names[len(self._mesh.mesh_dim_names) - 1 - item] if item != -1 else "None"
980 )
982 tensor_info = f"Tensor Map: {tuple(readable_map)}"
984 interleaved = "Yes" if "interleaved_parallel" in self._mesh.mesh_dim_names else "No"
985 interleaved_info = f"Interleaved Parallel: {interleaved}"
987 return (
988 f"Layout Configuration:\n"
989 f" {device_info}\n"
990 f" {alias_info}\n"
991 f" {partial_info}\n"
992 f" {tensor_info}\n"
993 f" {interleaved_info}\n"
994 f" {rank_info}"
995 )
997 def __str__(self):
998 """__str__"""
999 return self.to_string()
1001 def __repr__(self):
1002 """__repr__"""
1003 return f"<Layout at {hex(id(self))}>"
1005 def __eq__(self, other):
1006 """
1007 __eq__
1008 """
1009 if not isinstance(other, Layout):
1010 return False
1012 same_layout_attrs = (
1013 self.mesh_shape,
1014 self.alias_name,
1015 self.partial,
1016 self.rank_list,
1017 self.uneven_shard_mesh_dims,
1018 self.tensor_shape,
1019 self.tensor_stride,
1020 self.tensor_dtype,
1021 ) == (
1022 other.mesh_shape,
1023 other.alias_name,
1024 other.partial,
1025 other.rank_list,
1026 other.uneven_shard_mesh_dims,
1027 other.tensor_shape,
1028 other.tensor_stride,
1029 other.tensor_dtype,
1030 )
1031 if not same_layout_attrs:
1032 return False
1034 if self._tensor_map is None or other.tensor_map is None:
1035 return self._tensor_map is other.tensor_map
1036 return self._tensor_map == other.tensor_map