A Layout is built from a Shape and a Stride of the same hierarchical
profile. This chapter walks through both halves carefully and introduces the
integer-modules that PyCuTe's strides are allowed to live in.
Naming note. The Whitepaper (§2.3.1) introduces the more general integer-semimodule abstraction, which doesn't require an additive identity. PyCuTe always has a unique additive identity
int 0, so each stride leaf type is the stricter integer-module (an abelian group with aZscalar action). We use "module" below.ArithTuplestores its children verbatim, so rank-bearing zeros likeArithTuple(0, 0)(a rank-2 element, distinct from rank-0int 0) participate in congruence checks; equality applies implicit zero-extension along trailing positions. Seeatuple.py.
- The Whitepaper's source-of-truth is (Whitepaper, §2.2 Shape, §2.3 Stride).
- PyCuTe source:
shape.py,stride.py,atuple.py,typedefs.py.
Shape. A
Shapeis anHTupleof positive integers.(Whitepaper, §2.2.)
In PyCuTe, a shape is just a Python integer or a Python tuple/list of
shapes. There is no Shape class — the carrier is still int and tuple —
but PyCuTe exports a documentation-grade Shape type alias (an
HTuple(Integer), defined in typedefs.py) for use in
annotations such as Layout(shape: Shape, ...) and crd2idx(crd, shape: Shape).
It is a hint, not a runtime check: positivity of the leaves (Z⁺) and
congruence with a stride remain runtime invariants verified by
congruent / weakly_congruent and
compatible.
>>> from pycute import *
>>> shape(7)
7
>>> shape((3, 4))
(3, 4)
>>> shape(((2, 3), 4))
((2, 3), 4)shape, size, rank, and depth work on shapes as HTuples and on
layouts/tensors that store a .shape attribute:
>>> S = ((2, 3), 4)
>>> size(S) # product of leaves
24
>>> rank(S) # number of top-level entries
2
>>> depth(S) # depth of the deepest tuple
2(See shape.py and stride.py
for the implementations; test_htuple.py::TestHTuple
covers product / prefix_product / inner_product, and
test_htuple.py::TestHTuple::test_idx2crd covers
idx2crd / crd2idx.)
Every shape S defines a finite set Z(S) of valid coordinates, but those
coordinates can be written in three equivalent forms:
- The integral coordinate
i ∈ {0, 1, ..., size(S)-1}— a single integer. - The flat R-D coordinate
(c_0, ..., c_{R-1})— one integer per top-level mode. - The natural (or h-D) coordinate, which has the same hierarchical
profile as
S.
All three forms are captured by the Coord type alias (defined in
typedefs.py). Coord also admits None at any
position as a slice-marker — the form produced by Tensor.__getitem__ and
consumed by Layout._offset_and_slice (and idx2crd(None, S) maps a None
to zeros).
For shape (3, (2, 3)) the three sets enumerate as:
| Integral | Flat | Natural |
|---|---|---|
| 0 | (0, 0) | (0, (0, 0)) |
| 1 | (1, 0) | (1, (0, 0)) |
| 2 | (2, 0) | (2, (0, 0)) |
| 3 | (0, 1) | (0, (1, 0)) |
| ... | ... | ... |
| 17 | (2, 5) | (2, (1, 2)) |
PyCuTe's idx2crd(i, S) and crd2idx(c, S) (defined in
shape.py) implement the bijection between any
two coordinate forms via the colexicographical ordering — reading from
right to left, the leftmost mode varies fastest.
>>> idx2crd(7, (3, (2, 3)))
(1, (0, 1))
>>> crd2idx((1, (0, 1)), (3, (2, 3)))
7This is the exact same convention as in C++ CuTe and the CuTe DSL.
Three relations between shapes appear constantly in PyCuTe's post-conditions
and error messages. congruent and weakly_congruent are implemented
in htuple.py (they apply to any HTuple profile).
compatible is implemented in shape.py and applies
to shapes (and objects with a .shape):
congruent(a, b)—aandbhave the same profile.weakly_congruent(a, b)—acoarsensb; every leaf ofamay fold a sub-tree ofb.compatible(a, b)— every coordinate ofais a valid coordinate ofb. Equivalently,ais a coarsened shape ofb. This is a partial order on shapes.
>>> congruent((4, 8), (5, 7))
True
>>> congruent((4, 8), (4, (2, 4)))
False
>>> weakly_congruent(30, (1, 1))
True # int coarsens any shape
>>> compatible(24, (4, 6))
True # | (4, 6) | == 24
>>> compatible((4, 6), ((2, 2), 6))
True
>>> compatible((4, 6), ((2, 3), 8))
False(See (Whitepaper, §2.2.1) for the full definitions,
test_compatibility.py for the
checked PyCuTe semantics, and the C++ documentation's
01_layout.md § Layout compatibility
for an extended list of examples.)
When two shapes share a common refinement or coarsening in the compatibility
partial order, PyCuTe exposes join/meet helpers in shape.py:
common_refinement(a, b)— minimal shape refining bothaandb(least upper bound under≼). RaisesValueErrorwhen no such shape exists.common_coarsening(a, b)— maximal shape coarsening bothaandb(greatest lower bound under≼). RaisesValueErrorwhen sizes disagree.
These are used internally by advanced layout analysis (for example,
layout_add and greatest_common_domain in algebra.py).
See the worked examples under Coordinate sets above.
idx2crd accepts out-of-bounds integral coordinates (the final mode absorbs
overflow); crd2idx inverts it on in-bounds inputs.
(See test_htuple.py::TestHTuple::test_idx2crd.)
Generator over all natural coordinates of shape in colexicographical order:
>>> list(coordinates((3, 2)))
[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]Stride. A
Stride Dfor aShape Sis anHTuplecongruent withSwhose leaves are elements of an integer-semimodule. The stride defines a map from natural coordinates to a codomainDviainner_product.(Whitepaper, §2.3.)
In annotations, a stride is spelled with the Stride type alias — an
HTuple(StrideScalar), defined in typedefs.py.
In most code, strides are integers and the codomain is just Z. PyCuTe
also supports more exotic strides, all of which are integer-modules:
| Stride leaf | Codomain | Used for |
|---|---|---|
Python int |
Z |
Common data layouts |
sympy.Symbol, etc. |
symbolic Z |
Symbolic shape/stride analysis |
ArithTuple (incl. basis elements E(...)) |
Z^S |
Coordinate layouts (TMA, predication) and arithmetic on hierarchical coordinates |
F2 |
F_2 = (Z, XOR, ·) |
Bank-conflict-avoiding swizzles |
This generalization is one of the key contributions of the (Whitepaper, §2.3.1) and is the most significant difference between PyCuTe and the older C++ CuTe documentation.
PyCuTe treats a value as an "integer" iff its type is registered with the
abstract base class Integer — int (and
subclasses) automatically, numpy.integer / sympy.Expr when importable, and
anything else via register_integer_type(...). Membership is a deliberate
registration, not duck-typing. bool and float are explicitly excluded, but
sympy symbols are integers:
>>> from pycute import *
>>> is_int(7)
True
>>> is_int(True)
False
>>> import sympy
>>> N = sympy.symbols('N')
>>> is_int(N)
True(See test_typing.py.)
A stride scalar is anything that supports
elementwise addition (+) and scalar multiplication by an integer (*).
That is the operations needed by inner_product. The abstract base class
StrideScalar registers int and Integer by
default; ArithTuple and F2 are subclasses (basis elements are just
canonical ArithTuples).
>>> is_stride_scalar(3)
True
>>> is_stride_scalar(ArithTuple(1, 2, 3))
True
>>> is_stride_scalar(E(0)) # also an ArithTuple
TrueThe default constructor Layout(shape) calls
prefix_product(shape) to generate strides, producing a "generalized
column-major" layout — that is, the leftmost mode varies fastest in memory:
>>> Layout((4, 8))
Layout((4, 8), (1, 4))
>>> Layout((3, (2, 4)))
Layout((3, (2, 4)), (1, (3, 6)))You can override the stride by passing it explicitly:
>>> Layout((4, 8), (8, 1)) # row-major
Layout((4, 8), (8, 1))
>>> Layout((3, (2, 4)), (24, (1, 6)))
Layout((3, (2, 4)), (24, (1, 6)))If the constructor is given a single integer in place of a stride, that is
the base stride and the default prefix_product(shape, base) is
applied:
>>> Layout((4, 8), 2)
Layout((4, 8), (2, 8)) # column-major with base 2If you want to skip the prefix-product expansion entirely (e.g. when you
have already-computed strides), use the private constructor
Layout._set(shape, stride). This is what the algebra uses internally to
avoid recomputing strides.
The (Whitepaper, §2.3.1 Integer-Semimodules) generalizes "stride" beyond integers, and the most useful generalization in CuTe is the coordinate stride: a stride leaf that produces a coordinate (not an offset) when an integer scales it.
PyCuTe encodes coordinate strides as canonical
ArithTuple values. A basis element is an
ArithTuple with a single nonzero leaf; the shorthand E(*seq) creates
a unit basis element:
>>> from pycute import *
>>> E(0) # the basis vector e_0
1@0
>>> E(1)
1@1
>>> E(0, 0) # nested basis: e_0 of the e_0 sub-mode
1@0@0
>>> 3 * E(0) # scaled basis 3 e_0
3@0The string form value@m_n@...@m_0 reads "scale by value in mode-path
(m_0, ..., m_n)". Equivalently, the factory ScaledBasis(value, seq)
represents value placed at the path seq of an otherwise-zero
arithmetic tuple:
>>> ScaledBasis(42, []) # bare scalar — collapses to int 42
42
>>> ScaledBasis(42, [0]) # (42, 0, 0, 0, 0)
42@0
>>> ScaledBasis(42, [1]) # (0, 42, 0, 0, 0)
42@1
>>> ScaledBasis(42, [1, 0]) # (0, (42, 0, ...), 0, ...)
42@0@1ScaledBasis is a factory function, not a class — it returns the
canonical int or ArithTuple representation. To detect a basis
element at a use site, call is_basis(x); to read the underlying
(coefficient, path) decomposition of any ArithTuple (single basis
element or multi-term sum), call basis_repr(x).
(See test_atuple.py::TestArithTuple::test_sbasis for the
full table of equivalences and
test_atuple.py::TestBasisRepr /
test_atuple.py::TestIsBasis for the
decomposition and predicate.)
When you scale E(0) by 3 and E(1) by 5 and add them, you get a rank-2
arithmetic tuple (3, 5):
>>> 3 * E(0) + 5 * E(1)
(3, 5)
>>> isinstance(3 * E(0) + 5 * E(1), ArithTuple)
TrueArithTuple is closed under elementwise addition (and subtraction, since
Z^S is a group) and scalar multiplication
(but not element-by-element multiplication, because that is not how
strides compose):
>>> ArithTuple(1, 2, 3) + ArithTuple(7, 8, 9)
(8, 10, 12)
>>> ArithTuple(1, 2, 3) * 4
(4, 8, 12)
>>> 4 * ArithTuple(1, 2, 3)
(4, 8, 12)
>>> ArithTuple(1, 2, (3, 4)) + (7, 8, (9, 10))
(8, 10, (12, 14))Adding a non-ArithTuple scalar to an ArithTuple is an incompatibility
error and raises:
>>> ArithTuple(1, 2, 3) + 7 # 7 has no profile
Traceback (most recent call last):
...
TypeError: ArithTuple Incompatibility: ...(See test_atuple.py::TestArithTuple::test_atuple.)
A layout Layout((4, 8), (E(0), E(1))) is the identity on its
2-D coordinate space — that is, it maps (i, j) to itself rather than to
some integer offset. This is exactly what you want when you need the
coordinate of a position in a tile, not the offset.
>>> A = Layout((4, 8), (E(0), E(1)))
>>> A(2, 3)
(2, 3)
>>> A = Layout((4, 8), (E(1), E(0)))
>>> A(2, 3)
(3, 2) # row and col swapped, like a transpose(See test_atuple.py::TestArithTuple::test_coord_layout
for several variations.)
Coordinate strides are also the way you describe predication coordinates,
TMA tensor descriptors, and the layout of "logical coordinates within a
tile". They appear pervasively in
test_complement.py,
test_composition.py,
test_inverse_left.py, and
test_inverse_right.py.
Note on conformance. Coordinate strides are first-class in PyCuTe and in the Whitepaper. They are not a built-in feature of the C++ CuTe headers. The C++ documentation's claim that "strides are integers" is a simplification that this documentation deliberately corrects.
ArithTuple defines __eq__, __lt__, and __gt__ along the
colexicographical ordering. This is what lets complement sort its
strides without knowing whether they are integers or coordinates:
>>> 0 < E(0)
True
>>> ArithTuple(1, 0, 0) < ArithTuple(0, 1, 0)
True
>>> ArithTuple(idx2crd(3, (3, 4))) < ArithTuple(idx2crd(7, (3, 4)))
True(See test_atuple.py::TestArithTuple::test_atuple_lt.)
Convenience functions defined in atuple.py:
make_basis_like(profile)— build the matching basis tuple for a profile.make_basis_like((a, (b, c)))returns(E(0), (E(1, 0), E(1, 1))), which prints as(1@0, (1@0@1, 1@1@1)). Used bycomplementto construct a weakly-congruent extension.proj(x, profile)— extract fromxthe part that lives at the leaf position ofprofile.profilemust be a single scaled basis vector.unit(profile)— the unit basis element atprofile's path (E()for an integerprofile,E(*seq)otherwise).profilemust be a single scaled basis vector. Used byrecastto produce a stride of the same type as the input.as_tuple(obj)— convert anArithTupleinto a plain Python tuple of integers.basis_repr(x)— algebraic decomposition ofxinto(value, path)pairs;is_basis(x)is true iff that decomposition has length 1.
Field
$F_2 = ({0, 1}, \mathrm{XOR}, \mathrm{AND})$ is also an integer-module (XOR is an abelian group onZwith every element self-inverse). Strides drawn fromF_2produce swizzled layouts where what would normally be addition becomes XOR.(Whitepaper, §2.3.1.)
(See test_swizzle.py::TestF2 and
test_swizzle.py::TestF2Layout for
checked F2 arithmetic and worked F2-layout examples.)
PyCuTe's F2 class wraps an integer, so a value
is an element of + is ^
(bitwise XOR) and * is a carry-less product: the bits are polynomial
coefficients, so an integer operand acts through its bits, not its value.
>>> from pycute import *
>>> F2(0b1010) + F2(0b1100)
F6 # 0b0110
>>> F2(0b1010) * 2
F20 # a power of two is a plain shift
>>> F2(0b11) * 0b11
F5 # 0b101 carry-lessly, where 3 * 3 == 9 in ZThe two products coincide whenever the schoolbook multiplication carries nowhere, which is why the difference only shows up for strides and extents that are not powers of two.
F2 strides describe XOR-based swizzles directly inside a regular
Layout — no special wrapper is needed. See Swizzling
for examples and the broader theory.
Many algorithms need to know the codomain of a layout (the smallest bounding box the layout's image fits inside) or the profile of that codomain.
coshape(L)— forLayoutobjects, returnsas_tuple(L(size(L) - 1) + 1)treating each codomain leaf as a0..max+1range.coprofile(L)— the codomain's structure only, congruent tocoshape(L)but with meaningless leaf values. Used incomposition,complement,right_inverse, andleft_inverseto know whether they need to produce an integer-codomain or a coordinate-codomain layout. A tiler is profiled astiler_to_layoutpromotes it:coprofile(24)is a leaf, andcoprofile((None, 4))is(0, 0), one axis per mode.
>>> coshape(Layout((4, 8), (1, 4)))
32
>>> coshape(Layout((4, 8), (E(0), E(1))))
(4, 8)
>>> coshape(Layout((4, 8), (8, 1)))
32- Source:
shape.py,stride.py,atuple.py,typedefs.py,swizzle.py. - Tests:
test_htuple.py,test_atuple.py,test_typing.py,test_compatibility.py,test_swizzle.py::TestF2.
Copyright (c) 2023 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0