Files
Hongyi Jin 58b71d78cf [FIX][TIRx][CUDA] Fix tcgen05 register fragment layouts (#20068)
## Motivation and context

`tcgen05.ld/st` with a `.16x*b` atom accesses one 16-row half-slab from
each 32-row TMEM partition owned by a warp. Across a four-warp
warpgroup, the two physical half-slabs are:

| View | Physical TMEM lanes | PTX row immediate |
|---|---|---|
| lower half | `0..15, 32..47, 64..79, 96..111` | `0` |
| upper half | `16..31, 48..63, 80..95, 112..127` | `16` |

Layout D for an M=128 accumulator occupies both halves. Layout F exposes
an M=64 logical tile over one half, which is useful both for a native
M=64 accumulator and for reading either 64-row half of an existing
Layout D accumulator.

Before this PR, `tmem_datapath_layout("F", 64, cols)` could describe
only the lower half. The copy dispatcher classified a TMEM buffer only
as `"D"` or `"F"` and every M=64 `.16x*b` operation started at `row=0`.
As a result, there was no layout-preserving way to create a recognized
64-row view of the upper half of a Layout D accumulator: a normal Layout
F view still addressed the lower half, while a hand-written `+16@TLane`
layout was not recognized by the dispatcher.

The half-slab selection belongs in the buffer layout because layout is
the source of truth for physical placement in TIRx. It should not be an
out-of-band `copy_async` option. This PR therefore records the selection
in Layout F, carries it through datapath classification, and derives the
PTX row immediate from it.

A related invariant is that a default M=128 TMEM allocation must be
structurally identical to named Layout D. The dispatcher recognizes
datapaths structurally, so keeping a separate hand-written default
layout creates an unnecessary drift risk. This PR makes the default call
the public Layout D factory directly.

Finally, the register-side `tcgen05_atom_layout` must agree with the PTX
mapping from a logical `(row, col)` to `(laneid, wid_in_wg, register)`.
A self-consistent load/store round trip is not enough to prove that
mapping: raw PTX can move the same bits back even when the logical
layout label is wrong. Elementwise dispatch does consume that label, so
this PR adds direct mapping and elementwise compilation fences for all
`.16x*b` atom families. These are coverage additions; the production
atom-layout construction itself is unchanged here.

## Changes

- Add `sub_slab={0,1}` to `tmem_datapath_layout("F", ...)`.
- Encode the upper view as a `+16@TLane` offset and reject invalid
selectors, including nonzero selectors for Layout D.
- Classify TMEM layouts as `(datapath, sub_slab)` and emit `.16x*b` with
`row=(sub_slab + slab) * 16`.
- Preserve the existing M=128 behavior: Layout D with a 128-row `.16x*b`
fragment still emits two operations at rows `0` and `16`.
- Build the default M=128 TMEM layout through `tmem_datapath_layout("D",
...)`.
- Document the physical lane mapping and supported datapath/atom
combinations.
- Add direct atom-layout mapping coverage and a warpgroup elementwise
regression.

## Testing

- Static Layout F checks cover every logical row for both `sub_slab=0`
and `sub_slab=1`.
- B200 readback tests populate one Layout D accumulator, then verify
that lower and upper Layout F views reproduce the two corresponding
register halves for `.16x64b`, `.16x128b`, and `.16x256b`.
- Negative tests cover invalid sub-slab values and incompatible
datapath/atom pairings.
- Direct `(row, col) -> (laneid, wid_in_wg, register)` sweeps cover
supported `.16x*b` shapes and repetitions.
- Warpgroup elementwise codegen verifies that an atom-layout fragment
canonicalizes and slices correctly.
- Changed-files pre-commit checks.
2026-07-29 14:06:01 -04:00

1318 lines
51 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=super-init-not-called
"""Definition of layout."""
import functools
import operator
import re
from collections.abc import Sequence
from typing import ClassVar, Optional, Union
import tvm_ffi
import tvm
from tvm.runtime import Object
from tvm.tirx.expr import Expr
from . import _ffi_api
from .exec_scope import ExecScope
def _flatten_coord(coord: list[Expr], shape: list[Expr]) -> Expr:
"""Python mirror of ``src/tirx/ir/layout/utils.cc::FlattenCoord``."""
flat: Expr = 0
for c, s in zip(coord, shape, strict=False):
flat = flat * s + c
return flat
def _split_coord(coord: Expr, extents: list[Expr]) -> list[Expr]:
"""Python mirror of ``src/tirx/ir/layout/utils.cc::SplitCoord``.
Walks ``extents`` from the innermost (last index, ``%``-ed first) toward
the outermost (index 0, gets the final remaining ``//``).
"""
n = len(extents)
if n == 0:
return []
result: list = [None] * n
remaining = coord
for i in range(n - 1, -1, -1):
if i == 0:
result[0] = remaining
else:
result[i] = tvm.tirx.floormod(remaining, extents[i])
remaining = tvm.tirx.floordiv(remaining, extents[i])
return result
@tvm_ffi.register_object("tirx.Layout")
class Layout(Object):
def __init__(self):
self.__init_handle_by_constructor__(_ffi_api.Layout) # pylint: disable=no-member
def verify_well_formed(self) -> bool:
"""Verify if the layout is well-formed.
Returns
-------
bool
True if the layout is well-formed, False otherwise
"""
return _ffi_api.LayoutVerifyWellFormed(self) # pylint: disable=no-member
def size(self, axis_name: str | None = None):
"""Get the size of the layout.
Parameters
----------
axis_name : Optional[str]
The name of the axis to get the size of. If not provided, the default input size will be returned.
""" # noqa: E501
return _ffi_api.LayoutGetSize(self, axis_name) # pylint: disable=no-member
def span(self, axis_name: str | None = None):
"""Get the span of the layout.
Parameters
----------
axis_name : Optional[str]
The name of the axis to get the span of. If not provided, the default span will be returned.
""" # noqa: E501
return _ffi_api.LayoutGetSpan(self, axis_name) # pylint: disable=no-member
# Note: no backward-compat alias; `cosize` is removed.
def apply(self, *coord: list[Expr], shape: list[Expr] | None = None) -> dict[str, Expr]:
"""Apply the layout on the input coordinate and get the mapped output.
Input cases:
- coord is a single element -> will be treated as a 1D coordinate
- coord is a list of elements -> will be treated as a multi-dimensional coordinate
- shape is provided -> turn the coord with shape into a 1D coordinate
- shape is not provided -> use the default shape
Returns
-------
Dict[str, Expr]
The mapped output (axis name -> value on the axis)
"""
if len(coord) == 1:
# assert shape is None, "shape must be None if coord is not a list or tuple"
return _ffi_api.LayoutApplyLinear(self, coord[0]) # pylint: disable=no-member
if shape is None:
return _ffi_api.LayoutApply(self, coord) # pylint: disable=no-member
return _ffi_api.LayoutApplyWithShape(self, coord, shape) # pylint: disable=no-member
def apply_to_shape(self, coord: list[Expr], input_shape: list[Expr]) -> list[Expr]:
"""Compute the per-shard value that each shard would take if ``coord``
were interpreted against ``input_shape``.
Tries ``self.group(input_shape)`` first. On success, each group owns
exactly one ``input_shape`` entry, so ``coord[d]`` can be split
*within* that group's shard extents (bounds stay local to one input
dim — simpler analyzer simplification, no cross-dim complications).
Falls back to ``FlattenCoord(coord, input_shape)`` + ``SplitCoord``
on ``self``'s raw shard shape when the group call fails (e.g. when
``input_shape`` does not align with the layout's factor boundaries).
Returns a list of length ``len(self.shard)``; each entry is the value
that shard would iterate.
"""
try:
grouped, seps = self.group(list(input_shape))
except Exception:
flat = _flatten_coord(coord, input_shape)
return _split_coord(flat, [sh.extent for sh in self.shard])
results: list = [None] * len(grouped.shard)
for d in range(len(input_shape)):
start = seps[d]
end = seps[d + 1]
extents = [grouped.shard[i].extent for i in range(start, end)]
part = _split_coord(coord[d], extents)
for i, c in zip(range(start, end), part, strict=False):
results[i] = c
return results
def canonicalize(self) -> "Layout":
"""Canonicalize the layout by simplifying and fusing iterators where possible.
Returns
-------
Layout
The canonicalized layout
"""
return _ffi_api.LayoutCanonicalize(self) # pylint: disable=no-member
def tile(
self, outer: "TileLayout", outer_shape: list[Expr], inner_shape: list[Expr]
) -> Union["TileLayout", "ComposeLayout"]:
"""Tile the current layout with an outer layout.
Parameters
----------
outer : TileLayout
The outer layout to tile with
outer_shape : List[Expr]
The shape of the outer layout
inner_shape : List[Expr]
The shape of the inner layout
Returns
-------
Union[TileLayout, ComposeLayout]
The resulting tiled layout
"""
return _ffi_api.LayoutTile( # pylint: disable=no-member
self, outer, outer_shape, inner_shape
)
def direct_sum(
self, left: "TileLayout", left_shape: list[Expr], right_shape: list[Expr]
) -> Union["TileLayout", "ComposeLayout"]:
"""Direct-sum on the tiling domain (unscaled composition): A + B.
This layout is treated as the right addend B grouped by `right_shape`.
The `left` layout is treated as A grouped by `left_shape`.
The resulting layout is evaluated over the interleaved domain S_A ⊗ S_B,
without span scaling (unlike tiling).
"""
return _ffi_api.LayoutDirectSum( # pylint: disable=no-member
self, left, left_shape, right_shape
)
def is_tile_inner(
self,
tile_layout: Union["TileLayout", "ComposeLayout"],
tiled_shape: list[Expr],
inner_shape: list[Expr],
) -> Optional["TileLayout"]:
"""Check if a layout is the inner layout of a tiled layout.
Parameters
----------
tile_layout : Union[TileLayout, ComposeLayout]
The tiled layout to check
tiled_shape : List[Expr]
The shape of the tiled layout
inner_shape : List[Expr]
The shape of the inner layout
Returns
-------
Optional[TileLayout]
The outer layout if it is the inner layout of the tiled layout, None otherwise
"""
return _ffi_api.LayoutIsTileInner( # pylint: disable=no-member
self, tile_layout, tiled_shape, inner_shape
)
def is_tile_outer(
self,
tile_layout: Union["TileLayout", "ComposeLayout"],
tiled_shape: list[Expr],
outer_shape: list[Expr],
) -> Optional["Layout"]:
"""Check if a layout is the outer layout of a tiled layout.
Parameters
----------
tile_layout : Union[TileLayout, ComposeLayout]
The tiled layout to check
tiled_shape : List[Expr]
The shape of the tiled layout
outer_shape : List[Expr]
The shape of the outer layout
Returns
-------
Optional[Layout]
The inner layout if it is the outer layout of the tiled layout, None otherwise
"""
return _ffi_api.LayoutIsTileOuter( # pylint: disable=no-member
self, tile_layout, tiled_shape, outer_shape
)
def is_direct_sum_right(
self,
sum_layout: Union["TileLayout", "ComposeLayout"],
interleaved_shape: list[Expr],
right_shape: list[Expr],
) -> Optional["TileLayout"]:
"""Check if this layout is the right addend B in a direct-sum A + B.
Returns the left addend A if recognized, otherwise None.
"""
return _ffi_api.LayoutIsDirectSumRight( # pylint: disable=no-member
self, sum_layout, interleaved_shape, right_shape
)
def is_direct_sum_left(
self,
sum_layout: Union["TileLayout", "ComposeLayout"],
interleaved_shape: list[Expr],
left_shape: list[Expr],
) -> Optional["Layout"]:
"""Check if this layout is the left addend A in a direct-sum A + B.
Returns the right addend B if recognized, otherwise None.
"""
return _ffi_api.LayoutIsDirectSumLeft( # pylint: disable=no-member
self, sum_layout, interleaved_shape, left_shape
)
def slice(self, shape: list[Expr], region: list[tuple[Expr, Expr]]) -> Optional["Layout"]:
"""Slice the layout with a given shape and region.
Parameters
----------
shape : List[Expr]
The shape of the layout
region : List[Tuple[Expr, Expr], tvm.ir.Range]
The region to slice, each element is (begin, end)
Returns
-------
Optional[Layout]
The sliced layout, or None if slicing is not possible
"""
assert len(shape) == len(region), "shape and region must have the same length"
region_list = []
for range_i in region:
if isinstance(range_i, tvm.ir.Range):
region_list.append(range_i)
else:
region_list.append(tvm.ir.Range(range_i[0], range_i[1]))
return _ffi_api.LayoutSlice(self, shape, region_list) # pylint: disable=no-member
def tile_to(self, to_shape: list[Expr], current_shape: list[Expr]) -> "Layout":
"""Tile the current layout to the given shape.
Parameters
----------
to_shape : List[Expr]
The shape to tile to
current_shape : List[Expr]
The current shape of the layout
"""
tile_shape = [to_shape[i] // current_shape[i] for i in range(len(to_shape))]
return self.tile(TileLayout(S[tuple(tile_shape)]), tile_shape, current_shape)
@staticmethod
def _get_default_strides(data: list[int | Expr], stride: int = 1) -> tuple:
assert isinstance(data, list | tuple), "data must be a tuple"
# Promote ``stride`` to the dtype of the shape extents so the resulting
# strides match what te-create_prim_func / C++ ``GetDefaultStrides``
# produce for int64-shaped buffers (otherwise the last stride stays a
# Python ``int`` -> int32 IntImm and breaks structural-equal).
for t in data:
if tvm.ir.is_prim_expr(t) and t.ty.dtype != "int32":
from .expr import IntImm # pylint: disable=import-outside-toplevel
stride = IntImm(t.ty, stride)
break
res = list()
for t in reversed(data):
assert isinstance(t, int) or tvm.ir.is_prim_expr(t), (
f"data must be int or Expr, but got {t}"
)
res.append(stride)
stride *= t
return list(reversed(res))
def is_swizzle(self) -> bool:
"""Check if the layout is swizzle."""
return isinstance(self, SwizzleLayout)
def is_trivial(self) -> bool:
"""Check if the layout is trivial."""
return False
def is_trainium(self) -> bool:
"""Check if the layout is trainium layout."""
if not isinstance(self, TileLayout):
return False
return _ffi_api.TileLayoutIsTrainium(self) # pylint: disable=no-member
def storage(self) -> "Layout":
if isinstance(self, TileLayout):
# Filter out shard with thread axis
shard = [iter for iter in self.shard if not iter.axis.is_thread()]
replicate = [iter for iter in self.replica if not iter.axis.is_thread()]
exclude = {axis: offset for axis, offset in self.offset.items() if not axis.is_thread()}
return TileLayout.from_iters(shard, replicate, exclude) # pylint: disable=no-member
elif isinstance(self, SwizzleLayout):
return self
elif isinstance(self, ComposeLayout):
return ComposeLayout(self.swizzle.storage(), self.tile_layout.storage())
else:
raise ValueError(f"Unsupported layout type: {type(self)}")
def unpack(self, num: int) -> "Layout":
"""Unpack the layout, where a single element in the layout is unpacked into num contiguous elements.
Parameters
----------
num : int
The number of elements to unpack into
Returns
-------
Layout
The unpacked layout
""" # noqa: E501
if isinstance(self, TileLayout):
shard = [Iter(iter.extent, iter.stride * num, iter.axis) for iter in self.shard]
shard.append(Iter(num, 1, Axis.get("m")))
return TileLayout.from_iters(shard, self.replica, self.offset)
elif isinstance(self, SwizzleLayout):
assert num & (num - 1) == 0, "num must be a power of 2"
return SwizzleLayout(
self.per_element + (num.bit_length() - 1),
self.swizzle_len,
self.atom_len,
self.swizzle_inner,
)
elif isinstance(self, ComposeLayout):
return ComposeLayout(self.swizzle.unpack(num), self.tile_layout.unpack(num))
else:
raise ValueError(f"Unsupported layout type: {type(self)}")
def broadcast(self, num: int, position: int = -1, axis: '"Axis" | str' = "m") -> "Layout":
"""Insert a stride-0 broadcast dim of extent ``num`` at ``position``.
``position`` follows Python list-insert semantics (negative indices
count from the end; ``-1`` appends after the last shard dim). The
new dim has stride 0 — accessing along it doesn't move the byte
offset, so the same physical element is "seen" ``num`` times.
Useful for layouts where a consumer reads the same SMEM datum
multiple times (e.g. ``sf_reuse`` over MMA-K steps).
"""
if isinstance(self, TileLayout):
if isinstance(axis, str):
axis = Axis.get(axis)
new_iter = Iter(num, 0, axis)
shard = list(self.shard)
insert_at = position if position >= 0 else len(shard) + 1 + position
shard.insert(insert_at, new_iter)
return TileLayout.from_iters(shard, self.replica, self.offset)
elif isinstance(self, ComposeLayout):
return ComposeLayout(self.swizzle, self.tile_layout.broadcast(num, position, axis))
else:
raise ValueError(f"broadcast not supported for {type(self)}")
def pack(self, num: int) -> "Layout":
"""Pack the layout, where num contiguous elements in the layout are packed into a single element.
Parameters
----------
num : int
The number of elements to pack into
Returns
-------
Layout
The packed layout
""" # noqa: E501
if isinstance(self, TileLayout):
inner_iter = self.shard[-1]
assert (
inner_iter.stride == 1
and inner_iter.extent % num == 0
and inner_iter.axis.is_memory()
), f"Layout {self} can not be packed into {num} elements"
shard = [Iter(iter.extent, iter.stride // num, iter.axis) for iter in self.shard[:-1]]
shard.append(Iter(inner_iter.extent // num, 1, inner_iter.axis))
return TileLayout.from_iters(shard, self.replica, self.offset)
elif isinstance(self, SwizzleLayout):
assert num & (num - 1) == 0, "num must be a power of 2"
assert self.per_element >= num.bit_length() - 1, (
"per_element must be greater than or equal to num.bit_length() - 1"
)
return SwizzleLayout(
self.per_element - (num.bit_length() - 1),
self.swizzle_len,
self.atom_len,
self.swizzle_inner,
)
elif isinstance(self, ComposeLayout):
return ComposeLayout(self.swizzle.pack(num), self.tile_layout.pack(num))
else:
raise ValueError(f"Unsupported layout type: {type(self)}")
# Set of axis names registered on the C++ side. Used for lazy resolution of
# both module-level (`from tvm.tirx.layout import laneid`) and class-attribute
# (`Axis.laneid`) accesses. The actual FFI call to look up each axis is
# deferred until first access — keeps `import tvm.tirx.layout` runtime-safe
# (compiler-side FFI need not be present, matching apache's discipline).
_AXIS_NAMES = (
"bx",
"by",
"bz",
"cbx",
"cby",
"cbz",
"tx",
"warpid",
"laneid",
"wgid",
"tid_in_wg",
"wid_in_wg",
"m",
"P",
"F",
"Bank",
"TCol",
"TLane",
)
class _AxisMeta(type(Object)):
"""Metaclass: lazy resolve `Axis.<name>` for registered axes."""
def __getattr__(cls, name):
if name in _AXIS_NAMES:
return cls.get(name)
raise AttributeError(f"type object 'Axis' has no attribute {name!r}")
@tvm_ffi.register_object("tirx.Axis")
class Axis(Object, metaclass=_AxisMeta):
"""Layout axis wrapper."""
# ---- forbid direct construction ----
def __init__(self, *args, **kwargs):
raise RuntimeError("Cannot create Axis directly; use Axis.get()")
@staticmethod
def _register_axis(name: str) -> "Axis":
return _ffi_api.AxisGet(name) # pylint: disable=no-member
# Singleton cache, populated lazily as names are accessed.
reg_dict: ClassVar[dict[str, "Axis"]] = {}
@staticmethod
def get(name: str) -> "Axis":
"""Get or create an axis by name. Unknown names are auto-registered."""
if name not in Axis.reg_dict:
Axis.reg_dict[name] = Axis._register_axis(name)
return Axis.reg_dict[name]
def is_thread(self) -> bool:
"""Check if the axis is a thread axis."""
return _ffi_api.AxisIsThreadAxis(self) # pylint: disable=no-member
def is_memory(self) -> bool:
"""Check if the axis is a memory axis."""
return _ffi_api.AxisIsMemoryAxis(self) # pylint: disable=no-member
def get_scope(self) -> ExecScope | None:
"""Get the scope of the axis."""
return _ffi_api.AxisGetScope(self) # pylint: disable=no-member
def get_subscope(self) -> ExecScope | None:
"""Get the subscope of the axis."""
return _ffi_api.AxisGetSubscope(self) # pylint: disable=no-member
# Enable syntax like `4 @ Axis.laneid` to attach an axis to a stride/term.
# This mirrors libraries that overload the matrix multiply operator for DSLs.
def __rmatmul__(self, other: Expr): # type: ignore[override]
# Represent a single value bound to an axis.
return _OnAxis(other, self)
# ------------------------------------------------------------------
# 2) Lazy module-level axis lookup
# ------------------------------------------------------------------
# PEP 562 module-level __getattr__ for `from tvm.tirx.layout import laneid`.
# The FFI call to look up each axis is deferred until first access; bare
# `import tvm.tirx.layout` performs zero compiler-side FFI calls.
def __getattr__(name):
if name in _AXIS_NAMES:
return Axis.get(name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
try:
__all__ # type: ignore[name-defined]
except NameError: # pragma: no cover
__all__ = [] # type: ignore[var-annotated]
__all__ += list(_AXIS_NAMES)
__all__ += ["R", "S"]
__all__ += ["tcgen05_atom_layout", "tmem_datapath_layout", "wg_local_layout"]
# ============================================================================
# TMEM datapath layouts (PTX ISA §9.7.16.10.5)
# ============================================================================
#
# ``tcgen05.mma`` writes its output matrix C into TMEM using one of several
# **datapath layouts** depending on the MMA's M dimension and ``.ws`` mode.
# Each layout determines *which* physical TMEM lanes (rows) the matrix
# occupies; the leak in the original ``_default_tmem_layout`` was that it
# always used the identity ``(rows, cols) : (1@TLane, 1@TCol)`` mapping,
# which is correct only for Layout D (M=128 full datapath). For Layout F
# (M=64 non-``.ws``) the MMA writes scattered lanes
# ``{0..15, 32..47, 64..79, 96..111}`` — half of each warp's 32-lane
# partition — and the readback path (``.16x*b`` M=64 atom) has the matching
# scatter built into the PTX. To keep the buffer's logical row indexing in
# sync with the physical scatter, the buffer's TileLayout must encode the
# scatter directly.
#
# We surface this via the factory below. Callers pass the datapath letter
# (``"D"`` / ``"F"``) and the logical ``(rows, cols)``; the factory returns
# the appropriate TileLayout. ``tmem_pool.alloc(..., datapath="F")`` plumbs
# this into the buffer's layout so the dispatch can structurally verify
# atom ↔ datapath compatibility instead of silently accepting mismatches.
#
# Supported today:
# - ``"D"``: M=128, ``.cta_group::1``, full datapath. Identity row→lane.
# - ``"F"``: M=64, non-``.ws``, half datapath (4x1 lane utilization).
# Logical row r → physical lane
# (r // 16) * 32 + sub_slab * 16 + (r % 16).
#
# Layouts A / B / C / E / G are reserved for future expansion.
_TMEM_DATAPATH_ROWS = {"D": 128, "F": 64}
def tmem_datapath_layout(datapath: str, rows: int, cols: int, sub_slab: int = 0) -> "TileLayout":
"""Return the ``TileLayout`` for a tcgen05 MMA datapath.
See PTX ISA §9.7.16.10.5 for the datapath enumeration. The returned
layout is shape-compatible with a buffer of ``(rows, cols)`` and
encodes the logical-row → physical-TMEM-lane mapping that the
corresponding MMA writes to (and that the matching ``.16x*b`` /
``.32x32b`` atom expects to read).
Parameters
----------
datapath : str
One of ``"D"`` (M=128, ``.cta_group::1``, full datapath) or
``"F"`` (M=64, non-``.ws``, half datapath). Other layouts are not
yet supported by this factory.
rows : int
Logical row count of the TMEM buffer. Must match the datapath's M
dimension: 128 for D, 64 for F.
cols : int
Logical column count.
sub_slab : int
For Layout F, select the lower (``0``) or upper (``1``) 16-lane
half of each warp's 32-lane TMEM partition. The upper half is useful
as a 64-row read/write view of the high half-slab of a Layout D
accumulator. Layout D already spans both halves and therefore only
accepts ``0``.
Returns
-------
TileLayout
Buffer-shape-compatible layout for ``(rows, cols)``.
"""
if datapath not in _TMEM_DATAPATH_ROWS:
raise ValueError(
f"tmem_datapath_layout: unknown datapath {datapath!r}; "
f"supported: {sorted(_TMEM_DATAPATH_ROWS)}"
)
expected = _TMEM_DATAPATH_ROWS[datapath]
if rows != expected:
raise ValueError(
f"tmem_datapath_layout: datapath={datapath!r} expects rows={expected}, got {rows}"
)
if sub_slab not in (0, 1):
raise ValueError(f"tmem_datapath_layout: sub_slab must be 0 or 1, got {sub_slab}")
tlane = Axis.get("TLane")
tcol = Axis.get("TCol")
if datapath == "D":
# M=128, identity row→lane: row r ∈ [0, 128) → physical lane r. D
# already spans both 16-lane sub-slabs of every warp partition.
if sub_slab != 0:
raise ValueError(
"tmem_datapath_layout: datapath='D' (M=128) already spans both "
"sub-slabs; sub_slab must be 0"
)
return TileLayout(S[(rows, cols) : (1 @ tlane, 1 @ tcol)])
# Layout F: M=64 scattered. Logical row r = wid * 16 + intra (wid ∈ [0,4),
# intra ∈ [0,16)) → physical lane wid * 32 + sub_slab * 16 + intra.
# ``TileLayout`` decomposes a scalar row index via ``SplitCoord``
# (src/tirx/ir/layout/utils.cc), which uses row-major ordering: with
# shape ``(s0, s1)`` the FIRST iter receives ``coord // s1`` (the high
# bits) and the SECOND receives ``coord % s1`` (the low bits). So we
# pin the warp selector to iter 0 (extent 4, TLane stride 32) and the
# within-slab lane to iter 1 (extent 16, TLane stride 1), then shift the
# TMEM lane offset by 16 for the upper sub-slab.
spec = S[(4, 16, cols) : (32 @ tlane, 1 @ tlane, 1 @ tcol)]
if sub_slab:
spec = spec + (16 @ tlane)
return TileLayout(spec)
def wg_local_layout(cols, rows=128):
"""Return a warpgroup-local register layout.
The logical ``(rows, cols)`` tile is distributed on ``tid_in_wg`` along rows,
so each thread owns one row and contiguous ``cols`` local elements.
"""
return TileLayout(S[(rows, cols) : (1 @ Axis.tid_in_wg, 1)])
# Allowed (.shape, .num) combinations for tcgen05.ld/st atoms.
# Source: PTX ISA Table 49 (tcgen05-num-shapes-ld).
_TCGEN05_ATOM_REPS = {
"32x32b": (1, 2, 4, 8, 16, 32, 64, 128),
"16x64b": (1, 2, 4, 8, 16, 32, 64, 128),
"16x128b": (1, 2, 4, 8, 16, 32, 64),
"16x256b": (1, 2, 4, 8, 16, 32),
}
# Per-warp fp32-column factor for each instr_shape. For .16x*b atoms the
# warpgroup fragment is 64 rows x (factor * rep) fp32 cols; for .32x32b the
# fragment is 128 rows x (factor * rep) fp32 cols with factor=1.
_TCGEN05_COL_FACTOR_FP32 = {"32x32b": 1, "16x64b": 2, "16x128b": 4, "16x256b": 8}
# Allowed fragment row counts per warpgroup for each instr_shape. ``.32x32b``
# is fixed at M=128; ``.16x*b`` natively covers M=64 (one 16-row slab per
# warp, using lanes 0..15 of each warp's 32-lane TMEM partition) and can be
# extended to M=128 by issuing the atom twice with row offsets 0 and 16
# (covering lanes 0..15 + 16..31, i.e. the warp's full slab). The M=128
# variant doubles per-thread registers and treats the extra slab as the
# highest m-bit.
_TCGEN05_FRAG_ROWS = {
"32x32b": (128,),
"16x64b": (64, 128),
"16x128b": (64, 128),
"16x256b": (64, 128),
}
def tcgen05_atom_layout(instr_shape: str, tensor_shape: tuple[int, int], dtype) -> "TileLayout":
"""Register-side ``TileLayout`` for ``tcgen05.ld``/``tcgen05.st`` ``.16x*`` atoms.
Describes the per-warpgroup register tile that ``Tx.copy_async`` produces
when reading a TMEM fragment via ``tcgen05.{ld,st}.<instr_shape>.xN``.
``rep`` (the ``.xN`` qualifier) is inferred from ``tensor_shape``.
Fragment row count is determined by ``instr_shape``: ``.32x32b`` covers an
M=128 fragment (128 rows per warpgroup), and ``.16x{64,128,256}b`` covers
an M=64 fragment (64 rows per warpgroup).
TMEM is kept **dense** for 16-bit dtypes: two 16-bit elements per 32-bit
TMEM cell (matching the existing ``.32x32b`` convention). The PTX op is
issued with the plain ``.b32`` form (no ``.pack::16b`` qualifier), and
the returned layout describes the per-thread register file with two
packed 16-bit elements per 32-bit register.
Parameters
----------
instr_shape : str
The PTX atom's ``.shape`` qualifier. One of ``"32x32b"``, ``"16x64b"``,
``"16x128b"``, ``"16x256b"``.
tensor_shape : tuple[int, int]
The logical fragment shape in **element units**. Must be
``(frag_rows, K)`` where ``frag_rows`` is ``128`` for ``.32x32b`` and
``64`` for the other shapes, and ``K`` is divisible by the per-warp
column factor for the chosen instr_shape and dtype::
K must be a power-of-two multiple of (factor_fp32 * elem_per_32b)
where ``factor_fp32`` is ``1`` / ``2`` / ``4`` / ``8`` for ``.32x32b`` /
``.16x64b`` / ``.16x128b`` / ``.16x256b``, and ``elem_per_32b`` is
``1`` for fp32 and ``2`` for fp16/bf16. The inferred rep must be in PTX
Table 49's supported set for the chosen instr_shape.
dtype : str | tvm.DataType
Element dtype. ``"float32"``, ``"float16"``, or ``"bfloat16"``.
Returns
-------
TileLayout
A ``(64, K)``-shaped tile layout. The factory builds it as a sequence
of fine-grained iters describing the per-(lane, register) destination
position; ``.group([(64, K)])[0]`` flattens to two iters.
Examples
--------
``tcgen05_atom_layout("16x64b", (64, 64), "float32")`` → ``.16x64b.x32`` (rep=32, fp32).
``tcgen05_atom_layout("16x128b", (64, 256), "float16")`` → ``.16x128b.x32`` (rep=32,
fp16; two fp16 elements packed per 32-bit register and per 32-bit TMEM cell).
"""
if instr_shape not in _TCGEN05_ATOM_REPS:
raise ValueError(
f"tcgen05_atom_layout instr_shape must be one of "
f"{list(_TCGEN05_ATOM_REPS)}, got {instr_shape!r}"
)
bits = tvm.runtime.DataType(dtype).bits
if bits not in (16, 32):
raise ValueError(
f"tcgen05_atom_layout dtype must be a 32-bit or 16-bit type, got {dtype} ({bits} bits)"
)
if len(tensor_shape) != 2:
raise ValueError(
f"tcgen05_atom_layout tensor_shape must be 2-D (rows, cols), got {tensor_shape!r}"
)
rows, cols = tensor_shape
allowed_rows = _TCGEN05_FRAG_ROWS[instr_shape]
if rows not in allowed_rows:
raise ValueError(
f"tcgen05_atom_layout {instr_shape!r} expects rows ∈ {allowed_rows}, got {rows}"
)
elem_per_32b = 32 // bits
col_factor_elem = _TCGEN05_COL_FACTOR_FP32[instr_shape] * elem_per_32b
if cols % col_factor_elem != 0:
raise ValueError(
f"tcgen05_atom_layout cols={cols} not divisible by the per-rep column "
f"factor {col_factor_elem} for instr_shape={instr_shape!r} dtype={dtype}; "
f"valid cols are k * {col_factor_elem} for k in "
f"{_TCGEN05_ATOM_REPS[instr_shape]}"
)
rep = cols // col_factor_elem
if rep not in _TCGEN05_ATOM_REPS[instr_shape]:
raise ValueError(
f"tcgen05_atom_layout inferred rep={rep} (from cols={cols}) is not in "
f"the PTX Table 49 supported set for {instr_shape}: "
f"{_TCGEN05_ATOM_REPS[instr_shape]}"
)
laneid = Axis.laneid
wid = Axis.wid_in_wg
N = rep
shape = instr_shape
# All m-strides below are written in fp32-reg units; we multiply by
# elem_per_32b at the end and prepend a C_pack iter for the 16-bit case
# (each fp32 reg packs ``elem_per_32b`` elements at adjacent col positions).
if shape == "32x32b":
# M=128 fragment, simple thread-rows layout:
# (rows=128, cols=K) : (1@tid_in_wg, 1)
# Each of 128 warpgroup threads owns one row; cols are contiguous in
# the per-thread storage. For 16-bit dtypes the K cols are packed two
# per 32-bit register (handled by the per-thread storage element count
# naturally — m-stride 1 in element units).
iters = [
Iter(rows, 1, Axis.tid_in_wg),
Iter(cols, 1, "m"),
]
return TileLayout.from_iters(iters, [], {})
# Iter lists are written high-to-low: ``TileLayout`` decomposes a flat
# coordinate via ``SplitCoord`` (src/tirx/ir/layout/utils.cc) using
# row-major ordering, where the FIRST iter receives the *high* bits and
# the LAST iter receives the *low* bits. So R_w (highest-stride row
# contribution) comes first in row_iters_fp32 and R_t1/t2 (lowest)
# comes last; same for col.
if shape == "16x64b":
# Per-warp tile (fp32 view): (16 rows, 2N cols). Per-lane regs = N.
# Lane (t0, t1, t2): t0 = laneid & 1, t1 = (laneid >> 1) & 1, t2 = laneid >> 2.
# Row = t2 + 8*t0 + 16*wid_in_wg
# Col (fp32) = t1 + 2*r, r ∈ [0, N)
row_iters_fp32 = [
(4, 1, wid), # R_w: wid_in_wg → R bits 4..5
(2, 1, laneid), # R_t0: laneid bit 0 → R bit 3
(8, 4, laneid), # R_t2: laneid bits 2..4 → R bits 0..2
]
col_iters_fp32 = [
(N, 1, "m"), # C_r: register slot → C bits 1..
(2, 2, laneid), # C_t1: laneid bit 1 → C bit 0
]
m_used_M64 = N
elif shape == "16x128b":
# Per-warp tile (fp32 view): (16 rows, 4N cols). Per-lane regs = 2N.
# Lane (t0, t1): t0 = laneid & 3, t1 = laneid >> 2.
# Reg r = ra + 2*rb, ra ∈ {0,1}, rb ∈ [0, N).
# Row = t1 + 8*ra + 16*wid_in_wg
# Col (fp32) = t0 + 4*rb
row_iters_fp32 = [
(4, 1, wid), # R_w
(2, 1, "m"), # R_ra: reg bit 0 → R bit 3
(8, 4, laneid), # R_t1: laneid bits 2..4 → R bits 0..2
]
col_iters_fp32 = [
(N, 2, "m"), # C_rb: reg bits 1.. → C bits 2..
(4, 1, laneid), # C_t0: laneid bits 0..1 → C bits 0..1
]
m_used_M64 = 2 * N
else: # 16x256b
# Per-warp tile (fp32 view): (16 rows, 8N cols). Per-lane regs = 4N.
# Lane (t0, t1) as for 16x128b. Reg r = v0p + 2*va + 4*vb.
# Row = t1 + 8*va + 16*wid_in_wg
# Col (fp32) = v0p + 2*t0 + 8*vb
row_iters_fp32 = [
(4, 1, wid), # R_w
(2, 2, "m"), # R_va: reg bit 1 → R bit 3
(8, 4, laneid), # R_t1
]
col_iters_fp32 = [
(N, 4, "m"), # C_vb: reg bits 2.. → C bits 3..
(4, 1, laneid), # C_t0
(2, 1, "m"), # C_v0p: reg bit 0 → C bit 0
]
m_used_M64 = 4 * N
if rows == 128:
# M=128 covers both 16-row half-slabs of each warp's 32-lane TMEM
# partition (the M=64 atom covers only lanes 0..15; the high half
# 16..31 needs a second PTX issue with row offset 16). We surface
# the combined fragment as a single (128, K) tile by inserting a
# v_slab iter right *after* R_w (i.e. as the next-highest row bit).
# v_slab claims one m-bit at the next free offset
# (stride = m_used_M64) so reg indices [0, m_used_M64) hold the
# low slab and [m_used_M64, 2*m_used_M64) hold the high slab — the
# split the dispatch uses when emitting the two PTX calls. The
# inserted iter also doubles wid_in_wg's row stride from 16 to 32,
# so the four warps now tile rows 0..31 / 32..63 / 64..95 / 96..127.
new_row_iters = []
for ext, stride, axis in row_iters_fp32:
new_row_iters.append((ext, stride, axis))
if axis is wid:
new_row_iters.append((2, m_used_M64, "m"))
row_iters_fp32 = new_row_iters
def _scale(iters):
out = []
for ext, stride, axis in iters:
if axis == "m":
out.append((ext, stride * elem_per_32b, axis))
else:
out.append((ext, stride, axis))
return out
row_iters = _scale(row_iters_fp32)
col_iters = _scale(col_iters_fp32)
# For the 16-bit packed variant each fp32 register holds two adjacent
# column elements (low / high halves). Add a C_pack iter of extent
# ``elem_per_32b`` and m-stride 1 at the *low* end of the col axis —
# i.e. as the LAST col iter under SplitCoord's high-to-low ordering.
if elem_per_32b > 1:
col_iters.append((elem_per_32b, 1, "m"))
iters = [Iter(ext, stride, axis) for ext, stride, axis in row_iters + col_iters]
return TileLayout.from_iters(iters, [], {})
# ------------------------------------------------------------------
# Helper types to support `Expr @ Axis` and `sum` for offsets
# ------------------------------------------------------------------
class _OnAxis:
"""Represents a single value attached to an axis, created via `value @ Axis.X`.
Used in two places:
- As stride spec in `TileLayout(..., shard=(extents, [value @ Axis.X]))`
- As terms to build an offset expression like `1 @ Axis.laneid + 512`
"""
def __init__(self, value: Expr, axis: Axis):
self.value = value
self.axis = axis
# Arithmetic to build offset sums
def __add__(self, other: "_OffsetExprLike") -> "_OffsetExpr":
base = _OffsetExpr({self.axis: self.value})
return base + other
def __radd__(self, other: "_OffsetExprLike") -> "_OffsetExpr":
return self.__add__(other)
class _OffsetExpr:
"""Sum of axis-bound terms forming an offset specification.
Internally stored as a dict {Axis: Expr}. When a plain Expr is
provided (without axis), it is treated as `Axis.m` by convention.
"""
def __init__(self, terms: dict[Axis, Expr] | None = None):
self.terms: dict[Axis, Expr] = dict(terms or {})
def _add_term(self, axis: Axis, value: Expr):
if axis in self.terms:
# Merge if both exist; rely on tvm arith for symbolic add
self.terms[axis] = self.terms[axis] + value # type: ignore[operator]
else:
self.terms[axis] = value
def __add__(self, other: "_OffsetExprLike") -> "_OffsetExpr":
res = _OffsetExpr(dict(self.terms))
if isinstance(other, _OffsetExpr):
for ax, v in other.terms.items():
res._add_term(ax, v)
elif isinstance(other, _OnAxis):
res._add_term(other.axis, other.value)
else: # Expr-like -> default to Axis.m
res._add_term(Axis.get("m"), other) # type: ignore[arg-type]
return res
def __radd__(self, other: "_OffsetExprLike") -> "_OffsetExpr":
return self.__add__(other)
_OffsetExprLike = _OffsetExpr | _OnAxis | Expr | int
# ------------------------------------------------------------------
# Composable layout specs: S[shape:stride] + R[shape:stride] + offset
# ------------------------------------------------------------------
class _LayoutSpec:
"""Composable layout specification built via ``S[shape:stride] + R[shape:stride] + offset``.
Instances are created by the module-level ``S`` and ``R`` builders and
combined with ``+``. Pass the result directly to :class:`TileLayout`.
"""
__slots__ = ("offset", "replica", "shard")
def __init__(self, shard=None, replica=None, offset=None):
self.shard = shard # (shape_tuple, stride_tuple) or (shape_tuple, None)
self.replica = replica # (shape_tuple, stride_tuple) or None
self.offset = offset # _OffsetExprLike or None
def __add__(self, other):
if isinstance(other, _LayoutSpec):
return _LayoutSpec(
shard=self.shard or other.shard,
replica=other.replica if other.replica else self.replica,
offset=_merge_offset(self.offset, other.offset),
)
if isinstance(other, _OnAxis | _OffsetExpr | int):
return _LayoutSpec(
shard=self.shard, replica=self.replica, offset=_merge_offset(self.offset, other)
)
return NotImplemented
def __radd__(self, other):
if isinstance(other, _OnAxis | _OffsetExpr | int):
return _LayoutSpec(
shard=self.shard, replica=self.replica, offset=_merge_offset(other, self.offset)
)
return NotImplemented
def _merge_offset(a: "_OffsetExprLike | None", b: "_OffsetExprLike | None"):
"""Combine two offsets that arrive at a `_LayoutSpec` via successive `+`.
`_LayoutSpec.__add__` used to overwrite `self.offset` with the new term,
which made `S[..] + 1 @ laneid + 2 @ warpid` silently drop the first
axis. Always merge through `_OffsetExpr.__add__` so each axis term is
accumulated correctly.
"""
if a is None:
return b
if b is None:
return a
return _to_offset_expr(a) + _to_offset_expr(b)
class _SpecBuilder:
"""Builder for ``S[shape : stride]`` and ``R[shape : stride]`` syntax.
- 1-D: ``S[8 : 4@laneid]``
- N-D: ``S[(8, 4, 2) : (4@laneid, 1@laneid, 1)]``
- Extents only: ``S[8, 4, 2]``
"""
__slots__ = ("_kind",)
def __init__(self, kind: str):
self._kind = kind # "shard" or "replica"
@staticmethod
def _to_tuple(x):
if isinstance(x, tuple):
return x
if isinstance(x, list):
return tuple(x)
return (x,)
def __getitem__(self, key):
if isinstance(key, slice):
pair = (self._to_tuple(key.start), self._to_tuple(key.stop))
elif isinstance(key, tuple | list):
pair = (tuple(key), None) # extents only
else:
pair = ((key,), None) # single extent
if self._kind == "shard":
return _LayoutSpec(shard=pair)
return _LayoutSpec(replica=pair)
S = _SpecBuilder("shard")
R = _SpecBuilder("replica")
def _to_offset_expr(x: _OffsetExprLike) -> _OffsetExpr:
if isinstance(x, _OffsetExpr):
return x
if isinstance(x, _OnAxis):
return _OffsetExpr({x.axis: x.value})
# Fallback: treat plain Expr/int as Axis.m
return _OffsetExpr({Axis.get("m"): x}) # type: ignore[arg-type]
@tvm_ffi.register_object("tirx.Iter")
class Iter(Object):
"""A memory layout that tiles data across devices."""
extent: Expr
stride: Expr
axis: Axis
def __init__(self, extent: Expr, stride: Expr, axis: Axis | str):
if isinstance(axis, str):
axis = Axis.get(axis)
self.__init_handle_by_constructor__(
_ffi_api.Iter,
extent,
stride,
axis, # pylint: disable=no-member
)
def _spec_to_iters(pair) -> list:
"""Convert a ``(shape, stride)`` pair from :class:`_LayoutSpec` to ``List[Iter]``."""
if pair is None:
return []
shape, strides = pair
if strides is None:
strides = Layout._get_default_strides(shape, 1)
result = []
for e, s in zip(shape, strides):
if isinstance(s, _OnAxis):
result.append(Iter(e, s.value, s.axis))
elif isinstance(s, str):
result.append(Iter(e, 1, s))
elif isinstance(s, tuple):
result.append(Iter(e, s[0], s[1]))
else:
result.append(Iter(e, s, "m"))
return result
@tvm_ffi.register_object("tirx.TileLayout")
class TileLayout(Layout):
"""A memory layout that tiles data across devices."""
shard: list[Iter]
replicate: list[Iter]
exclude: list[tuple[Axis, Expr]]
def __init__(self, spec: "_LayoutSpec"):
shard_iters = _spec_to_iters(spec.shard)
replica_iters = _spec_to_iters(spec.replica)
offset_dict = {}
if spec.offset is not None:
off_expr = _to_offset_expr(spec.offset)
offset_dict = dict(off_expr.terms)
self.__init_handle_by_constructor__(
_ffi_api.TileLayout, # pylint: disable=no-member
shard_iters,
replica_iters,
offset_dict,
)
@staticmethod
def from_iters(
shard: "Sequence[Iter]" = (),
replica: "Sequence[Iter]" = (),
offset: dict[Axis | str, Expr] | None = None,
) -> "TileLayout":
"""Construct a TileLayout from pre-built Iter objects."""
if offset:
offset = {Axis.get(k) if isinstance(k, str) else k: v for k, v in offset.items()}
return _ffi_api.TileLayout(shard, replica, offset or {}) # pylint: disable=no-member
def is_trivial(self) -> bool:
"""Check if the layout is trivial."""
return _ffi_api.TileLayoutIsTrivial(self) # pylint: disable=no-member
def group(self, shape: list[Expr]) -> tuple["Layout", list[int]]:
"""Group the current layout by the given shape.
Parameters
----------
shape : List[Expr]
The shape to group by
Returns
-------
Tuple[Layout, List[int]]
The grouped layout and the separators
"""
return _ffi_api.TileLayoutGroup(self, shape) # pylint: disable=no-member
def get_scope(self) -> tuple[ExecScope, ExecScope] | None:
"""Get the scope pair of the layout."""
return _ffi_api.TileLayoutGetScope(self) # pylint: disable=no-member
@classmethod
def trainium(cls, annotation: str, shape: tuple[Expr], is_psum: bool = False) -> "TileLayout":
"""Create a TileLayout from an annotation string and a shape."""
analyzer = tvm.arith.Analyzer()
assert re.fullmatch(r"[PF]*", annotation), (
f"annotation {annotation} must be a string of 'P' and 'F'"
)
assert len(annotation) == len(shape), (
f"annotation {annotation} and shape {shape} must have the same length"
)
num_p_dim = annotation.count("P")
if num_p_dim == 1:
p_idx = annotation.index("P")
p_dim = shape[p_idx]
assert analyzer.can_prove(p_dim <= 128 or p_dim % 128 == 0), (
f"There is only 1 P in the annotation. Partition size {p_dim} must be less than or equal to 128 or a multiple of 128" # noqa: E501
)
if analyzer.can_prove(p_dim > 128):
# split out the P dimension and put the higher part on the free dimension with largest stride # noqa: E501
annotation = "F" + annotation
shape = (p_dim // 128, *shape[:p_idx], 128, *shape[p_idx + 1 :])
elif num_p_dim > 1:
p_dim_prod = functools.reduce(
operator.mul, [s for s, c in zip(shape, annotation) if c == "P"]
)
assert analyzer.can_prove(p_dim_prod <= 128), (
f"There are {num_p_dim} Ps in the annotation. Partition size {p_dim_prod} must be less than or equal to 128" # noqa: E501
)
f_shape = [s for i, (s, c) in enumerate(zip(shape, annotation)) if c == "F"]
p_shape = [s for i, (s, c) in enumerate(zip(shape, annotation)) if c == "P"]
f_strides = Layout._get_default_strides(f_shape, 1)
p_strides = Layout._get_default_strides(p_shape, 1)
f_tile_layout = TileLayout(S[tuple(f_shape) : tuple(s @ Axis.F for s in f_strides)])
p_tile_layout = TileLayout(S[tuple(p_shape) : tuple(s @ Axis.P for s in p_strides)])
result = []
f_index = p_index = 0
for char in annotation:
if char == "F":
result.append(f_tile_layout.shard[f_index])
f_index += 1
else: # char == 'P'
result.append(p_tile_layout.shard[p_index])
p_index += 1
if num_p_dim == 1 and analyzer.can_prove(p_dim > 128):
# put higher part of P to where it belongs
higher_P = result[0]
result = result[1:]
result = [*result[:p_idx], higher_P, *result[p_idx:]]
res = TileLayout.from_iters(result, [], dict()) # pylint: disable=no-member
if is_psum:
res = res.to_psum()
return res
kPSUMMaxElemPerBank = 512
kPSUMBankNum = 8
def to_psum(self) -> "TileLayout":
"""Convert the layout to a psum layout."""
analyzer = tvm.arith.Analyzer()
shard = []
for i in self.shard:
if i.axis.name == "F":
if analyzer.can_prove(i.stride % self.kPSUMMaxElemPerBank == 0):
stride = analyzer.simplify(i.stride // self.kPSUMMaxElemPerBank)
shard.append(Iter(i.extent, stride, Axis.get("Bank")))
elif analyzer.can_prove(self.kPSUMMaxElemPerBank % i.stride == 0):
c = analyzer.simplify(self.kPSUMMaxElemPerBank // i.stride)
if analyzer.can_prove(i.extent < c):
shard.append(i)
elif analyzer.can_prove(i.extent % c == 0):
shard.append(Iter(analyzer.simplify(i.extent // c), 1, Axis.get("Bank")))
shard.append(Iter(c, i.stride, Axis.get("F")))
else:
assert False, f"layout {self} can not be converted to psum layout"
else:
assert False, f"layout {self} can not be converted to psum layout"
else:
shard.append(i)
return TileLayout.from_iters(shard, [], dict()) # pylint: disable=no-member
def permute_dims(self, perm: list[int]) -> "TileLayout":
"""Permute the dimensions of the layout."""
assert len(perm) == len(self.shard), (
"perm must have the same length as the number of dimensions in the layout"
)
new_shard = []
for i in perm:
new_shard.append(self.shard[i])
return TileLayout.from_iters(new_shard, self.replica, self.offset)
def permute_by_groups(self, seps: list[int], perm: list[int]) -> "TileLayout":
"""Permute groups of shard iters defined by ``seps``.
``seps`` follows the convention of :meth:`group`'s second return value:
``seps[0] == 0`` and group ``i`` covers shard indices
``[seps[i], seps[i + 1])``. The number of groups is ``len(seps) - 1``.
Parameters
----------
seps : list[int]
Group boundary positions in the shard list.
perm : list[int]
Permutation of ``range(len(seps) - 1)`` selecting the new group order.
"""
n_groups = len(seps) - 1
assert sorted(perm) == list(range(n_groups)), f"invalid perm {perm}"
flat = [k for g in perm for k in range(seps[g], seps[g + 1])]
return self.permute_dims(flat)
@tvm_ffi.register_object("tirx.SwizzleLayout")
class SwizzleLayout(Layout):
"""A memory layout that swizzles elements to improve memory access patterns."""
per_element: int
swizzle_len: int
atom_len: int
swizzle_inner: bool
def __init__(
self, per_element: int, swizzle_len: int, atom_len: int, swizzle_inner: bool = True
):
self.__init_handle_by_constructor__(
_ffi_api.SwizzleLayout, # pylint: disable=no-member
per_element,
swizzle_len,
atom_len,
swizzle_inner,
)
@tvm_ffi.register_object("tirx.ComposeLayout")
class ComposeLayout(Layout):
"""A memory layout that composes 2 layouts."""
def __init__(self, layout_A: "SwizzleLayout", layout_B: "TileLayout"):
self.__init_handle_by_constructor__(
_ffi_api.ComposeLayout, # pylint: disable=no-member
layout_A,
layout_B,
)