Shapes & strides¶
Each dimension's size and stride may be static (a compile-time constant) or dynamic (a run-time value) — independently. Static values fold into machine code; dynamic ones are carried at run time. One kernel source covers every combination.
shape<...> — the sizes¶
shape<...> lists the per-dimension sizes of a tensor. Its int64_t index type
matches DLPack's, so it drops onto ndarray bindings directly. (Under the hood it is
teeny's spelling of cuda::std::extents<int64_t, ...> — see
mdspan vs teeny.)
shape<2,3,4> // fully static 2×3×4
shape<-1,3> // dynamic rows, static 3 columns (-1 == dynamic)
shape<dynamic_extent,3> // the same type, spelled out
A dynamic dimension is -1 (numpy-style) or dynamic_extent. Construct a tensor
by supplying only the dynamic sizes:
auto a = wrap(ptr, shape<2,3,4>{}); // all static — nothing to supply
auto b = wrap(ptr, shape<-1,3>{n}); // supply the one dynamic size, n
auto c = wrap(ptr, shape<-1,-1>{r, k}); // supply both
auto d = wrap(ptr, rank<2>{r, k}); // rank<N> is the fully-dynamic rank-N shape
rank<N> is the fully-dynamic shape of rank N, so rank<2> is exactly
shape<-1,-1>. (There is no rank{r, k} / shape{r, k} shortcut: class-template
argument deduction does not apply to alias templates in C++17, so you always give
the rank — rank<2>{r, k} or shape<-1,-1>{r, k}.)
Query sizes with shape:
t.rank(); // number of dimensions (static)
t.numel(); // total element count
t.shape(); // the whole shape as an array-like accessor
t.shape(1); // RUNTIME lookup -> the size of axis 1 (an index_type value)
t.shape(Int<0>()); // STATIC lookup -> an integral_constant when that axis is static
// (so it folds into later arithmetic)
t.shape(Int<-1>()); // negative axis: the last dimension
(These are teeny's numpy/pytorch-flavoured spellings. The underlying mdspan
spellings — t.extent(d), t.extents() — still exist as an interop escape hatch;
see mdspan vs teeny.)
strides<...> — the strides¶
The default layout is ccontiguous (C-order). In this case, strides are computed
automatically from the shape and do not need to be provided. For specific strides —
a padded row, a channel-last view, a reversed axis — use strides<...>, the stride
analogue of shape<...>:
auto a = wrap(ptr, shape<3,4>{}, strides<4,1>{}); // row stride 4 (padding), col 1
auto b = wrap<dynamic_stride,1>(ptr, shape<-1,4>{n}, {4}); // outer runtime, inner 1 (folds)
- Known strides become compile-time constants baked into the code, and only the
dynamic ones are stored. A fully-static
strides<...>mapping is empty, so astrides<>view is exactly a pointer. - Strides are signed:
strides<-4,1>is a real stride of −4 (a reversed view), not dynamic. A runtime stride is the sentineldynamic_stride.
Query a stride, static when derivable:
t.stride(0); // runtime
t.stride(Int<1>()); // a compile-time constant for a strides<> layout, a
// contiguous static shape, or a contiguous layout's
// always-unit stride (even under a dynamic shape)
A strides<...> tensor is fully sliceable. Every view op — operator()
slicing, take_along, permute, flip, squeeze/unsqueeze, peel — works on
any source layout including strides<...>, and folds the output strides the same
way. Slicing a contiguous static tensor keeps folded compile-time strides. (If you
know mdspan, the mdspan vs teeny page explains how strides<>
relates to layout_static_stride and why teeny builds these views by hand.)
The static/runtime idiom¶
The API accepts runtime integers, static integers (integral_constant), and
slices of either, and returns the matching output type. alias.h provides short
names for these static integers: Int<V>, Long<V>, Size<V>, UInt<V>,
Int32<V>, Int64<V>, Diff<V>, Bool<V>. Each static type converts implicitly
to a runtime value and carries a ::value.
Rule of thumb: pass Int<k>() to make the compiler fold; pass a plain
int/long when the value is only known at run time.
t.extent(Int<0>()); // -> integral_constant<…,2> (a constant, folds)
t.extent(runtime_d); // -> int64_t (a value)
The index type — shape32 / reindex¶
Every offset computation runs in the shape's index_type (int64_t for
shape<...>, matching DLPack). At a kernel boundary you can narrow it to 32-bit
without a copy:
reindex preserves the layout — same pointer and layout kind, with the shape and
any dynamic strides narrowed to the new width (a strides<...> literal pack is
unchanged). It's the index-width twin of recast (which recovers static shape
values): orthogonal, and they compose. shape32<...> == shape_as<int32_t, ...>
is the int32-indexed shape; shape_as<Idx, ...> picks any index type.
The payoff is on the device: a dynamic-stride view carries its strides by value, so
halving their width cuts the register footprint (rank-2: 40 → 24 bytes) and runs the
address math in 32-bit. Guard it with t.index_fits<int32_t>() — a signed-reach
check that handles negative-stride (flipped) and broadcast views — and only narrow
when the element span provably fits. See Performance.
Mixing widths in a broadcast¶
When two operands of different index widths meet in a broadcast (a + b,
a < b, …), the result takes the wider of the two — an int32-indexed view
plus an int64-indexed one yields an int64-indexed result, in either order. This
is lossless (the broadcast engine already runs its offset math in the result's index
type) and avoids silently truncating the wide operand's strides to the narrow width.
Two equal-width operands are unchanged; note that broadening cannot rescue two int32
operands whose broadcast span overflows int32 (an outer-product stretch) — that
stays the caller's concern, guarded by index_fits/dispatch_index at the boundary.