Skip to content

API cheat sheet

The whole surface on one screen. For type-annotated tables (inputs/outputs) see the Reference; for every symbol's extracted signature see Autodoc.

Everything is in namespace tny. namespace cs = cuda::std. Include <teeny/teeny.h> for all of it; <teeny/cuda.h> adds the CUDA memory spaces.


The tensor type

template <class T, class Shape, class Layout = ccontiguous, storage O = storage::view>
struct tensor;  // Shape = any cuda::std::extents (spell it shape<...>); Layout: ccontiguous/fcontiguous/...

One tensor type parameterised by element type, cuda::std::extents, an mdspan layout, and ownership. Rarely named directly — use the aliases and factories below. See Tensors & ownership.

Ownership aliases

view<T, E, L = ccontiguous>       // non-owning view (default; the bare `tensor` is this)
local<T, E, L = ccontiguous>        // stack-owned (requires a fully static shape)
owned<T, E, L = ccontiguous>        // heap-owned, host, move-only
gpu<T, E, L>  pinned<T, E, L>  mapped<T, E, L>  // CUDA memory (from <teeny/cuda.h>)

Factories

wrap(ptr, shape);  wrap<Layout>(ptr, shape);  // a view (C-order / chosen layout)
wrap(ptr, shape, fcontiguous{});               // value-tag layout (deduced, no .template)
wrap(ptr, shape, {s0, s1, ...});              // view with RUNTIME strides (dynamic_strides)
wrap(ptr, shape, strides<S...>{});           // view with COMPILE-TIME strides (fold into type)
as_tensor(any_mdspan);  wrap(any_mdspan);      // wrap an mdspan/submdspan result — same view, both
                                               //   public on purpose: wrap = the one factory name,
                                               //   as_tensor = the mdspan-adapting primitive (interop)
wrap(ptr, shape, storage_v<storage::gpu>);   // memory-space tag — on EVERY wrap form, mdspan included
wrap(any_mdspan, storage_v<storage::gpu>);   //   pass the plain backend; it folds to the view kind
                                             //   (gpu -> gpu_view) — you never spell the _view kinds
wrap(ptr, shape, storage_c<storage::heap>{});   // heap/stack name no memory SPACE, so this still folds
                                                //   to storage::view (NOT an owning tensor) — use
                                                //   empty<T,storage::heap>(e)/make_heap<T>(e) for that

make_view(ptr, shape);           // alias of wrap that deduces the extents type
make_view(ptr, shape, fcontiguous{});   // ...same layout spellings as wrap (or make_view<fcontiguous>)
empty<T>(shape);                 // UNINITIALISED (np.empty); deduces stack (static) / heap (dynamic)
                                 //   (a value-initialised local<...>{} / owned(e) stays zeroed; empty opts out)
empty<T, storage::gpu>(shape);       // ...or name a backend: stack/heap/gpu/pinned/mapped
empty<T>(shape, storage_c<storage::gpu>{});   // value-tag backend form (same result)
empty(shape, dtype<T>{});            // value-tag ELEMENT-TYPE form: deduces T, no .template needed.
                                     //   O stays a leading explicit template arg: empty<storage::gpu>(shape, dtype<T>{})
empty(shape, fcontiguous{}, dtype<double>{});   // a layout tag composes with dtype/storage_c, any
                                                 //   order/subset (also zeros/ones/full; arange has no
                                                 //   layout keyword)
empty<storage::gpu>(shape, fcontiguous{}, dtype<double>{});   // ...and the leading backend arg takes
                                                 //   that SAME bag, any subset/order — dtype
                                                 //   and/or a layout tag, or nothing at all
make_local<T>(shape);  make_heap<T>(shape);            // thin spellings of empty<T,storage::stack/heap>
make_gpu<T>(shape); make_pinned<T>(shape); make_mapped<T>(shape);   // empty<T,storage::gpu/...>; T defaults to float

zeros<T>(shape);  ones<T>(shape);   // T defaults to float; static->stack, dyn->heap
full(shape, v);                     // element type = the VALUE's type (full<T>(...) to force)
arange<T>(n);                       // 1-D [0..n-1] (heap); T defaults to int64
arange<T, N>();  arange<T>(Int<N>());  // static 1-D [0..N-1] (stack, folds)
zeros<T, storage::pinned>(shape);  arange<T>(n, storage_c<storage::pinned>{});   // ...or name a
                    // host-accessible backend (stack/heap/pinned/mapped); a gpu fill
                    // static_asserts -> use to<storage::gpu>(zeros<T>(shape))
