MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
The btensor Plugin

See also
mim::plug::btensor

Bufferized tensor operations: a generic map_reduce loop-nest abstraction plus derived operations (matrix product, transpose, pad, concat, ...), lowered to affine.For loop nests over buffer element operations.

This is the bufferized mirror of the tensor plugin: every op here is the buffer.Buf counterpart of a tensor.* op, takes the same groups in the same order, and threads mem.M on top. tensor.lower_to_mem bufferizes the SSA-world tensor.* ops onto these. Only the differences from that counterpart are documented below, and shapes are stated as the logical ones — buffer.Buf normalizes literal size-1 axes away and the loop generation folds its read/write indices to match.

Dependencies

plugin core;
plugin cps; // for cps.cps2ds
plugin affine;
plugin buffer;
plugin vec; // for vec.fold
plugin mem;
import ll; // for the ll.vec loop annotation (declarations only — no module load, no emit phase)
import compile;
use core.ops.n;

Types

Matrices are n-dimensional dependent arrays, represented by the shared buffer.Buf type. The element operations (buffer.alloc / buffer.read / buffer.write / buffer.lit / buffer.shape) and their lowering to mem.Ptr (buffer.lower_ptr) live in the buffer plugin.

Ring

Represents an algebraic ring. E.g., let nat_ring = (Nat, 0, core.nat.add, core.nat.mul);.

tensor.Ring is an alias for this type: tensor bufferizes onto *, so btensor is the lower of the two layers and owns the definition.

anx let Ring = [
T: *,
_0: T,
add: [T, T] → T,
mul: [T, T] → T,
];

Map-Reduce

The loop-nest core every other operation is lowered onto: product_2d and transpose_2d are plain lams over map_reduce, the remaining axioms are expanded by lower_map_reduce directly.

id

The mem-threaded CPS identity (with an empty epilogue-input pack) — the neutral post epilogue normalize_map_reduce delegates with.

anx fun id {T: *} (m: mem.M 0, x: T, _: [])@tt: [mem.M 0, T] = return (m, x);

map_reduce

map_reduce_post without the epilogue and with the neutral schedule — the buffer-world counterpart of tensor.map_reduce. Its normalizer delegates to map_reduce_post with post = id (so Tp = To), no epilogue inputs (nps = 0), no vector dim, and no unrolling.

