Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / checkpoint / region_tensor.py: 90%
508 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +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"""Symbolic checkpoint tensors, which track where every element of a converted tensor is read from.
17A weight conversion cuts, stacks, reorders and reshapes the tensors of a checkpoint into the tensors of
18a model. Given :class:`RegionTensor` inputs instead of real tensors, the same conversion code computes
19no values. For every element of each result it tracks which checkpoint tensor and index the element is
20copied from, and which elementwise scalar operations are applied to it on the way.
21:meth:`RegionTensor.remap_blocks` hands that over as :class:`RemapBlock` entries, which
22:class:`RemapLoadPlanner` loads without ever assembling a whole checkpoint tensor or a whole result.
24What cannot be tracked that way raises :class:`UnsupportedRegionOp`: an operation that combines two
25tensors elementwise, reads values, writes in place, or indexes with tensors.
26"""
27import math
28import operator
29from collections.abc import Callable, Iterator, Mapping, Sequence
30from dataclasses import dataclass, replace
31from functools import partial
32from typing import Any
34import torch # pylint: disable=forbidden-backend-import
36from hyper_parallel.components.checkpoint.remap_planner import RemapBlock
38# A region cut into more blocks than this is not worth describing block by block. The conversion is
39# run on the real tensors instead.
40MAX_BLOCKS = 1 << 16
43class UnsupportedRegionOp(TypeError):
44 """Raised by an operation whose result cannot be described as regions of checkpoint tensors."""
47@dataclass(frozen=True)
48class RegionBlock:
49 """
50 A box of a symbolic tensor, and where in one checkpoint tensor each element of it comes from.
52 Attributes:
53 offsets (tuple[int, ...]): Where the box starts in the symbolic tensor.
54 lengths (tuple[int, ...]): Size of the box, positive in every dimension.
55 source (str): Name of the checkpoint tensor.
56 base (tuple[int, ...]): Checkpoint index of the first element of the box.
57 steps (tuple[tuple[int, ...], ...]): One column per dimension of the symbolic tensor, giving how
58 far along each checkpoint dimension one step along that dimension moves.
59 dtype (torch.dtype): Dtype of the values once ``post`` has been applied.
60 post (tuple[Callable[[Any], Any], ...]): Elementwise operations applied in order. Default ().
61 """
63 offsets: tuple[int, ...]
64 lengths: tuple[int, ...]
65 source: str
66 base: tuple[int, ...]
67 steps: tuple[tuple[int, ...], ...]
68 dtype: torch.dtype
69 post: tuple[Callable[[Any], Any], ...] = ()
72def _replaced(values: tuple, index: int, value: Any) -> tuple:
73 """``values`` with the entry at ``index`` replaced by ``value``."""
74 return values[:index] + (value,) + values[index + 1:]
77def _strides(sizes: Sequence[int]) -> tuple[int, ...]:
78 """How many elements one step along each of ``sizes`` skips, in row-major order."""
79 strides, stride = [], 1
80 for size in reversed(sizes):
81 strides.append(stride)
82 stride *= size
83 return tuple(reversed(strides))
86def _check_count(count: int) -> None:
87 """Refuse to go on describing a region cut into more than :data:`MAX_BLOCKS` blocks."""
88 if count > MAX_BLOCKS:
89 raise UnsupportedRegionOp(f"the result is cut into more than {MAX_BLOCKS} blocks")
92def _restrict(block: RegionBlock, axis: int, begin: int, length: int) -> RegionBlock:
93 """The part of ``block`` from ``begin`` to ``begin + length`` along ``axis``, in the same coordinates."""
94 delta = begin - block.offsets[axis]
95 return replace(
96 block,
97 offsets=_replaced(block.offsets, axis, begin),
98 lengths=_replaced(block.lengths, axis, length),
99 base=tuple(origin + step * delta for origin, step in zip(block.base, block.steps[axis])),
100 )
103def _slice_axis(blocks: Sequence[RegionBlock], axis: int, start: int, stop: int, step: int) -> list[RegionBlock]:
104 """
105 The blocks of ``tensor[..., start:stop:step, ...]`` along ``axis``, in the coordinates of the slice.
107 Args:
108 blocks (Sequence[RegionBlock]): Blocks of the tensor being sliced.
109 axis (int): Dimension to slice.
110 start (int): First index kept, already clamped into the dimension.
111 stop (int): Index the slice stops before, already clamped into the dimension.
112 step (int): Positive distance between the indices kept.
114 Returns:
115 list[RegionBlock]: The part of every block the slice keeps.
116 """
117 sliced = []
118 for block in blocks:
119 low = max(block.offsets[axis], start)
120 high = min(block.offsets[axis] + block.lengths[axis], stop)
121 if low >= high:
122 continue
123 first = -(-(low - start) // step)
124 last = (high - 1 - start) // step
125 if first > last:
126 continue
127 delta = start + step * first - block.offsets[axis]
128 column = block.steps[axis]
129 sliced.append(replace(
130 block,
131 offsets=_replaced(block.offsets, axis, first),
132 lengths=_replaced(block.lengths, axis, last - first + 1),
133 base=tuple(origin + c * delta for origin, c in zip(block.base, column)),
134 steps=_replaced(block.steps, axis, tuple(c * step for c in column)),
135 ))
136 return sliced
139def _drop_axis(block: RegionBlock, axis: int) -> RegionBlock:
140 """``block`` without the dimension ``axis``, along which it is one element long."""
141 return replace(
142 block,
143 offsets=block.offsets[:axis] + block.offsets[axis + 1:],
144 lengths=block.lengths[:axis] + block.lengths[axis + 1:],
145 steps=block.steps[:axis] + block.steps[axis + 1:],
146 )
149def _insert_axis(block: RegionBlock, axis: int) -> RegionBlock:
150 """``block`` with a new dimension of size one at ``axis``."""
151 return replace(
152 block,
153 offsets=block.offsets[:axis] + (0,) + block.offsets[axis:],
154 lengths=block.lengths[:axis] + (1,) + block.lengths[axis:],
155 steps=block.steps[:axis] + ((0,) * len(block.base),) + block.steps[axis:],
156 )
159def _shift(block: RegionBlock, axis: int, distance: int) -> RegionBlock:
160 """``block`` moved ``distance`` elements along ``axis``."""
161 return replace(block, offsets=_replaced(block.offsets, axis, block.offsets[axis] + distance))
164def _permute_block(block: RegionBlock, order: Sequence[int]) -> RegionBlock:
165 """``block`` with its dimensions in ``order``."""
166 return replace(
167 block,
168 offsets=tuple(block.offsets[axis] for axis in order),
169 lengths=tuple(block.lengths[axis] for axis in order),
170 steps=tuple(block.steps[axis] for axis in order),
171 )
174def _merge_axes(blocks: Sequence[RegionBlock], start: int, sizes: Sequence[int]) -> list[RegionBlock]:
175 """
176 The blocks of a tensor whose dimensions ``start`` to ``start + len(sizes)`` are flattened into one.
178 A block stays whole when it covers consecutive flat indices and walks the checkpoint the same way
179 along them. That holds when every dimension after its first varying one is covered whole and moves
180 through the checkpoint as a multiple of the last. Any other block is cut into pieces one element
181 long along its first varying dimension, and those pieces are merged in turn.
183 Args:
184 blocks (Sequence[RegionBlock]): Blocks of the tensor.
185 start (int): First dimension to flatten.
186 sizes (Sequence[int]): Sizes of the dimensions to flatten, all larger than one.
188 Returns:
189 list[RegionBlock]: Blocks of the flattened tensor.
190 """
191 stop = start + len(sizes)
192 strides = _strides(sizes)
193 merged, pending = [], list(blocks)
194 while pending:
195 block = pending.pop()
196 varying = [axis for axis in range(len(sizes)) if block.lengths[start + axis] > 1]
197 column = block.steps[stop - 1] if varying else (0,) * len(block.base)
198 length = 1
199 if varying:
200 first = varying[0]
201 whole = all(
202 block.offsets[start + axis] == 0 and block.lengths[start + axis] == sizes[axis]
203 for axis in range(first + 1, len(sizes))
204 )
205 in_step = all(
206 block.steps[start + axis] == tuple(c * strides[axis] for c in column)
207 for axis in range(first, len(sizes))
208 )
209 if not (whole and in_step):
210 origin = block.offsets[start + first]
211 pending.extend(
212 _restrict(block, start + first, origin + index, 1)
213 for index in range(block.lengths[start + first])
214 )
215 _check_count(len(merged) + len(pending))
216 continue
217 length = block.lengths[start + first] * strides[first]
218 offset = sum(index * stride for index, stride in zip(block.offsets[start:stop], strides))
219 merged.append(replace(
220 block,
221 offsets=block.offsets[:start] + (offset,) + block.offsets[stop:],
222 lengths=block.lengths[:start] + (length,) + block.lengths[stop:],
223 steps=block.steps[:start] + (column,) + block.steps[stop:],
224 ))
225 return merged
228def _decompose(start: int, length: int, sizes: Sequence[int]) -> list[tuple[tuple[int, ...], tuple[int, ...]]]:
229 """
230 Cut a run of flat indices over dimensions of ``sizes`` into boxes.
232 Args:
233 start (int): First flat index of the run.
234 length (int): Number of flat indices in the run.
235 sizes (Sequence[int]): Sizes of the dimensions the flat index is spread over.
237 Returns:
238 list[tuple[tuple[int, ...], tuple[int, ...]]]: ``(offsets, lengths)`` of every box: a partial
239 first row, the whole rows, then a partial last row.
240 """
241 if len(sizes) == 1:
242 return [((start,), (length,))]
243 inner = tuple(sizes[1:])
244 stride = math.prod(inner)
245 boxes = []
246 row, column = divmod(start, stride)
247 if column:
248 head = min(stride - column, length)
249 boxes.extend(((row,) + offsets, (1,) + lengths) for offsets, lengths in _decompose(column, head, inner))
250 row, length = row + 1, length - head
251 rows, tail = divmod(length, stride)
252 if rows:
253 boxes.append(((row,) + (0,) * len(inner), (rows,) + inner))
254 if tail:
255 boxes.extend(((row + rows,) + offsets, (1,) + lengths) for offsets, lengths in _decompose(0, tail, inner))
256 return boxes
259def _split_axis(blocks: Sequence[RegionBlock], axis: int, sizes: Sequence[int]) -> list[RegionBlock]:
260 """
261 The blocks of a tensor whose dimension ``axis`` is unflattened into dimensions of ``sizes``.
263 Args:
264 blocks (Sequence[RegionBlock]): Blocks of the tensor.
265 axis (int): Dimension to unflatten.
266 sizes (Sequence[int]): Sizes it is unflattened into, all larger than one.
268 Returns:
269 list[RegionBlock]: Blocks of the unflattened tensor.
270 """
271 strides = _strides(sizes)
272 pieces = []
273 for block in blocks:
274 column = block.steps[axis]
275 scaled = tuple(tuple(c * stride for c in column) for stride in strides)
276 for offsets, lengths in _decompose(block.offsets[axis], block.lengths[axis], sizes):
277 delta = sum(index * stride for index, stride in zip(offsets, strides)) - block.offsets[axis]
278 pieces.append(replace(
279 block,
280 offsets=block.offsets[:axis] + offsets + block.offsets[axis + 1:],
281 lengths=block.lengths[:axis] + lengths + block.lengths[axis + 1:],
282 base=tuple(origin + c * delta for origin, c in zip(block.base, column)),
283 steps=block.steps[:axis] + scaled + block.steps[axis + 1:],
284 ))
285 return pieces
288def _runs(old: Sequence[int], new: Sequence[int]) -> list[tuple[int, int, int, int]]:
289 """
290 Match the dimensions of two shapes up into runs holding as many elements on either side.
292 Args:
293 old (Sequence[int]): Sizes of one shape, none of them one.
294 new (Sequence[int]): Sizes of the other, none of them one, holding as many elements in all.
296 Returns:
297 list[tuple[int, int, int, int]]: ``(old start, old stop, new start, new stop)`` of every run.
298 """
299 runs, i, j = [], 0, 0
300 while i < len(old):
301 i0, j0 = i, j
302 old_size, new_size = old[i], new[j]
303 i, j = i + 1, j + 1
304 while old_size != new_size:
305 if old_size < new_size:
306 old_size, i = old_size * old[i], i + 1
307 else:
308 new_size, j = new_size * new[j], j + 1
309 runs.append((i0, i, j0, j))
310 return runs
313def _non_singleton_shape(shape: tuple[int, ...]) -> list[int]:
314 """Return dimensions retained by the reshape run matching algorithm."""
315 return [size for size in shape if size != 1]
318def _reshape(
319 blocks: Sequence[RegionBlock], old_shape: tuple[int, ...], new_shape: tuple[int, ...]
320) -> list[RegionBlock]:
321 """
322 The blocks of a tensor of ``old_shape`` reshaped to ``new_shape``, which holds as many elements.
324 Dimensions of size one are dropped first and put back last. The rest are matched up into runs
325 holding as many elements on either side, and each run is flattened and then unflattened.
327 Args:
328 blocks (Sequence[RegionBlock]): Blocks of the tensor.
329 old_shape (tuple[int, ...]): Its shape.
330 new_shape (tuple[int, ...]): The shape it is reshaped to.
332 Returns:
333 list[RegionBlock]: Blocks of the reshaped tensor.
334 """
335 if math.prod(old_shape) == 0:
336 return []
337 blocks = list(blocks)
338 for axis in reversed([axis for axis, size in enumerate(old_shape) if size == 1]):
339 blocks = [_drop_axis(block, axis) for block in blocks]
340 old = _non_singleton_shape(old_shape)
341 new = _non_singleton_shape(new_shape)
342 for i0, i1, j0, j1 in reversed(_runs(old, new)):
343 if i1 - i0 > 1:
344 blocks = _merge_axes(blocks, i0, old[i0:i1])
345 if j1 - j0 > 1:
346 blocks = _split_axis(blocks, i0, new[j0:j1])
347 _check_count(len(blocks))
348 for axis, size in enumerate(new_shape):
349 if size == 1:
350 blocks = [_insert_axis(block, axis) for block in blocks]
351 return blocks
354def _shape_argument(shape: tuple) -> tuple:
355 """A shape passed either as one sequence or as separate sizes."""
356 if len(shape) == 1 and isinstance(shape[0], (tuple, list, torch.Size)):
357 return tuple(shape[0])
358 return tuple(shape)
361def _expand_index(index: Any, ndim: int) -> list:
362 """
363 The items of an index into a region, with Ellipsis spelled out as whole slices.
365 Args:
366 index (Any): What ``__getitem__`` was given.
367 ndim (int): Number of dimensions of the region.
369 Returns:
370 list: Integers, slices and None, one integer or slice per dimension indexed.
372 Raises:
373 UnsupportedRegionOp: If the index holds anything but integers, slices, None and Ellipsis.
374 """
375 items = index if isinstance(index, tuple) else (index,)
376 supported = (int, slice, type(None), type(Ellipsis))
377 if any(isinstance(item, bool) or not isinstance(item, supported) for item in items):
378 raise UnsupportedRegionOp(f"indexing a region with {index!r} is not supported")
379 consumed = sum(1 for item in items if isinstance(item, (int, slice)))
380 expanded = []
381 for item in items:
382 expanded.extend([slice(None)] * (ndim - consumed) if item is Ellipsis else [item])
383 return expanded
386def _check_scalar(other: Any) -> None:
387 """Refuse an operand that is not a Python number."""
388 if not isinstance(other, (int, float)):
389 raise UnsupportedRegionOp(f"only Python numbers can be combined with a region, got {type(other).__name__}")
392def _to_dtype(tensor: Any, dtype: torch.dtype) -> Any:
393 """``tensor`` converted to ``dtype``."""
394 return tensor.to(dtype)
397def _forward(function: Callable[[Any, Any], Any], other: Any, tensor: Any) -> Any:
398 """``function(tensor, other)``."""
399 return function(tensor, other)
402def _reflected(function: Callable[[Any, Any], Any], other: Any, tensor: Any) -> Any:
403 """``function(other, tensor)``."""
404 return function(other, tensor)
407def _divide(other: Any, rounding_mode: str, tensor: Any) -> Any:
408 """``tensor / other``, rounded as ``rounding_mode`` says."""
409 return torch.div(tensor, other, rounding_mode=rounding_mode)
412def _unsupported(name: str) -> Callable[..., Any]:
413 """A method that refuses the operation ``name``."""
415 def refuse(self: "RegionTensor", *args: Any, **kwargs: Any) -> Any:
416 """Refuse an operation a region cannot describe."""
417 del args, kwargs
418 raise UnsupportedRegionOp(f"{name} is not supported on {self!r}")
420 refuse.__name__ = name
421 return refuse
424class RegionTensor:
425 """
426 A tensor that knows, for each of its elements, which checkpoint tensor and index it is read from.
428 Supports what weight conversions do to lay tensors out differently: concatenating, stacking,
429 splitting, slicing, reordering and reshaping, plus elementwise arithmetic with Python scalars and
430 dtype conversions, as methods and through the matching ``torch`` functions. Shapes and dtypes follow
431 torch exactly, since every operation is first run on a meta tensor of the same shape and dtype.
433 Example::
435 q = RegionTensor.leaf("q_proj.weight", (8, 16), torch.bfloat16)
436 k = RegionTensor.leaf("k_proj.weight", (4, 16), torch.bfloat16)
437 fused = torch.cat([q, k])
438 fused.remap_blocks() # two blocks: rows 0-7 read from q_proj, rows 8-11 from k_proj
439 """
441 __slots__ = ("_shape", "_dtype", "_blocks")
443 def __init__(self, shape: Sequence[int], dtype: torch.dtype, blocks: Sequence[RegionBlock] = ()) -> None:
444 """
445 Args:
446 shape (Sequence[int]): Shape of the tensor.
447 dtype (torch.dtype): Dtype of the tensor.
448 blocks (Sequence[RegionBlock]): Blocks tiling the tensor. Default (), for an empty tensor.
449 """
450 self._shape = tuple(int(size) for size in shape)
451 self._dtype = dtype
452 self._blocks = tuple(blocks)
453 _check_count(len(self._blocks))
455 @classmethod
456 def leaf(cls, source: str, shape: Sequence[int], dtype: torch.dtype) -> "RegionTensor":
457 """
458 The whole of one checkpoint tensor.
460 Args:
461 source (str): Name of the checkpoint tensor.
462 shape (Sequence[int]): Its shape.
463 dtype (torch.dtype): Its dtype.
465 Returns:
466 RegionTensor: A tensor of one block reading the checkpoint tensor in place.
467 """
468 shape = tuple(int(size) for size in shape)
469 ndim = len(shape)
470 blocks = ()
471 if math.prod(shape):
472 identity = tuple(tuple(int(row == column) for row in range(ndim)) for column in range(ndim))
473 blocks = (RegionBlock((0,) * ndim, shape, source, (0,) * ndim, identity, dtype),)
474 return cls(shape, dtype, blocks)
476 @classmethod
477 def __torch_function__(cls, func: Any, types: Any, args: tuple = (), kwargs: Any = None) -> Any:
478 """Route the ``torch`` functions a region supports to its methods, and refuse the rest."""
479 del types
480 kwargs = dict(kwargs or {})
481 name = _FUNCTION_NAMES.get(func)
482 if name is None or kwargs.get("out") is not None:
483 raise UnsupportedRegionOp(f"torch.{getattr(func, '__name__', func)} is not supported on regions")
484 kwargs.pop("out", None)
485 if name in _SEQUENCE_FUNCTIONS:
486 return _SEQUENCE_FUNCTIONS[name](*args, **kwargs)
487 args = list(args)
488 tensor = kwargs.pop("input") if "input" in kwargs else args.pop(0)
489 if isinstance(tensor, RegionTensor):
490 return getattr(tensor, name)(*args, **kwargs)
491 other = kwargs.pop("other") if "other" in kwargs else (args.pop(0) if args else None)
492 if name in _REFLECTED and isinstance(other, RegionTensor) and not args and not kwargs:
493 return getattr(other, _REFLECTED[name])(tensor)
494 raise UnsupportedRegionOp(f"torch.{name} is not supported with {type(tensor).__name__} as its tensor")
496 @property
497 def shape(self) -> torch.Size:
498 """Shape of the tensor."""
499 return torch.Size(self._shape)
501 @property
502 def dtype(self) -> torch.dtype:
503 """Dtype of the tensor."""
504 return self._dtype
506 @property
507 def ndim(self) -> int:
508 """Number of dimensions."""
509 return len(self._shape)
511 @property
512 def device(self) -> torch.device:
513 """The meta device: a region holds no values anywhere."""
514 return torch.device("meta")
516 @property
517 def requires_grad(self) -> bool:
518 """Always False."""
519 return False
521 @property
522 def blocks(self) -> tuple[RegionBlock, ...]:
523 """Blocks tiling the tensor."""
524 return self._blocks
526 def sources(self) -> frozenset[str]:
527 """Names of the checkpoint tensors the elements are read from."""
528 return frozenset(block.source for block in self._blocks)
530 def remap_blocks(self) -> list[RemapBlock]:
531 """
532 The blocks as :class:`RemapLoadPlanner` loads them.
534 A block kept in another dtype than the tensor, which a concatenation of tensors of different
535 dtypes leaves behind, is converted to the dtype of the tensor last, as the conversion would.
537 Returns:
538 list[RemapBlock]: One entry per block.
539 """
540 remapped = []
541 for block in self._blocks:
542 post = block.post
543 if block.dtype != self._dtype:
544 post += (partial(_to_dtype, dtype=self._dtype),)
545 coeff = tuple(tuple(column[dim] for column in block.steps) for dim in range(len(block.base)))
546 remapped.append(RemapBlock(block.offsets, block.lengths, block.source, block.base, coeff, post))
547 return remapped
549 def size(self, dim: Any = None) -> Any:
550 """The shape, or the size of dimension ``dim``."""
551 return self.shape if dim is None else self._meta().size(dim)
553 def dim(self) -> int:
554 """Number of dimensions."""
555 return len(self._shape)
557 def numel(self) -> int:
558 """Number of elements."""
559 return math.prod(self._shape)
561 def nelement(self) -> int:
562 """Number of elements."""
563 return self.numel()
565 def element_size(self) -> int:
566 """Bytes one element takes."""
567 return self._meta().element_size()
569 def is_floating_point(self) -> bool:
570 """Whether the dtype is a floating point one."""
571 return self._dtype.is_floating_point
573 def is_complex(self) -> bool:
574 """Whether the dtype is a complex one."""
575 return self._dtype.is_complex
577 def __len__(self) -> int:
578 """Size of the first dimension."""
579 if not self._shape:
580 raise TypeError("len() of a 0-d tensor")
581 return self._shape[0]
583 def __iter__(self) -> Iterator["RegionTensor"]:
584 """The tensor one index of its first dimension at a time."""
585 if not self._shape:
586 raise TypeError("iteration over a 0-d tensor")
587 return iter(self.unbind(0))
589 def __repr__(self) -> str:
590 """Shape, dtype and blocks."""
591 return f"RegionTensor(shape={self._shape}, dtype={self._dtype}, blocks={len(self._blocks)})"
593 def reshape(self, *shape: Any) -> "RegionTensor":
594 """The tensor reshaped, as ``torch.reshape`` does it."""
595 return self._reshaped(tuple(self._meta().reshape(_shape_argument(shape)).shape))
597 def view(self, *shape: Any) -> "RegionTensor":
598 """The tensor reshaped. Viewing it as another dtype is not supported."""
599 if len(shape) == 1 and isinstance(shape[0], torch.dtype):
600 raise UnsupportedRegionOp("viewing a region as another dtype reinterprets its bytes")
601 return self.reshape(*shape)
603 def reshape_as(self, other: Any) -> "RegionTensor":
604 """The tensor reshaped to the shape of ``other``."""
605 return self.reshape(tuple(other.shape))
607 def view_as(self, other: Any) -> "RegionTensor":
608 """The tensor reshaped to the shape of ``other``."""
609 return self.reshape(tuple(other.shape))
611 def flatten(self, start_dim: int = 0, end_dim: int = -1) -> "RegionTensor":
612 """The tensor with dimensions ``start_dim`` to ``end_dim`` flattened into one."""
613 return self._reshaped(tuple(self._meta().flatten(start_dim, end_dim).shape))
615 def unflatten(self, dim: int, sizes: Sequence[int]) -> "RegionTensor":
616 """The tensor with dimension ``dim`` unflattened into ``sizes``."""
617 return self._reshaped(tuple(self._meta().unflatten(dim, sizes).shape))
619 def transpose(self, dim0: int, dim1: int) -> "RegionTensor":
620 """The tensor with dimensions ``dim0`` and ``dim1`` swapped."""
621 self._meta().transpose(dim0, dim1)
622 if not self._shape:
623 return self
624 order = list(range(len(self._shape)))
625 first, second = dim0 % len(order), dim1 % len(order)
626 order[first], order[second] = order[second], order[first]
627 return self._permuted(order)
629 def swapaxes(self, axis0: int, axis1: int) -> "RegionTensor":
630 """The tensor with dimensions ``axis0`` and ``axis1`` swapped."""
631 return self.transpose(axis0, axis1)
633 def swapdims(self, dim0: int, dim1: int) -> "RegionTensor":
634 """The tensor with dimensions ``dim0`` and ``dim1`` swapped."""
635 return self.transpose(dim0, dim1)
637 def t(self) -> "RegionTensor":
638 """The tensor with its two dimensions swapped, or itself when it has fewer."""
639 self._meta().t()
640 return self.transpose(0, 1) if len(self._shape) == 2 else self
642 def permute(self, *dims: Any) -> "RegionTensor":
643 """The tensor with its dimensions reordered."""
644 dims = _shape_argument(dims)
645 self._meta().permute(dims)
646 return self._permuted([dim % len(self._shape) for dim in dims]) if self._shape else self
648 def squeeze(self, dim: Any = None) -> "RegionTensor":
649 """The tensor without the dimensions of size one ``dim`` names, or without all of them."""
650 expected = tuple((self._meta().squeeze() if dim is None else self._meta().squeeze(dim)).shape)
651 if not self._shape:
652 return self
653 if dim is None:
654 axes = {axis for axis, size in enumerate(self._shape) if size == 1}
655 else:
656 dims = dim if isinstance(dim, (tuple, list)) else (dim,)
657 axes = {d % len(self._shape) for d in dims if self._shape[d % len(self._shape)] == 1}
658 blocks = list(self._blocks)
659 for axis in sorted(axes, reverse=True):
660 blocks = [_drop_axis(block, axis) for block in blocks]
661 return self._checked(expected, blocks)
663 def unsqueeze(self, dim: int) -> "RegionTensor":
664 """The tensor with a new dimension of size one at ``dim``."""
665 expected = tuple(self._meta().unsqueeze(dim).shape)
666 axis = dim % (len(self._shape) + 1)
667 return self._checked(expected, [_insert_axis(block, axis) for block in self._blocks])
669 def split(self, split_size_or_sections: Any, dim: int = 0) -> tuple["RegionTensor", ...]:
670 """The tensor cut along ``dim``, as ``torch.split`` cuts it."""
671 return self._pieces(self._meta().split(split_size_or_sections, dim), dim)
673 def chunk(self, chunks: int, dim: int = 0) -> tuple["RegionTensor", ...]:
674 """The tensor cut along ``dim``, as ``torch.chunk`` cuts it."""
675 return self._pieces(self._meta().chunk(chunks, dim), dim)
677 def unbind(self, dim: int = 0) -> tuple["RegionTensor", ...]:
678 """Every index of dimension ``dim``, without that dimension."""
679 size = self._meta().size(dim)
680 return tuple(self.select(dim, index) for index in range(size))
682 def narrow(self, dim: int, start: int, length: int) -> "RegionTensor":
683 """``length`` indices of dimension ``dim`` from ``start``."""
684 expected = tuple(self._meta().narrow(dim, start, length).shape)
685 axis = dim % len(self._shape)
686 start = start + self._shape[axis] if start < 0 else start
687 return self._checked(expected, _slice_axis(self._blocks, axis, start, start + length, 1))
689 def select(self, dim: int, index: int) -> "RegionTensor":
690 """Index ``index`` of dimension ``dim``, without that dimension."""
691 expected = tuple(self._meta().select(dim, index).shape)
692 axis = dim % len(self._shape)
693 index = index + self._shape[axis] if index < 0 else index
694 blocks = [_drop_axis(block, axis) for block in _slice_axis(self._blocks, axis, index, index + 1, 1)]
695 return self._checked(expected, blocks)
697 def __getitem__(self, index: Any) -> "RegionTensor":
698 """The part of the tensor integers, slices with a positive step, None and Ellipsis pick out."""
699 expanded = _expand_index(index, len(self._shape))
700 expected = tuple(self._meta()[index].shape)
701 shape, blocks, axis = list(self._shape), list(self._blocks), 0
702 for item in expanded:
703 if item is None:
704 blocks = [_insert_axis(block, axis) for block in blocks]
705 shape.insert(axis, 1)
706 axis += 1
707 elif isinstance(item, slice):
708 start, stop, step = item.indices(shape[axis])
709 if step <= 0:
710 raise UnsupportedRegionOp("slicing a region with a negative step is not supported")
711 blocks = _slice_axis(blocks, axis, start, stop, step)
712 shape[axis] = len(range(start, stop, step))
713 axis += 1
714 else:
715 position = item + shape[axis] if item < 0 else item
716 blocks = [_drop_axis(block, axis) for block in _slice_axis(blocks, axis, position, position + 1, 1)]
717 del shape[axis]
718 return self._checked(expected, blocks)
720 def contiguous(self, *args: Any, **kwargs: Any) -> "RegionTensor":
721 """The tensor itself: a region has no memory layout."""
722 del args, kwargs
723 return self
725 def clone(self, *args: Any, **kwargs: Any) -> "RegionTensor":
726 """The tensor itself: a region is never written to."""
727 del args, kwargs
728 return self
730 def detach(self) -> "RegionTensor":
731 """The tensor itself."""
732 return self
734 def cpu(self) -> "RegionTensor":
735 """The tensor itself: moving it does not change where its values come from."""
736 return self
738 def to(self, *args: Any, **kwargs: Any) -> "RegionTensor":
739 """The tensor converted to the dtype among the arguments, if any. Devices are ignored."""
740 dtype = kwargs.get("dtype")
741 for arg in args:
742 if isinstance(arg, (RegionTensor, torch.Tensor)):
743 raise UnsupportedRegionOp("converting a region to the dtype of another tensor is not supported")
744 if isinstance(arg, torch.dtype):
745 dtype = arg
746 return self if dtype is None else self._cast(dtype)
748 def add(self, other: Any) -> "RegionTensor":
749 """``self + other``."""
750 return self._arithmetic(operator.add, other, reflected=False)
752 def sub(self, other: Any) -> "RegionTensor":
753 """``self - other``."""
754 return self._arithmetic(operator.sub, other, reflected=False)
756 def mul(self, other: Any) -> "RegionTensor":
757 """``self * other``."""
758 return self._arithmetic(operator.mul, other, reflected=False)
760 def div(self, other: Any, *, rounding_mode: Any = None) -> "RegionTensor":
761 """``self / other``, rounded as ``rounding_mode`` says."""
762 if rounding_mode is None:
763 return self._arithmetic(operator.truediv, other, reflected=False)
764 _check_scalar(other)
765 scalar = torch.empty((), dtype=self._dtype, device="meta")
766 dtype = torch.div(scalar, other, rounding_mode=rounding_mode).dtype
767 return self._elementwise(partial(_divide, other, rounding_mode), dtype)
769 def neg(self) -> "RegionTensor":
770 """``-self``."""
771 return self._elementwise(operator.neg, (-torch.empty((), dtype=self._dtype, device="meta")).dtype)
773 subtract = sub
774 multiply = mul
775 divide = div
776 true_divide = div
777 negative = neg
778 __add__ = add
779 __sub__ = sub
780 __mul__ = mul
781 __truediv__ = div
782 __neg__ = neg
784 def __radd__(self, other: Any) -> "RegionTensor":
785 """``other + self``."""
786 return self._arithmetic(operator.add, other, reflected=True)
788 def __rsub__(self, other: Any) -> "RegionTensor":
789 """``other - self``."""
790 return self._arithmetic(operator.sub, other, reflected=True)
792 def __rmul__(self, other: Any) -> "RegionTensor":
793 """``other * self``."""
794 return self._arithmetic(operator.mul, other, reflected=True)
796 def __rtruediv__(self, other: Any) -> "RegionTensor":
797 """``other / self``."""
798 return self._arithmetic(operator.truediv, other, reflected=True)
800 def __pos__(self) -> "RegionTensor":
801 """The tensor itself."""
802 return self
804 __hash__ = object.__hash__
805 __eq__ = _unsupported("__eq__")
806 __ne__ = _unsupported("__ne__")
807 __lt__ = _unsupported("__lt__")
808 __le__ = _unsupported("__le__")
809 __gt__ = _unsupported("__gt__")
810 __ge__ = _unsupported("__ge__")
811 __bool__ = _unsupported("__bool__")
812 __index__ = _unsupported("__index__")
813 __setitem__ = _unsupported("__setitem__")
814 __iadd__ = _unsupported("__iadd__")
815 __isub__ = _unsupported("__isub__")
816 __imul__ = _unsupported("__imul__")
817 __itruediv__ = _unsupported("__itruediv__")
818 __pow__ = _unsupported("__pow__")
819 item = _unsupported("item")
820 tolist = _unsupported("tolist")
821 numpy = _unsupported("numpy")
822 copy_ = _unsupported("copy_")
823 add_ = _unsupported("add_")
824 mul_ = _unsupported("mul_")
826 def _meta(self) -> torch.Tensor:
827 """A meta tensor of the same shape and dtype, which torch validates arguments against."""
828 return torch.empty(self._shape, dtype=self._dtype, device="meta")
830 def _checked(self, expected: tuple[int, ...], blocks: Sequence[RegionBlock]) -> "RegionTensor":
831 """A tensor of these blocks, whose shape the blocks' dimensions have to agree with."""
832 ndim = len(expected)
833 if any(len(block.offsets) != ndim for block in blocks):
834 raise UnsupportedRegionOp(f"blocks disagree with the shape {expected} torch computes")
835 return RegionTensor(expected, self._dtype, blocks)
837 def _reshaped(self, shape: tuple[int, ...]) -> "RegionTensor":
838 """The tensor reshaped to ``shape``, already validated by torch."""
839 if shape == self._shape:
840 return self
841 return self._checked(shape, _reshape(self._blocks, self._shape, shape))
843 def _permuted(self, order: Sequence[int]) -> "RegionTensor":
844 """The tensor with its dimensions in ``order``."""
845 shape = tuple(self._shape[axis] for axis in order)
846 return RegionTensor(shape, self._dtype, [_permute_block(block, order) for block in self._blocks])
848 def _pieces(self, metas: Sequence[torch.Tensor], dim: int) -> tuple["RegionTensor", ...]:
849 """The tensor cut along ``dim`` into pieces as long as ``metas``, torch's own cut of it."""
850 axis = dim % len(self._shape)
851 pieces, start = [], 0
852 for meta in metas:
853 size = meta.shape[axis]
854 pieces.append(self._checked(tuple(meta.shape), _slice_axis(self._blocks, axis, start, start + size, 1)))
855 start += size
856 return tuple(pieces)
858 def _arithmetic(self, function: Callable[[Any, Any], Any], other: Any, reflected: bool) -> "RegionTensor":
859 """The tensor combined elementwise with a Python number."""
860 _check_scalar(other)
861 scalar = torch.empty((), dtype=self._dtype, device="meta")
862 dtype = (function(other, scalar) if reflected else function(scalar, other)).dtype
863 operation = partial(_reflected if reflected else _forward, function, other)
864 return self._elementwise(operation, dtype)
866 def _elementwise(self, operation: Callable[[Any], Any], dtype: torch.dtype) -> "RegionTensor":
867 """
868 The tensor with ``operation`` applied to every element.
870 A block still in another dtype than the tensor is converted to it first, so the operation sees
871 the values in the dtype it would see them in.
872 """
873 blocks = []
874 for block in self._blocks:
875 post = block.post
876 if block.dtype != self._dtype:
877 post += (partial(_to_dtype, dtype=self._dtype),)
878 blocks.append(replace(block, post=post + (operation,), dtype=dtype))
879 return RegionTensor(self._shape, dtype, blocks)
881 def _cast(self, dtype: torch.dtype) -> "RegionTensor":
882 """The tensor converted to ``dtype``."""
883 if dtype == self._dtype:
884 return self
885 return self._elementwise(partial(_to_dtype, dtype=dtype), dtype)
887 def float(self) -> "RegionTensor":
888 """The tensor converted to float32."""
889 return self._cast(torch.float32)
891 def double(self) -> "RegionTensor":
892 """The tensor converted to float64."""
893 return self._cast(torch.float64)
895 def half(self) -> "RegionTensor":
896 """The tensor converted to float16."""
897 return self._cast(torch.float16)
899 def bfloat16(self) -> "RegionTensor":
900 """The tensor converted to bfloat16."""
901 return self._cast(torch.bfloat16)
903 def int(self) -> "RegionTensor":
904 """The tensor converted to int32."""
905 return self._cast(torch.int32)
907 def long(self) -> "RegionTensor":
908 """The tensor converted to int64."""
909 return self._cast(torch.int64)
912def _regions(tensors: Any, function: str) -> list[RegionTensor]:
913 """The tensors passed to ``function``, which all have to be regions."""
914 tensors = list(tensors)
915 if not tensors or not all(isinstance(tensor, RegionTensor) for tensor in tensors):
916 raise UnsupportedRegionOp(f"torch.{function} of regions mixed with other tensors is not supported")
917 return tensors
920def _cat(tensors: Sequence[Any], dim: int = 0) -> RegionTensor:
921 """Regions concatenated along ``dim``, as ``torch.cat`` concatenates tensors."""
922 tensors = _regions(tensors, "cat")
923 meta = torch.cat([tensor._meta() for tensor in tensors], dim) # pylint: disable=protected-access
924 axis = dim % meta.dim()
925 blocks, offset = [], 0
926 for tensor in tensors:
927 if tensor.ndim != meta.dim():
928 # A one-dimensional empty tensor, which torch.cat leaves out.
929 continue
930 blocks.extend(_shift(block, axis, offset) for block in tensor.blocks)
931 offset += tensor.shape[axis]
932 return RegionTensor(meta.shape, meta.dtype, blocks)
935def _stack(tensors: Sequence[Any], dim: int = 0) -> RegionTensor:
936 """Regions stacked along a new dimension ``dim``, as ``torch.stack`` stacks tensors."""
937 tensors = _regions(tensors, "stack")
938 meta = torch.stack([tensor._meta() for tensor in tensors], dim) # pylint: disable=protected-access
939 axis = dim % meta.dim()
940 blocks = [
941 _shift(_insert_axis(block, axis), axis, position)
942 for position, tensor in enumerate(tensors)
943 for block in tensor.blocks
944 ]
945 return RegionTensor(meta.shape, meta.dtype, blocks)
948_SEQUENCE_FUNCTIONS: dict[str, Callable[..., RegionTensor]] = {
949 "cat": _cat,
950 "concat": _cat,
951 "concatenate": _cat,
952 "stack": _stack,
953}
954_TENSOR_FUNCTIONS = (
955 "reshape", "flatten", "unflatten", "transpose", "swapaxes", "swapdims", "t", "permute", "squeeze",
956 "unsqueeze", "split", "chunk", "unbind", "narrow", "select", "clone", "detach", "add", "sub", "subtract",
957 "mul", "multiply", "div", "divide", "true_divide", "neg", "negative",
958)
959_FUNCTION_NAMES = {
960 getattr(torch, name): name for name in (*_SEQUENCE_FUNCTIONS, *_TENSOR_FUNCTIONS) if hasattr(torch, name)
961}
962_REFLECTED = {
963 "add": "__radd__",
964 "sub": "__rsub__",
965 "subtract": "__rsub__",
966 "mul": "__rmul__",
967 "multiply": "__rmul__",
968 "div": "__rtruediv__",
969 "divide": "__rtruediv__",
970 "true_divide": "__rtruediv__",
971}
974def materialize(region: RegionTensor, sources: Mapping[str, torch.Tensor]) -> torch.Tensor:
975 """
976 Compute the values of a region out of the whole checkpoint tensors it reads.
978 Args:
979 region (RegionTensor): The region.
980 sources (Mapping[str, torch.Tensor]): Every checkpoint tensor the region reads, by name.
982 Returns:
983 torch.Tensor: A new tensor of the region's shape and dtype, on the device of the sources.
984 """
985 device = next((sources[name].device for name in region.sources()), None)
986 result = torch.empty(tuple(region.shape), dtype=region.dtype, device=device)
987 for block in region.remap_blocks():
988 source = sources[block.source]
989 strides = source.stride()
990 stride = tuple(
991 sum(row[axis] * step for row, step in zip(block.coeff, strides)) for axis in range(len(block.lengths))
992 )
993 offset = source.storage_offset() + sum(start * step for start, step in zip(block.base, strides))
994 values = source.as_strided(block.lengths, stride, offset)
995 for operation in block.post:
996 values = operation(values)
997 box = tuple(slice(start, start + length) for start, length in zip(block.offsets, block.lengths))
998 result[box].copy_(values)
999 return result