zeros(shape, dtype<T>{});  full(shape, v, dtype<T>{});  arange(n, dtype<T>{});   // value-tag T (also ones)
zeros(shape, dtype<T>{}, storage_c<S>{});  // ...or compose BOTH tags, either order (also
                    //   storage_c<S>{}, dtype<T>{}) — no explicit template arg at all (also empty/ones/full)

Shapes & strides (layout.h, alias.h)

template <auto... E>       using shape   = cs::extents<int64_t, E...>;  // -1 == dynamic
template <class Idx, auto... E> using shape_as = cs::extents<Idx, E...>; // shape<> with a chosen index type
template <auto... E>       using shape32 = shape_as<int32_t, E...>;     // int32-indexed boundary view
template <size_t N>        using rank    = shape<-1 ...N times>;        // fully-dynamic rank-N shape
template <int64_t... S>    struct strides;                // signed; dynamic_stride sentinel
template <int64_t... S>    using layout_static_stride = strides<S...>;  // back-compat alias
constexpr int64_t dynamic_stride;                         // a runtime stride

t.reindex<int32_t>() (free: reindex<int32_t>(t)) retypes the offset index width without a copy (layout preserved; extents/dynamic strides narrowed) — halves a dynamic view's footprint and runs offset math in 32-bit at the kernel boundary. t.index_fits<int32_t>() is the guard: every element offset and every axis size must be representable in the target width (the shape narrows too). See Performance.

Static-integer aliases (each converts to a runtime integer and carries ::value); pass them where a static index/size is wanted, e.g. x.extent(Int<0>()):

Int<0>{};  Long<3>{};  UInt<2>{};  Bool<true>{};  // classic:     Int Long Size UInt Diff Bool
Int32<4>{};  UInt64<8>{};                         // fixed-width: Int8/16/32/64  UInt8/16/32/64
I4<3>{};  U8<9>{};                                // numpy short (BYTES): I4==Int32, U8==UInt64

Element dtype aliases — numpy short codes, width in bytes:

auto z = zeros<i4>(sh);        // i1 i2 i4 i8   u1 u2 u4 u8   (i4 == int32_t, u8 == uint64_t)
auto m = local<f4, shape<3>>{};// f4 == float, f8 == double, f2 == half, bf16 == bfloat16

See Shapes & strides.


Geometry

x.rank();  x.numel();  x.is_contiguous();  x.is_dense();  // is_contiguous = C-order; is_dense = any order
x.shape();            x.shape(d);          // the teeny SHAPE: array-like accessor / size of axis d
x.strides();          x.stride(d);         // the teeny STRIDES: array-like accessor / stride of axis d
x.shape()[Int<k>()];  x.strides()[Int<k>()];  // static index folds to integral_constant; runtime [i] is a value
x.data();  x.view();                       // base ptr / non-owning teeny view (gpu_view if device)
// raw-mdspan escape hatch (interop only):
x.mdspan();  x.extents();  x.mapping();     // raw cs::mdspan / raw cs::extents / layout mapping
x.extent(d);  x.extent(Int<d>());          // mdspan-side per-axis size (== x.shape(d)); static Int<d> folds

mdspan equivalent

x.shape()/x.strides() are teeny's array-like accessors and the primary spelling; x.extents()/x.mapping() return the raw cuda::std mdspan objects. Per-axis, x.extent(d) == x.shape(d). See mdspan vs teeny for the full vocabulary map.


Indexing & slicing (indexing.h)

x(i, j, k);                     // element access -> T& (negatives wrap)
x.at(i, j, k);                  // one element as a rank-0 VIEW (rank-0 <-> scalar, .item())
x(0, all, slice(1, 4));         // any slice arg -> a VIEW
x(1, ellipsis, 2);              // ellipsis = (rank - #other args) copies of `all`
x(1, etc, 2);                   // `etc` == `ellipsis` (one marker, two names)
x(ellipsis) = b;  x(0, all) = v;  // assign INTO a slice copies/fills (a = b rebinds)
slice(start, stop);  slice(start, stop, step);  // half-open range, optional (neg) step
none;  all;                     // open slice end (== python None); keep-axis marker
x(none, all, all);  x(0, newaxis, all);   // BARE none/newaxis arg -> insert a size-1
                    //   axis (== unsqueeze at that position); newaxis is an alias of none