Unlike tensor.map_reduce this cannot be a delegating lam: applying a lam instantiates its later group domains eagerly, and the is domain here contains the dependent axm application buffer.Buf (Ris#i, Sis#i, Tis#i), whose signature cannot be checked over the still-unsolved {Tis, Ris, Sis} holes — the tensor-level counterpart is a plain array and checks structurally. An axm application, in contrast, stays stuck until the checker has solved the holes from the operands.

axm map_reduce: {nis: Nat}
→ {To: *, Ro Rn: Nat}
→ [So: «Ro; Nat», Sr: «Rn; Nat»]
→ {Tis: «nis; *», Ris: «i: nis; Nat», Sis: «i: nis; «Ris#i; Nat»»}
→ [f: Fn [mem.M 0, To, «i: nis; Tis#i»] → [mem.M 0, To], init: To]
→ [map_out: [«Rn; affine.Index»] → «Ro; affine.Index»]
→ [maps: «i: nis; ([«Rn; affine.Index»] → «Ris#i; affine.Index»)»]
→ [mem.M 0, is: «i: nis; buffer.Buf (Ris#i, Sis#i, Tis#i)»]
→ [mem.M 0, buffer.Buf (Ro, So, To)], normalize_map_reduce;

map_reduce_post

The buffer-world counterpart of tensor.map_reduce_post and the workhorse behind every operation below: the affine-indexed generalisation of map_reduce on buffer.Buf operands, threading mem.M. Same groups in the same order as there, so only the differences:

  • f and the post epilogue are mem-threaded.
  • sched is the op's schedule — a chooser over the target's loop-nest builder (canonically a tensor.mk_sched value) selected in the frontend; lower_map_reduce binds it to mr_nest and merely applies the result to the op's decision-free fold step and write-back.
  • So/Sr are the logical output shape and loop bounds, while the buffer.Buf handles may have size-1 axes folded away.
axm map_reduce_post: {nis nps: Nat}
→ {To Tp: *, Ro Rn: Nat, TSched: □}
→ [So: «Ro; Nat», Sr: «Rn; Nat», sched: TSched]
→ {Tis: «nis; *», Ris: «i: nis; Nat», Sis: «i: nis; «Ris#i; Nat»»,
Tps: «nps; *», Rps: «j: nps; Nat», Sps: «j: nps; «Rps#j; Nat»»}
→ [f: Fn [mem.M 0, To, «i: nis; Tis#i»] → [mem.M 0, To], init: To,
post: Fn [mem.M 0, To, «j: nps; Tps#j»] → [mem.M 0, Tp]]
→ [map_out: [«Rn; affine.Index»] → «Ro; affine.Index»]
→ [maps: «i: nis; ([«Rn; affine.Index»] → «Ris#i; affine.Index»)»,
post_maps: «j: nps; ([«Ro; affine.Index»] → «Rps#j; affine.Index»)»]
→ [mem.M 0, is: «i: nis; buffer.Buf (Ris#i, Sis#i, Tis#i)»,
post_is: «j: nps; buffer.Buf (Rps#j, Sps#j, Tps#j)»]
→ [mem.M 0, buffer.Buf (Ro, So, Tp)];

mk_sched

The canonical schedule value: a chooser over the target's loop-nest builder, picking

  • vdim, the dim to run innermost as the unit-stride vector loop — a parallel dim runs over a row of accumulators, the innermost reduction dim selects the reduction-vectorized nest (see mr_nest), and vdim ≥ Rn means "none";
  • unroll, the number of trailing reduction dims to unroll into straight-line code.

tensor.mk_sched is an alias for this lam: btensor owns it for the same reason it owns Ring.

anx lam mk_sched (vdim unroll: Nat) (N: *) (mk: [vdim unroll: Nat] → N): N = mk (vdim, unroll);

proj_map

subs ↦ λ o. ‹j; o#(subs#j)›: the access map that reads input axis j at loop variable subs#j, e.g. ((0, 2), (2, 1)) for the two inputs of a matrix product. tensor.proj_map is an alias for this lam.

anx lam proj_map {n r: Nat} (subs: «r; Nat») (o: «n; affine.Index»): «r; affine.Index» =
‹j: r; o#(core.idx 0 n (subs#j))›;

Shape Operations

Pure re-indexing: every element of the result is one element of an input, so the operation lives entirely in the access map.

broadcast

Expands the dimensions of input to fit s_out: for all i, either s_in#i = s_out#i or s_in#i = 1, and the size-1 dimensions are expanded. The exception to the rule above: ri/si and ro/so are the actual — possibly size-1-folded — buffer shapes, inferred from the operands, while s_in/s_out are the logical ones.

axm broadcast: {T: *, ri: Nat, si: «ri; Nat», ro: Nat, so: «ro; Nat», r: Nat}
→ [s_in s_out: «r; Nat»]
→ [mem.M 0, input: buffer.Buf (ri, si, T)]
→ [mem.M 0, buffer.Buf (ro, so, T)];

concat

Joins nis buffers along axis ax: out[…o…] = is#k[… o with o#ax ↦ o#ax − off#k …], where k is the input whose range o#ax lands in and off are the prefix sums of the per-input extents along ax. s_out — the summed extent along ax, the shared extents elsewhere — is passed explicitly, since concat_shape lives in the tensor plugin.

axm concat: {T: *, nis r: Nat}
→ [ax: Idx r]
→ {Sis: «i: nis; «r; Nat»»}
→ [s_out: «r; Nat»]
→ [mem.M 0, is: «i: nis; buffer.Buf (r, Sis#i, T)»]
→ [mem.M 0, buffer.Buf (r, s_out, T)];

pad

Pads each axis: out[…o…] = input[…o−lo…] inside the interior, value (mode 0, constant) or the clamped edge element (mode ≠ 0, replicate) outside. s_out cannot be computed here — pad_shape lives in the tensor plugin — so it is passed as its own group in front of the operands, as in concat.

axm pad: {T: *, r: Nat}
→ [s_in: «r; Nat»]
→ [mode: Nat, lo hi: «r; Nat»]
→ [s_out: «r; Nat»]
→ [mem.M 0, input: buffer.Buf (r, s_in, T), value: T]
→ [mem.M 0, buffer.Buf (r, s_out, T)];

btensor_copy

Ignores the (⊥) accumulator and just returns the read value — the mem-threaded counterpart of tensor_copy.

fun btensor_copy {T: *} (m: mem.M 0, acc: T, y: «1; T»)@tt: [mem.M 0, T] = return (m, y#0_1);

transpose_2d

Permutes the dimensions of a 2-dimensional matrix. As there, the shape is a single «2; Nat» vector instead of two separate Nats and {T} precedes {s}; like product_2d this is a lam over map_reduce rather than an axm.

Transpose a matrix by reading the input at the swapped loop variables; there is no reduction loop.

anx lam transpose_2d {T: *} {s: «2; Nat»}
(m: mem.M 0, input: buffer.Buf (2, s, T))
: [mem.M 0, buffer.Buf (2, (s#tt, s#ff), T)] =
let s_out = (s#tt, s#ff);
map_reduce (s_out, s_out)
(btensor_copy, ⊥:T)
affine.id
(proj_map (1, 0),)
(m, input);

Reductions

Operations that fold a reduction dim away.

product_2d_fun

Fused multiply-add over the ring R; ab#0 is the t1 element, ab#1 the t2 element. The mem-threaded counterpart of the tensor plugin's conv_fun.

fun product_2d_fun (R: Ring) (m: mem.M 0, acc: R#T, ab: «2; R#T»)@tt: [mem.M 0, R#T] =
return (m, R#add (acc, R#mul (ab#0_2, ab#1_2)));

product_2d

Computes the matrix product of two 2-dimensional matrices in the ring R. [R: Ring] is the leading explicit group, as there: a ring cannot be recovered from R#T by inference.

Unlike tensor.product_2d this is a lam wrapping map_reduce directly, not an axm paired with an _impl annex and a lowering phase: nothing in this plugin inspects the high-level form — no fusion phase over *, and the normalizer was a no-op — so the extra staging bought nothing. transpose_2d follows the same pattern.

The loop vector is (i, j, k) and follows the principle ij ← ik, kj ( \(\mathit{out}_{i,j} = \sum_k \mathit{t1}_{i,k} \cdot \mathit{t2}_{k,j}\)), so k is the single reduction loop.

anx lam product_2d (R: Ring) {m k l: Nat}
(mm: mem.M 0, t1: buffer.Buf (2, (m, k), R#T), t2: buffer.Buf (2, (k, l), R#T))
: [mem.M 0, buffer.Buf (2, (m, l), R#T)] =
map_reduce ((m, l), (m, l, k))
(product_2d_fun R, R#_0)
(proj_map (0, 1))
(proj_map (0, 2), proj_map (2, 1))
(mm, (t1, t2));

Indexing

Data-dependent access: an index buffer picks the coordinate along dim.

gather

For each output coordinate o, reads the source at o with axis dim replaced by index[o]. The index element type proves that selected coordinates are in bounds along dim.

axm gather: {T: *, r: Nat}
→ [s_src s_idx: «r; Nat»]
→ [dim: Idx r]
→ [mem.M 0,
input: buffer.Buf (r, s_src, T),
index: buffer.Buf (r, s_idx, Idx (s_src#dim))]
→ [mem.M 0, buffer.Buf (r, s_idx, T)];

scatter

Starts with a copy of input, then applies updates in row-major index order; for duplicate destinations, the last update wins.

axm scatter: {T: *, r: Nat}
→ [s_src s_idx s_updates: «r; Nat»]
→ [dim: Idx r]
→ [mem.M 0,
input: buffer.Buf (r, s_src, T),
index: buffer.Buf (r, s_idx, Idx (s_src#dim)),
updates: buffer.Buf (r, s_updates, T)]
→ [mem.M 0, buffer.Buf (r, s_src, T)];

Loop Nests

Loop nests are first-class IR values here: the schedule of a map_reduce_post is not data interpreted by a lowering — it is a chooser over the target's loop-nest builder (see tensor.mk_sched), carried as an operand; lower_map_reduce binds it to mr_nest and merely applies the result. The builders below construct every loop structure in the frontend IR (loop-implementation policies, window unrolling, the row-accumulator interchange); partial evaluation unfolds them once the ranks are known.

ForT

The type of anything with affine.For's calling convention over [mem, A] accumulators — a "loop implementation" policy value.

lam ForT (A: *): * =
[Cn [I32, [mem.M 0, A], Cn [mem.M 0, A]]]
→ [Cn [mem.M 0, A]]
→ Cn [I32, I32, I32, [mem.M 0, A]];

affine_for

The default loop implementation: affine.For itself, eta-wrapped.

con affine_for {A: *} (body: Cn [I32, [mem.M 0, A], Cn [mem.M 0, A]])
(exit: Cn [mem.M 0, A])
(begin stop step: I32, init: [mem.M 0, A])@tt =
affine.For body exit (begin, stop, step, init);

vec_for

The loop implementation for a schedule's vector dim: the same affine.For, its body wrapped in the identity annotation ll.vec — the nomination to run this loop as the innermost unit-stride vectorized loop. affine.lower_for moves the wrapper onto the loop's exit condition for the backend (and drops it when the ll module is not loaded).

con vec_for {A: *} (body: Cn [I32, [mem.M 0, A], Cn [mem.M 0, A]])
(exit: Cn [mem.M 0, A])
(begin stop step: I32, init: [mem.M 0, A])@tt =
affine.For (ll.vec body) exit (begin, stop, step, init);

range_nest

One nest of loop-implemented loops over the dim range [lo, hi) of the bounds Sr, threading an accumulator A and the shared «r; I32» loop-vector prefix; calls inner at the bottom with the prefix updated at the range's positions. Composed by a right fold — no recursion, and the flat I32 prefix avoids dependently-typed loop-variable tuples.

lam range_nest (r: Nat, Sr: «r; Nat») (A: *) (loop: ForT A, lo hi: Nat)
(inner: Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]])
: Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]] =
lam wrap (d: Nat, w: Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]])
: Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]] =
con level (pref: «r; I32», m: mem.M 0, acc: A, k: Cn [mem.M 0, A]) =
loop lbody lexit (0I32, core.bitcast I32 (Sr#(core.idx 0 r d)), 1I32, (m, acc))
where
con lbody (iv: I32, macc: [mem.M 0, A], yield: Cn [mem.M 0, A]) =
let pref2 = ‹j: r; core.select (core.bitcast Nat j == d, iv, pref#j)›;
w (pref2, macc#0_2, macc#1_2, yield);
con lexit (macc: [mem.M 0, A]) = k macc;
end;
level;
let m = hi - lo;
vec.fold.r @(Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]], Nat) @m
wrap (‹i: m; lo + core.bitcast Nat i›, inner);

range_unroll

Like range_nest, but every dim in the range is fully unrolled: the inner continuation is chained once per iteration point by a right fold over the dim's (compile-time) extent — straight-line code with constant coordinates, no loops, and structurally terminating (no recursion, no PE filters: the fold itself unfolds once the extent is a literal).

lam range_unroll (r: Nat, Sr: «r; Nat») (A: *) (lo hi: Nat)
(inner: Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]])
: Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]] =
lam wrap (d: Nat, w: Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]])
: Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]] =
con level (pref: «r; I32», m: mem.M 0, acc: A, k: Cn [mem.M 0, A]) =
lam step (i: Nat, kk: Cn [mem.M 0, A]): Cn [mem.M 0, A] =
con at (m2: mem.M 0, a2: A) =
let pref2 = ‹j: r; core.select (core.bitcast Nat j == d, core.bitcast I32 i, pref#j)›;
w (pref2, m2, a2, kk);
at;
let t = Sr#(core.idx 0 r d);
(vec.fold.r @(Cn [mem.M 0, A], Nat) @t step (‹i: t; core.bitcast Nat i›, k)) (m, acc);
level;
let m = hi - lo;
vec.fold.r @(Cn [«r; I32», mem.M 0, A, Cn [mem.M 0, A]], Nat) @m
wrap (‹i: m; lo + core.bitcast Nat i›, inner);

NestT

The type of scheduled loop nests for a map_reduce over the domain Sr (ro leading parallel dims, rr trailing reduction dims, fold element type To): given the fold seed init, the per-point fold step cell (fold the elements at one loop vector into the accumulator) and the per-cell write-back wb (consume one folded value at its output coordinates), run the nest.

anx lam NestT (ro rr: Nat, Sr: «ro + rr; Nat», To U: *): * =
[init: To,
cell: Cn [mem.M 0, To, «ro + rr; I32», Cn [mem.M 0, To]],
wb: Cn [mem.M 0, U, To, «ro + rr; I32», Cn [mem.M 0, U]]]
→ Cn [mem.M 0, U, Cn [mem.M 0, U]];

mr_nest

The scheduled nest builder, selecting one of the four nests below from the schedule:

  • unroll trailing reduction dims always run as compile-time-unrolled straight-line code.
  • a vdim naming a parallel dim (currently the last one) of extent ≠ 1 runs it as the innermost loop, inside the reduction loops, over a row-accumulator buffer — the unit-stride vectorization dim.
  • a vdim naming the innermost reduction dim selects the reduction-vectorized nest (redvec below): the contraction runs as the vector loop with the last parallel dim unroll-and-jammed into its body.

Per-cell fold order is untouched by any of these transforms.

anx lam mr_nest (ro rr: Nat, Sr: «ro + rr; Nat», To U: *) (vdim unroll: Nat)
: NestT (ro, rr, Sr, To, U) =
let r = ro + rr;
// The unroll window must stay within the reduction dims.
let u_lo = r - unroll;
let u_lo = (ro, u_lo)#(ro <= u_lo);
lam nest (init: To,
cell: Cn [mem.M 0, To, «r; I32», Cn [mem.M 0, To]],
wb: Cn [mem.M 0, U, To, «r; I32», Cn [mem.M 0, U]])
: Cn [mem.M 0, U, Cn [mem.M 0, U]] =
con cell_at (pref: «r; I32», m: mem.M 0, acc: To, k: Cn [mem.M 0, To]) =
cell (m, acc, pref, k);
// The fully unrolled trailing window around the fold step.
let win = range_unroll (r, Sr) To (u_lo, r) cell_at;
// `vd` is `vdim` clamped through the dispatch condition: the non-selected branch is still
// ELABORATED, so its body must be type-sane even when `vdim` is the out-of-range sentinel.
let vec_ok = core.bit2.and_ 2 (vdim + 1 == ro, Sr#(core.idx 0 r ((0, vdim)#(vdim < r))) != 1);
let vd = (0, vdim)#vec_ok;
// CLASSIC: reduction loops inside all output loops, one accumulator cell at a time; the
// write-back target `U` is threaded through the output loops like any loop accumulator.
con classic (mem0: mem.M 0, u0: U, done: Cn [mem.M 0, U])@tt =
con at_cell (pref: «r; I32», m: mem.M 0, u: U, k: Cn [mem.M 0, U]) =
con folded (mem2: mem.M 0, v: To) =
wb (mem2, u, v, pref, k);
(range_nest (r, Sr) To (affine_for @To, ro, u_lo) win) (pref, m, init, folded);
(range_nest (r, Sr) U (affine_for @U, 0, ro) at_cell) (‹r; 0I32›, mem0, u0, done);
// VECTORIZED: the vector dim as the innermost loop over a row of accumulators — refill the
// row per output row, fold whole rows in lockstep, then write the row back cell by cell.
con vectorized (mem0: mem.M 0, u0: U, done: Cn [mem.M 0, U])@tt =
let Sv = Sr#(core.idx 0 r vd);
let vi_of = lm (pref: «r; I32»): Idx Sv = core.conv.u Sv (pref#(core.idx 0 r vd));
let (mem1, row) = buffer.alloc (1, Sv, To) mem0;
con at_row (pref: «r; I32», m: mem.M 0, u: U, k: Cn [mem.M 0, U]) =
con fill (pref2: «r; I32», memf: mem.M 0, uf: U, kf: Cn [mem.M 0, U]) =
let (memf2, row2) = buffer.write (1, Sv, To) (memf, row, vi_of pref2, init);
kf (memf2, uf);
con at_point (pref2: «r; I32», memp: mem.M 0, up: U, kp: Cn [mem.M 0, U]) =
let (memp1, acc0) = buffer.read (1, Sv, To) (memp, row, vi_of pref2);
con after_win (memp2: mem.M 0, accn: To) =
let (memp3, row3) = buffer.write (1, Sv, To) (memp2, row, vi_of pref2, accn);
kp (memp3, up);
win (pref2, memp1, acc0, after_win);
con wb_cell (pref2: «r; I32», memw: mem.M 0, uw: U, kw: Cn [mem.M 0, U]) =
let (memw1, v) = buffer.read (1, Sv, To) (memw, row, vi_of pref2);
wb (memw1, uw, v, pref2, kw);
con s2 (memB: mem.M 0, uB: U) =
(range_nest (r, Sr) U (vec_for @U, vd, vd + 1) wb_cell) (pref, memB, uB, k);
con s1 (memA: mem.M 0, uA: U) =
(range_nest (r, Sr) U (affine_for @U, ro, u_lo)
(range_nest (r, Sr) U (vec_for @U, vd, vd + 1) at_point))
(pref, memA, uA, s2);
(range_nest (r, Sr) U (vec_for @U, vd, vd + 1) fill) (pref, m, u, s1);
(range_nest (r, Sr) U (affine_for @U, 0, vd) at_row) (‹r; 0I32›, mem1, u0, done);
// BLOCKED (`vdim = ro − 2`): the two trailing parallel dims are sunk inside the reduction
// loops — the last dim runs as the vector loop and the BLOCK dim before it (any extent Sp;
// a strip-mined row block) is unrolled into its body (unroll-and-jam), so one pass over the
// row folds the whole block through a flattened «Sp·Sv; To» accumulator panel. Values
// hoisted out of the vector loop (a conv's window weights) now serve every row of the
// block, and adjacent rows' overlapping reads sit in the same loop body. Clamps as above:
// the non-selected branch is still elaborated.
let pd_c = (0, vdim)#(vdim < r);
let sd_r = vdim + 1;
let sd_c = (0, sd_r)#(sd_r < r);
let block_ok = core.bit2.and_ 2 (vdim + 2 == ro,
core.bit2.and_ 2 (Sr#(core.idx 0 r pd_c) != 1, Sr#(core.idx 0 r sd_c) != 1));
con blocked (mem0: mem.M 0, u0: U, done: Cn [mem.M 0, U])@tt =
let Sp = Sr#(core.idx 0 r pd_c);
let Sv = Sr#(core.idx 0 r sd_c);
let Np = Sp * Sv;
let vi_of = lm (pref: «r; I32»): Idx Np =
core.conv.u Np (core.wrap.add 0
(core.wrap.mul 0 (pref#(core.idx 0 r pd_c), core.bitcast I32 Sv),
pref#(core.idx 0 r sd_c)));
let (mem1, panel) = buffer.alloc (1, Np, To) mem0;
con at_block (pref: «r; I32», m: mem.M 0, u: U, k: Cn [mem.M 0, U]) =
con fill (pref2: «r; I32», memf: mem.M 0, uf: U, kf: Cn [mem.M 0, U]) =
let (memf2, panel2) = buffer.write (1, Np, To) (memf, panel, vi_of pref2, init);
kf (memf2, uf);
con at_point (pref2: «r; I32», memp: mem.M 0, up: U, kp: Cn [mem.M 0, U]) =
let (memp1, acc0) = buffer.read (1, Np, To) (memp, panel, vi_of pref2);
con after_win (memp2: mem.M 0, accn: To) =
let (memp3, panel3) = buffer.write (1, Np, To) (memp2, panel, vi_of pref2, accn);
kp (memp3, up);
win (pref2, memp1, acc0, after_win);
con wb_cell (pref2: «r; I32», memw: mem.M 0, uw: U, kw: Cn [mem.M 0, U]) =
let (memw1, v) = buffer.read (1, Np, To) (memw, panel, vi_of pref2);
wb (memw1, uw, v, pref2, kw);
con s2 (memB: mem.M 0, uB: U) =
(range_nest (r, Sr) U (affine_for @U, pd_c, sd_c)
(range_nest (r, Sr) U (vec_for @U, sd_c, ro) wb_cell)) (pref, memB, uB, k);
con s1 (memA: mem.M 0, uA: U) =
(range_nest (r, Sr) U (affine_for @U, ro, u_lo)
(range_nest (r, Sr) U (vec_for @U, sd_c, sd_c + 1)
(range_unroll (r, Sr) U (pd_c, pd_c + 1) at_point)))
(pref, memA, uA, s2);
(range_nest (r, Sr) U (affine_for @U, pd_c, sd_c)
(range_nest (r, Sr) U (vec_for @U, sd_c, ro) fill)) (pref, m, u, s1);
(range_nest (r, Sr) U (affine_for @U, 0, pd_c) at_block) (‹r; 0I32›, mem1, u0, done);
// REDUCTION-VECTORIZED (`ro ≤ vdim < r`: the schedule names the innermost REDUCTION dim):
// the contraction itself runs as the innermost (vector) loop. For a contraction whose
// operands store the contracted axis unit-stride per output cell (x·Wᵀ — the fc/linear
// layout, where each output neuron folds one weight ROW), both operands stream unit-stride
// and the fold is a reduction the backend vectorizes with a horizontal sum (permitted by
// its reassociation flags) — where vectorizing an output dim would gather across weight
// rows. Here `unroll` names the JAM WIDTH instead of a trailing window (the contraction
// cannot be both looped and unrolled): the trailing `unroll` parallel dims — small
// strip-mined blocks of output cells, see `dot_schedule_kvec` — are unrolled INTO the
// vector body (unroll-and-jam) over a flattened «Sb; To» accumulator panel. The jammed
// cells share every load that is invariant in one of the jam dims (the input row across
// output neurons, the weight row across batch rows), and each panel cell is an independent
// reduction chain that gets its own vector accumulator. Clamps as above: the non-selected
// branch is still elaborated, and the jam extents are additionally clamped to 1 (`Sr_rv`)
// so the elaborated unrolls stay cheap.
let rv_j = (1, unroll)#(1 <= unroll);
let rv_bd = (0, ro - rv_j)#(rv_j <= ro);
let rv_vd = (0, vdim)#(vdim < r);
let redvec_ok = core.bit2.and_ 2 (ro <= vdim, core.bit2.and_ 2 (vdim + 1 == r, rv_j <= ro));
let Sr_rv = ‹j: r; let jj = core.bitcast Nat j;
(Sr#j, (1, Sr#j)#redvec_ok)#(core.bit2.and_ 2 (rv_bd <= jj, jj < ro))›;
lam rv_mul (d: Nat, acc: Nat): Nat = acc * Sr_rv#(core.idx 0 r d);
let rv_ds = ‹i: rv_j; rv_bd + core.bitcast Nat i›;
let Sb = vec.fold.r @(Nat, Nat) @rv_j rv_mul (rv_ds, 1);
con redvec (mem0: mem.M 0, u0: U, done: Cn [mem.M 0, U])@tt =
// Flatten a loop vector's jam coordinates into the panel (any per-dim-bounded
// linearization is a bijection into «Sb»; the exact order is internal to the panel).
lam rv_lin (pref: «r; I32») (d: Nat, acc: I32): I32 =
core.wrap.add 0 (core.wrap.mul 0 (acc, core.bitcast I32 (Sr_rv#(core.idx 0 r d))),
pref#(core.idx 0 r d));
let bi_of = lm (pref: «r; I32»): Idx Sb =
core.conv.u Sb (vec.fold.r @(I32, Nat) @rv_j (rv_lin pref) (rv_ds, 0I32));
let (mem1, panel) = buffer.alloc (1, Sb, To) mem0;
con at_block (pref: «r; I32», m: mem.M 0, u: U, k: Cn [mem.M 0, U]) =
con fill (pref2: «r; I32», memf: mem.M 0, uf: U, kf: Cn [mem.M 0, U]) =
let (memf2, panel2) = buffer.write (1, Sb, To) (memf, panel, bi_of pref2, init);
kf (memf2, uf);
con at_point (pref2: «r; I32», memp: mem.M 0, up: U, kp: Cn [mem.M 0, U]) =
let (memp1, acc0) = buffer.read (1, Sb, To) (memp, panel, bi_of pref2);
con after_cell (memp2: mem.M 0, accn: To) =
let (memp3, panel3) = buffer.write (1, Sb, To) (memp2, panel, bi_of pref2, accn);
kp (memp3, up);
// `cell` directly — `unroll` names the jam width here, not a trailing window.
cell (memp1, acc0, pref2, after_cell);
con wb_cell (pref2: «r; I32», memw: mem.M 0, uw: U, kw: Cn [mem.M 0, U]) =
let (memw1, v) = buffer.read (1, Sb, To) (memw, panel, bi_of pref2);
wb (memw1, uw, v, pref2, kw);
con s2 (memB: mem.M 0, uB: U) =
(range_unroll (r, Sr_rv) U (rv_bd, ro) wb_cell) (pref, memB, uB, k);
con s1 (memA: mem.M 0, uA: U) =
(range_nest (r, Sr) U (affine_for @U, ro, rv_vd)
(range_nest (r, Sr) U (vec_for @U, rv_vd, rv_vd + 1)
(range_unroll (r, Sr_rv) U (rv_bd, ro) at_point)))
(pref, memA, uA, s2);
(range_unroll (r, Sr_rv) U (rv_bd, ro) fill) (pref, m, u, s1);
(range_nest (r, Sr) U (affine_for @U, 0, rv_bd) at_block) (‹r; 0I32›, mem1, u0, done);
con run (mem0: mem.M 0, u0: U, done: Cn [mem.M 0, U])@tt =
((((classic, vectorized)#vec_ok, blocked)#block_ok, redvec)#redvec_ok) (mem0, u0, done);
run;
nest;

Phases

lower_map_reduce

Lowers every operation of this plugin to affine.For loops over buffer element operations, binding each op's sched chooser to mr_nest.

axm lower_map_reduce: compile.Phase;