x(m);  x.at(m);  x.uget(m);  x.uat(m);    // TUPLE-UNPACK: ONE tuple-like arg (cs::array /
                    //   cs::tuple) carrying the WHOLE index list -- numpy's x[(a,b,c)] == x[a,b,c].
                    //   Elements may be anything the variadic call takes (ints/Int<>/all/slice/
                    //   none/one ellipsis), and the result type is identical. Single-arg only
                    //   (never mixed with other positional args); C++23 x[m] forwards too.
                    //   It must be cuda::std's array/tuple: a std::array/std::tuple, or a pack
                    //   mixed with other positional args, is a COMPILE ERROR, not a silent `all`.
                    //   Closes the loop with peel(...).enumerate(), whose m IS a cs::array:
                    //   for (auto [m, cell] : peel(a, axis<0,1,2>{}).enumerate()) b(m) = f(cell);
x.slice_along<Axes...>(args...);  // bind named axes (negatives wrap), keep the rest -> a VIEW
                    //   (pytorch select/narrow over several axes). NOT numpy take_along_axis /
                    //   pytorch take_along_dim -- that index-TENSOR gather is index_select below
x.subsample<Axes...>(k, starts...);  // coloured/strided sub-lattice: slice_along + slice(start,
                    //   none,k) per named axis, one shared step k, per-axis start; k/starts
                    //   accept runtime or Int<> (folds static). Value form: x.subsample(
                    //   axis<Axes...>{}, k, starts...) -- same LEADING placement as slice_along's
x.unfold<Axis>(size, step);  x.unfold<Axis>(size);  // pytorch Tensor.unfold: appends a NEW
                    //   trailing axis of width `size` stepped by `step` along Axis (step
                    //   defaults to 1); Axis's own extent shrinks to the window COUNT
                    //   (shape(Axis)-size)/step+1. size/step accept runtime or Int<> (folds
                    //   static); size in [1,shape(Axis)] and step>=1, checked (static_assert
                    //   or debug-time). ND windows compose by chaining: x.unfold<0>(k0,s0).unfold<1>(k1,s1).
                    //   Value form: x.unfold(Int<Axis>(), size, step) -- single-axis Int<k>()
                    //   selector (like flip/squeeze), not an axis<...> list (only ONE axis binds).
x.index_select<Axis>(idx);       // gather along Axis by a rank-1 integer index TENSOR
                    //   (runtime data, unlike slice_along); idx values wrap negative;
                    //   static idx shape -> stack result (host+device, like clone()),
                    //   else heap (host only, source must be host-accessible).
                    //   into(dest) form too: x.index_select<Axis>(idx,
                    //   into(dest)) (no alloc, device-safe; dest's axis-Axis extent must
                    //   match idx's, checked; dest must not alias x). Value form takes a
                    //   TRAILING axis<...>{} (no .template on a dependent receiver -- unlike
                    //   slice_along/subsample's LEADING tag): x.index_select(idx, axis<Axis>{}).
                    //   axis<...>{} and into(dest) are trailing KEYWORDS (the generic bag, like
                    //   sum's/scan's), so they compose in any subset/order:
                    //   x.index_select(idx, into(dest), axis<Axis>{}) is the same call.
x.uget(i, j, k);  x.uget(0, slice(1,4));  x.uget(1, ellipsis);  x.uat(i...);
                    // uget = unchecked twin of operator() (element/slice/ellipsis,
                    // one entry point); uat = unchecked at. Skip the negative-index
                    // wrap for known-non-negative RUNTIME indices (per-call
                    // -DTNY_NO_NEGATIVE_INDEX). Same result type; static bounds
                    // still fold. A negative runtime index is then UB.

See Indexing & slicing.


Structure (views) (axis.h, tensor.h)

x.permute<Perm...>();                 // reorder axes
x.flip<Ax>();                         // reverse an axis (negative-stride view)
x.flip<Ax0,Ax1,...>();  x.flip(axis<0,2>{});  // reverse SEVERAL axes at once (numpy flip(a,axis=(0,2))):
                                      //   distinct axes, ANY order — flips commute, so flip<0,2> ==
                                      //   flip<2,0> == flip<0>().flip<2>(), built in ONE pass
x.unsqueeze<Ax>();  x.squeeze<Ax>();  // insert / drop a size-1 axis
x.unsqueeze<Ax0,Ax1,...>();  x.squeeze<Ax0,Ax1,...>();  // insert/drop SEVERAL at once (arity picks this
                                      //   overload); axes must be DISTINCT but may be listed in ANY order —
                                      //   unsqueeze positions are relative to the FINAL rank, squeeze to the
                                      //   SOURCE rank (the fold direction is an implementation detail)
x.unsqueeze(axis<>{});  x.squeeze(axis<>{});  x.flip(axis<>{});  // an EMPTY axis LIST names no axis -> a
                                      //   NO-OP (numpy's axis=() rule): same shape/strides back. NOT the
                                      //   same as the no-argument x.squeeze() (drop EVERY static singleton)
                                      //   / x.unsqueeze() (insert at axis 0) / x.flip() (reverse axis 0),
                                      //   which keep their meanings
x.reshape<NewExt...>();               // contiguous-view reshape (one -1 inferred)
x.flatten();                          // 1-D contiguous view
x.clone();                            // dense row-major OWNING copy (copies on the HOST; a gpu/gpu_view
                                      //   tensor must use the free to<Space>(x) below — dynamic clone static_asserts it)
x.recast<NewExtents>();               // reinterpret w/ a more-static same-rank extents (keeps source strides)
x.recast<NewExtents, ccontiguous>();  // ...AS contiguous (fold the strides; "I promise it's contiguous")
x.recast(shape<...>{}, ccontiguous{}); // functional form (shape + layout, both may mix static/dynamic)
x.to<T2>();                           // dtype convert (matching dtype -> no-copy borrow; else owning copy).
                                      //   The copy runs on the HOST; convert a gpu/gpu_view tensor via to<Space>(x)
x.to(dtype<T2>{});                    // value-tag twin (deduces T2, no .template on a dependent receiver)
to<storage::gpu>(x);                      // memory-space move: to<Space,ET,Force>(x) (from <teeny/cuda.h>);
                                      //   device-aware copy — use this (not clone()/member .to<>()) for a gpu source

Axis template arguments are signed (negatives count from the back); each <Ax> op also has a value formt.permute(Int<2>(),Int<0>(),Int<1>()) == t.permute<2,0,1>(), t.recast(shape<-1,3,3>{}) == t.recast<shape<-1,3,3>>(). The axis-list ops — permute/squeeze/unsqueeze and peel/peel_at/slice_along/the reductions — take an axis<...>{} selector (a compile-time axis list, sibling of shape<...>, like numpy's axis: int | list[int]) — reach for this spelling first: t.squeeze(axis<0,2>{}) == t.squeeze<0,2>(), t.permute(axis<2,0,1>{}) == t.permute<2,0,1>(), peel(t, axis<0,1>{}) == peel<0,1>(t), t.slice_along(axis<0,2>{}, i, slice(1,4)). Value forms are deduced, so a type-dependent receiver needs no .template. Every view op — operator()/slice_along/peel and permute/flip/unsqueeze/squeeze — folds its output strides into a static strides<...> (compile-time where the source strides are static), on any source layout. See Views & structure.


nd-peel (iteration) (iterate.h)

peel<Axes...>(x);       peel_at<Axes...>(x, i);  // peel named axes. range-for is INCREMENTAL (O(1)/cell,
                                                 //   no per-cell decode); peel_at = random access (grid-stride)
peel(x, axis<Axes...>{}); peel_at(x, i, axis<Axes...>{});  // value form (numpy-like axis selector)
peel_front<N>(x);       peel_front_at<N>(x, i);  // peel the first N axes (a COUNT, not an axis list, so no
                                                 //   axis<> tag; free fns -> no `.template` either way. The
                                                 //   anyrank MEMBERS take the count as Int<-Sr>(), see below)
peel<Axes...>(x).subrange(lo, hi);               // a [lo,hi) chunk for a CPU thread / device block
                                                 //   (seed once, then O(1)/step); peel_front<N>(x) too
for (auto [m, cell] : peel<Axes...>(x).enumerate()) ...;  // ALSO yield the peeled multi-index m
                                                 //   (m[d] = coord of peeled axis d) for a per-axis table
                                                 //   axtab[d][m[d]]. OPT-IN (bare cell stays lean); composes
                                                 //   with .subrange(lo,hi). Or it.index(d) on the raw iterator.
size_front<N>(x);                                // # cells peel_front<N> yields (no range built)

for (auto [a,b,c] : peel_zip<Axes...>(x,y,z)) ...;  // zip-peel 2 or 3 tensors in LOCK-STEP: one
                                                 //   cs::tuple<ViewX,ViewY,ViewZ> per step (a
                                                 //   DISTINCT name from peel, not an overload).
                                                 //   Operands may differ in shape if BROADCAST-
                                                 //   compatible (numpy right-align); Axes... are
                                                 //   in the BROADCAST rank's numbering. They may also
                                                 //   differ in INDEX TYPE -- the cells carry one wide
                                                 //   (and, on mixed signedness, signed) enough for
                                                 //   every operand, so a flipped operand zipped with
                                                 //   an unsigned-indexed one still steps backwards.
peel_zip(x, y, axis<Axes...>{});                 // value form: axis<...> TRAILING (after every
                                                 //   positional tensor -- unlike slice_along/peel_at's
                                                 //   leading tag)
peel_zip<Axes...>(x,y).enumerate();  peel_zip<Axes...>(x,y).subrange(lo,hi);  // same shape as peel's

scan_<Axis>(x, init, f);            // sequential fold along Axis, batched (peeled) over the
                                    //   rest: carry=init, then carry=f(carry,elem); elem=carry
                                    //   for each element (increasing order). f is a device-safe
                                    //   functor (like map_'s convention). Value form: scan_(x,
                                    //   init, f, axis<Axis>{}) -- the axis tag is TRAILING, like
                                    //   index_select's and the reductions'.
                                    //   Reverse sweep: scan_<Axis>(x.flip<Axis>(), init, f) -- a
                                    //   temporary view binds fine (lvalue + rvalue overloads).
scan_(x, init, f, ax);              // THE FORWARD + BACKWARD SWEEP: the idiom for a two-pass line
scan_(x.flip(ax), init, f, ax);     //   recurrence -- an L1 (min-plus) distance transform, or the
                                    //   causal + anticausal passes of an IIR spline prefilter.
                                    //   Two calls, no direction flag, no hand-written loop; `ax`
                                    //   is the axis<...> tag, named once. Worked example: see
                                    //   "The forward + backward sweep" in structure.md.
auto y = scan<Axis>(x, init, f);    // out-of-place: fresh dense copy, scanned (static->stack,
                                    //   dynamic->heap host-only, built on clone()); x untouched.
scan<Axis>(x, init, f, into(dest)); // no fresh allocation beyond the copy into dest; returns dest&.
                                    //   dest must match x's shape EXACTLY (checked -- unlike
                                    //   copy_'s own broadcast, since scan_ walks dest's own axes)
scan(x, init, f, axis<Axis>{}, into(dest));  // scan's two trailing keywords ride the generic
                                    //   keyword bag: any subset, any order -- scan(x, init, f,
                                    //   into(dest), axis<Axis>{}) is the same call.

See Views & structure.


Math (math.h)

// in-place (broadcasts tensor rhs; also scalar rhs). atomic_add_/atomic_sub_
// accumulate ATOMICALLY, host and device (underlying form: add_<Atomic>/sub_<Atomic>).
a.add_(x); a.sub_(x); a.mul_(x); a.div_(x);   a.atomic_add_(x); a.atomic_sub_(s);
a.minimum_(x); a.maximum_(x);   // running min/max update: *this = min/max(*this, x)
y.add_(x, alpha); y.sub_(x, alpha);  // fused axpy: y += alpha*x / y -= alpha*x (x broadcasts)
a += x; a -= s; a *= x; a /= s;  // compound-assign
++a; --a; auto old = a++;        // prefix in place; postfix (static) -> stack copy
a.neg_(); a.abs_(); a.exp_(); a.log_(); a.sin_(); a.cos_(); a.sqrt_(); a.tanh_();
a.floor_(); a.ceil_(); a.round_(); a.trunc_(); a.sign_(); a.pow_(e); a.clamp_(lo, hi);
a & b; a | b; a ^ b; ~a; a &= b; a |= s;  // bitwise (INTEGER element types only)
a.fill_(v); a.zero_(); a.copy_(b); a.iota_(start, step);
a.map_(f); a.zip_with_(g, b);  auto c = a.map(f);  a.map(f, into(y));  // user functor (device-safe)
a.at(i...).atomic_add_(v);                         // scatter-accumulate (atomic, host and device)

// out-of-place -> new tensor (promotes types; static->stack, dyn->heap)
auto c = a + b;  a.add(b);  a * 2.0;  2.0 - a;  1.0 / a;  -a;  a.pow(b);
auto c = neg(a); abs(a); exp(a); log(a); sin(a); cos(a); sqrt(a); tanh(a);
auto c = floor(a); ceil(a); round(a); trunc(a); sign(a);
auto c = minimum(a, b); maximum(a, s); clamp(a, lo, hi);
auto c = a.add(b, alpha);  a.sub(b, alpha);  // fused out-of-place axpy: a +/- alpha*b (b broadcasts)

// ...or write into a preallocated dest (one fused pass, no alloc) -> dest&: `into(y)` last.
//   y's SHAPE is checked -- a scalar-rhs or unary op wants the source's shape EXACTLY; a
//   tensor rhs checks each OPERAND against the y you pass (== y's extent or 1), so y may
//   deliberately be LARGER/higher-rank than the natural result and the operands stretch to
//   fill it: a.add(b, into(y)) is "y = a + b" minus the alloc+copy. Only operands broadcast,
//   never the dest -- a smaller y is an error. Every producer alike: a compile error when
//   the extents in play are static, a debug-time check otherwise. y's dtype may differ: the math runs
//   in the OPERANDS' precision (scalar rhs / axpy alpha too) and only the RESULT is cast to
//   y, so a.op(b, into(y)) == y.copy_(a.op(b)) numerically, minus the temporary (holds for
//   half/bfloat16 operands too: into(y) rounds through the twin's own promote_t first).
a.add(b, into(y));  a.mul(b, into(y));  a.add(2.0, into(y));  a.add(b, alpha, into(y));
exp(a, into(y)); sqrt(a, into(y)); minimum(a, b, into(y)); clamp(a, lo, hi, into(y));
normalize(a, into(y));  normalize(a, axis<1>{}, into(y));  a.map(f, into(y));
                      //   normalize's y matches a's shape EXACTLY, axis form included (the
                      //   result is a element-for-element; the reduced divisor is not an
                      //   operand you pick, so no broadcast leeway) — compile error when static.
cross(a, b, into(N(i, all)));                          // cross into row i of a matrix ("crossto")
                      //   The dest may be a TEMPORARY VIEW: every view-producing op (slicing,
                      //   at, permute, slice_along, ...) returns by value, and into() takes one
                      //   directly -- no named intermediate for "a slot of a bigger output".
                      //   sum(a, into(cells.at(i,j))); sum(m, axis<0>{}, into(rows(j, all)));
                      //   Use the call for its EFFECT; the returned dest& dangles past the
                      //   statement. A temporary OWNING tensor (into(zeros<T>(sh))) is a
                      //   compile error -- its storage would die with the statement.

// reductions -> scalar (all axes). ACCUMULATE in the "reduce type" (double for
//   small floats float/double/half, item type for ints), then CAST the result to
//   the tensor's element type: sum(float)->float. A leading TYPE arg makes that
//   type BOTH accumulator and result: sum<double>(a), dot<double>(a,b).
sum(a); prod(a); max(a); min(a); mean(a); dot(a, b);   // sum<Acc>(a), mean<Acc>(a), ...
a.sum(); a.mean(); a.dot(b); a.sum<0>(); a.mean(axis<1>{});  // ALSO methods (parity; same overloads)
sum(a, dtype<double>{});  a.sum(dtype<double>{});  // value-tag Acc == sum<double>(a) — a GENERIC
                      //   trailing keyword bag (tny::_kw, kwargs.h): dtype/axis/keepdims/into
                      //   compose in ANY subset, ANY order (see below), not just this bare form.
sum(a, into(cell)); dot(a, b, into(cell)); dot(a, b, dtype<float>{}, into(cell));  // into(dest):
                      //   FULL reduction -> a RANK-0 dest (local<T,shape<>>{} or wrap(&x,shape<>{}));
                      //   dtype casts, returns dest&. dot composes dtype+into too (no axis concept).
allclose(a, b, rtol=1e-5, atol=1e-8);  // |a-b| <= atol+rtol*|b| everywhere (broadcasts) -> bool
a.allclose(b);  a.allclose(b, rtol, atol);   // ALSO a method (parity with a.dot(b))
allclose(a, b, dtype<float>{});  allclose(a, b, rtol, atol, into(cell));  // same trailing bag as
                      //   dot/sqdist/dist: dtype<Acc>{} picks the COMPARISON's compute type
                      //   (== allclose<Acc>(a,b)); into(dest) writes the answer into a RANK-0 cell
                      //   (a bool cell keeps it, another dtype takes the 0/1 cast), returns dest&.
                      //   The tolerances stay POSITIONAL, ahead of the bag — pass any prefix of
                      //   (rtol, atol) and omit the rest: allclose(a, b, 0.1, into(cell)).
// axis reductions -> lower-rank tensor (named axes removed; negatives wrap). Same
//   rule: accumulate in reduce_type, result element type = the tensor's type;
//   sum<Acc, Axes...>(a) makes Acc accumulator AND result (leading TYPE = acc, int = axis).
//   That <Acc,Axes...> vs <Axes...> EXPLICIT TEMPLATE split stays (no universal template
//   param in C++17) — everything past it (axis<...>/dtype/keepdims/into) is a generic bag.
sum<Axes...>(a); prod<...>(a); max<...>(a); min<...>(a); mean<...>(a);  // sum<Acc,Axes...>(a)
sum(a, axis<0,2>{}); mean(a, axis<-1>{}); sum<double>(a, axis<0>{});    // numpy `axis=` value form
sum<2,0>(a);          // axes may be listed in ANY order, but must be DISTINCT: sum<0,0>(a) /
                      //   sum(a, axis<0,0>{}) is a COMPILE ERROR, not a dropped duplicate.
                      //   Negatives normalise first, so mean<1,-2>(a) at rank 3 is caught too.
sum<0>(a, into(buf)); mean(a, axis<1>{}, into(buf));  // into(dest) -> copies lower-rank result
sum<0>(a, keepdims);  sum(a, axis<0>{}, keepdims);    // keepdims: reduced axis stays size-1 (numpy
                      //   keepdims=True) -> broadcasts back over a. Every axis reduction.
sum(a, dtype<double>{}, axis<0>{}, keepdims, into(buf));  // ...and it ALL composes, any subset/order:
                      //   dtype x axis x keepdims x into == sum<double,0>(a, keepdims, into(buf))
sum(a, axis<>{});     // an EMPTY axis list reduces over NO axis (numpy's axis=()): each cell
                      //   aggregates its OWN element alone -> a's shape back, as an owned copy.
                      //   NOT sum(a) (no axis argument at all = EVERY axis -> a scalar). sqnorm/
                      //   norm follow the same rule, so they are the elementwise a² / |a| there.

// vector algebra & geometry (contained exact math; on views, host+device)
sqnorm(a);            // Σaᵢ² over all axes (== dot(a,a)); sqnorm<Acc> forces acc+result
norm(a);              // √Σaᵢ² (L2/Frobenius); floating result (int -> double); norm<Acc> too
sqdist(a,b); dist(a,b);// Σ(aᵢ-bᵢ)² / √Σ(aᵢ-bᵢ)² (one fused pass, no a-b intermediate); binary
                      //   only (no axis form, like dot); sqdist<Acc>/dist<Acc>, dtype<Acc>{}/into
a.normalize_();       // in place a /= norm(a) (floating types); zero vector -> NaN
auto u = normalize(a);// out-of-place unit vector -> new tensor (static->stack, dyn->heap)
a.normalize_<1>();  normalize<-1>(a);  a.normalize(axis<1>{});   // OVER NAMED AXES (keepdim
                      //   broadcast) — free or method, either spelling, each taking into(y):
                      //   normalize(a, axis<1>{}, into(y));  a.normalize<1>(into(y));
                      //   y matches a's shape EXACTLY (not broadcast-compatible), same as the
                      //   whole-tensor form — a static mismatch is a compile error.
                      //   Device-callable when the ALLOCATING forms' source is fully static; the
                      //   into(y)/in-place ones whenever the REDUCED extents are (they allocate
                      //   only the norm) — normalize_<0>() on shape<-1,3> works in a kernel
auto c = cross(a, b);  a.cross_(b);          // 3D cross (rank-1 length-3): new / in place (a = a×b)
                                             //   into a slot: cross(a, b, into(N(i, all)))

Promotion: C++ rules but lower-width float wins (-DTNY_STD_PROMOTION opts out). See Math & broadcasting.


Half precision (half.h)

half;  bfloat16;                      // native __half/__nv_bfloat16 under nvcc
compute_type<T>;  compute_type_t<T>;  // half -> float; else T

See Half precision.


Dispatch (the anyrank boundary) (dynamic.h)

dispatch_value<Vs...>(v, f);            // runtime value -> integral_constant
dispatch_values(f, candidates<1,2,3>(d), candidates<0,1,2,3>(o), candidates<0,,7>(b));
                                        // SEVERAL values at once: one candidate list per parameter,
                                        //   f(D,O,B) gets one integral_constant each. == the nested
                                        //   dispatch_value pyramid (same per-parameter match test; f
                                        //   instantiated once per COMBINATION). A value outside its
                                        //   list doesn't fire -> f not called, returns false. `v` may
                                        //   be an ENUM (no hand static_cast). NB f comes FIRST (the
                                        //   candidate lists are variadic).
as_anyrank(data, shape, stride, ndim);        // -> anyrank WRAPPING the arrays, NO copy
                                              //   (default; host only, arrays must outlive it)
as_anyrank(data, shape, stride, ndim, copy_meta);  // -> anyrank COPYING into an inline
                                              //   TNY_MAX_RANK store (device-passable)
as_anyrank(data, shape, stride, ndim, anyshape<etc,-1,-1,3>{});  // STATIC TRAILING shape in the type
                                              //   (anyshape<etc,...>: etc = erased batch, the rest = static tail):
                                              //   peeled cells fold the inner extents (no per-call recast). Checked
                                              //   vs the runtime shape once here, then trusted. `anyshape<etc>` (bare)
                                              //   == today's carrier. Also from_dlpack<T, anyshape<etc,-1,-1,3>>(m).
as_anyrank(data, shape, stride, ndim, anyshape<etc,-1,-1,3>{}, ccontiguous{});  // + LAYOUT tag folds the inner
                                              //   STRIDES too (checked vs runtime strides here). Fully-static tail ->
                                              //   EBO cell. Subsumes dispatch_layout for the "input contiguous"
                                              //   precondition. from_dlpack passes it by value: (m, ccontiguous{}).
at.peel_front(Int<-Sr>());  at.peel_front_at(i, Int<-Sr>());  // batch idiom (keep-count NEGATIVE: keep the last
at.size_front(Int<-Sr>());                    //   Sr dims); 1 kernel per Sr. size_front = flattened batch count
                                              //   (no range built). VALUE form -> no `.template` on a dependent
                                              //   receiver; == at.peel_front<-Sr>() / at.peel_front_at<-Sr>(i) /
                                              //   at.size_front<-Sr>() (the explicit-template spelling).
                                              //   A shape tag in that 2nd position is the fused recast instead:
                                              //   at.peel_front_at(i, shape<-1,c,c>{}) — never confusable.
dispatch_rank(at, f);                    // runtime rank -> fixed-rank view (per total rank)
dispatch_rank<narrow_index>(at, f);      // ...+ int32 offsets when the span fits (rank outer, width inner)
dispatch_index(v, f);                    // narrow one fixed view's offset width to int32 (else keep it).
                                         //   dispatch_index<Idx2>(v, f) for another width — Idx2 need not fit
                                         //   v's STATIC sizes: one too large drops the narrow arm at compile
                                         //   time (wide arm only), so generic code can pass any shape.
at.index_fits<int32_t>();                // ...every offset AND every axis size fits int32? (the shape narrows
                                         //   too, so both halves are checked). Narrow the WHOLE CARRIER, once,
                                         //   host-side before a launch:
at.reindex<int32_t>();                   //   same pointer/space/anyshape geometry, meta store copied into an
                                         //   int32 one -> every cell it hands out is int32-indexed, and the
                                         //   by-value store halves. Debug-checked by index_fits; UB if you lie.
                                         //   dispatch_index(at, f) picks the arm for you. from_dlpack never
                                         //   narrows on its own (it keeps DLPack's int64). Free forms too (no
                                         //   `.template` on a dependent receiver): reindex<int32_t>(at),
                                         //   index_fits<int32_t>(at) — same as the view's free forms above.
at.fixed<R>();                           // force a known rank

See Dispatch & the anyrank boundary.


Compile flags

flag effect
-DTNY_STD_PROMOTION standard C++ float promotion (wider wins) instead of lower-wins
-DTNY_NO_NEGATIVE_INDEX drop python-style negative-index wrap from operator() (tightest codegen)
-DTNY_PORTABLE_HALF force the portable software half/bfloat16 even under nvcc
-DTNY_HARDENED turn ON element-access bounds checks (OFF by default, like mdspan; the u* accessors always skip them; always off on device)
-DNDEBUG strip the debug shape/precondition checks (_TNY_CHECK; on by default host-side, already off on device)

This page is the quick scan. For type-annotated tables see the Reference; for the Doxygen-extracted signatures of every symbol see Autodoc.