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 the pointwise and indexing operations (broadcast, pad, concat, gather, scatter), 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. Every op is in destination-passing style: it takes the output buffer dst right after the memory, writes into it, and returns it. Allocation is thus explicit and belongs to the caller (buffer.alloc), and an in-place update is a mere choice of dst. 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 btensor, 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; 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, dst: buffer.Buf (Ro, So, To), 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:

  • dst is the output buffer: the op writes into it and returns it.
  • 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, dst: buffer.Buf (Ro, So, Tp),
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)];

Sched

The schedule record a nest builder is handed:

  • 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.

Transforms update the field they decide (s#vdim ← d) and pass the rest through. tensor.Sched is an alias for this type: btensor owns it for the same reason it owns Ring.

anx let Sched = [vdim unroll: Nat];

SchedT

The type of a schedule chooser: for whatever nest type N the lowering instantiates, it is handed the target's builder mk and returns the nest to run.

anx let SchedT = [N: *] → (Sched → N) → N;

mk_sched

The canonical chooser: it hands the builder s itself.

anx lam mk_sched (s: Sched) (N: *) (mk: Sched → N): N = mk s;

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, dst: buffer.Buf (ro, so, T), 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, dst: buffer.Buf (r, s_out, T), 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, dst: buffer.Buf (r, s_out, T), input: buffer.Buf (r, s_in, T), value: T]
→ [mem.M 0, buffer.Buf (r, s_out, T)];

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,
dst: buffer.Buf (r, s_idx, T),
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

Copies input into dst, 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,
dst: buffer.Buf (r, s_src, T),
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);

novec_for

The loop implementation for the loops enclosing a schedule's vector loop: affine.For with its body wrapped in ll.novec, so that LLVM vectorizes the nominated loop and none of the strided loops around it.

con novec_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.novec 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, (m: mem.M 0, acc: A), yield: Cn [mem.M 0, A]) =
let pref = ‹j: r; core.select (core.idx2nat j == d, iv, pref#j)›;
w (pref, m, acc, 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.idx2nat 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 (m: mem.M 0, acc: A) =
let pref = ‹j: r; core.select (core.idx2nat j == d, core.bitcast I32 i, pref#j)›;
w (pref, m, acc, kk);
at;
let t = Sr#(core.idx 0 r d);
(vec.fold.r @(Cn [mem.M 0, A], Nat) @t step (‹i: t; core.idx2nat 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.idx2nat 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 the last parallel dim 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 second-to-last parallel dim (ro − 2) selects the blocked nest: the last parallel dim runs as the vector loop with the block dim before it unroll-and-jammed into its body.
  • 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: *) (s: Sched)
: NestT (ro, rr, Sr, To, U) =
let vdim = s#vdim;
let unroll = s#unroll;
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 (m: mem.M 0, u: 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 (m: mem.M 0, v: To) =
wb (m, 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›, m, u, 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 (m: mem.M 0, u: 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 (m, row) = buffer.alloc (1, Sv, To) m;
con done_row (m: mem.M 0, u: U) =
let m = buffer.free (1, Sv, To) (m, row);
done (m, u);
con at_row (pref: «r; I32», m: mem.M 0, u: U, k: Cn [mem.M 0, U]) =
con fill (pref: «r; I32», m: mem.M 0, uf: U, kf: Cn [mem.M 0, U]) =
let (m, _) = buffer.write (1, Sv, To) (m, row, vi_of pref, init);
kf (m, uf);
con at_point (pref: «r; I32», m: mem.M 0, up: U, kp: Cn [mem.M 0, U]) =
let (m, acc) = buffer.read (1, Sv, To) (m, row, vi_of pref);
con after_win (m: mem.M 0, acc: To) =
let (m, _) = buffer.write (1, Sv, To) (m, row, vi_of pref, acc);
kp (m, up);
win (pref, m, acc, after_win);
con wb_cell (pref: «r; I32», m: mem.M 0, uw: U, kw: Cn [mem.M 0, U]) =
let (m, v) = buffer.read (1, Sv, To) (m, row, vi_of pref);
wb (m, uw, v, pref, kw);
con s2 (m: mem.M 0, u: U) =
(range_nest (r, Sr) U (vec_for @U, vd, vd + 1) wb_cell) (pref, m, u, k);
con s1 (m: mem.M 0, u: U) =
(range_nest (r, Sr) U (novec_for @U, ro, u_lo)
(range_nest (r, Sr) U (vec_for @U, vd, vd + 1) at_point))
(pref, m, u, s2);
(range_nest (r, Sr) U (vec_for @U, vd, vd + 1) fill) (pref, m, u, s1);
(range_nest (r, Sr) U (novec_for @U, 0, vd) at_row) (‹r; 0I32›, m, u, done_row);
// 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 (m: mem.M 0, u: 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 (m, panel) = buffer.alloc (1, Np, To) m;
con done_panel (m: mem.M 0, u: U) =
let m = buffer.free (1, Np, To) (m, panel);
done (m, u);
con at_block (pref: «r; I32», m: mem.M 0, u: U, k: Cn [mem.M 0, U]) =
con fill (pref: «r; I32», m: mem.M 0, uf: U, kf: Cn [mem.M 0, U]) =
let (m, _) = buffer.write (1, Np, To) (m, panel, vi_of pref, init);
kf (m, uf);
con at_point (pref: «r; I32», m: mem.M 0, up: U, kp: Cn [mem.M 0, U]) =
let (m, acc) = buffer.read (1, Np, To) (m, panel, vi_of pref);
con after_win (m: mem.M 0, acc: To) =
let (m, _) = buffer.write (1, Np, To) (m, panel, vi_of pref, acc);
kp (m, up);
win (pref, m, acc, after_win);
con wb_cell (pref: «r; I32», m: mem.M 0, uw: U, kw: Cn [mem.M 0, U]) =
let (m, v) = buffer.read (1, Np, To) (m, panel, vi_of pref);
wb (m, uw, v, pref, kw);
con s2 (m: mem.M 0, u: U) =
(range_nest (r, Sr) U (novec_for @U, pd_c, sd_c)
(range_nest (r, Sr) U (vec_for @U, sd_c, ro) wb_cell)) (pref, m, u, k);
con s1 (m: mem.M 0, u: U) =
(range_nest (r, Sr) U (novec_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, m, u, s2);
(range_nest (r, Sr) U (novec_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 (novec_for @U, 0, pd_c) at_block) (‹r; 0I32›, m, u, done_panel);
// 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.idx2nat 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.idx2nat i›;
let Sb = vec.fold.r @(Nat, Nat) @rv_j rv_mul (rv_ds, 1);
con redvec (m: mem.M 0, u: 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 (m, panel) = buffer.alloc (1, Sb, To) m;
con done_panel (m: mem.M 0, u: U) =
let m = buffer.free (1, Sb, To) (m, panel);
done (m, u);
con at_block (pref: «r; I32», m: mem.M 0, u: U, k: Cn [mem.M 0, U]) =
con fill (pref: «r; I32», m: mem.M 0, uf: U, kf: Cn [mem.M 0, U]) =
let (m, _) = buffer.write (1, Sb, To) (m, panel, bi_of pref, init);
kf (m, uf);
con at_point (pref: «r; I32», m: mem.M 0, up: U, kp: Cn [mem.M 0, U]) =
let (m, acc) = buffer.read (1, Sb, To) (m, panel, bi_of pref);
con after_cell (m: mem.M 0, acc: To) =
let (m, _) = buffer.write (1, Sb, To) (m, panel, bi_of pref, acc);
kp (m, up);
// `cell` directly — `unroll` names the jam width here, not a trailing window.
cell (m, acc, pref, after_cell);
con wb_cell (pref: «r; I32», m: mem.M 0, uw: U, kw: Cn [mem.M 0, U]) =
let (m, v) = buffer.read (1, Sb, To) (m, panel, bi_of pref);
wb (m, uw, v, pref, kw);
con s2 (m: mem.M 0, u: U) =
(range_unroll (r, Sr_rv) U (rv_bd, ro) wb_cell) (pref, m, u, k);
con s1 (m: mem.M 0, u: U) =
(range_nest (r, Sr) U (novec_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, m, u, s2);
(range_unroll (r, Sr_rv) U (rv_bd, ro) fill) (pref, m, u, s1);
(range_nest (r, Sr) U (novec_for @U, 0, rv_bd) at_block) (‹r; 0I32›, m, u, done_panel);
con run (m: mem.M 0, u: U, done: Cn [mem.M 0, U])@tt =
((((classic, vectorized)#vec_ok, blocked)#block_ok, redvec)#redvec_ok) (m, u, done);
run;
nest;

map_reduce_post_impl

Destination-passing map-reduce: apply the selected nest to ordinary fold and write-back functions.

priv anx lam map_reduce_post_impl {nis nps: Nat}
{To Tp: *, Ro Rr: Nat}
(So: «Ro; Nat», Sr: «Ro + Rr; Nat», sched: SchedT)
{Tis: «nis; *», Tps: «nps; *»}
(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: «Ro + Rr; affine.Index» → «Ro; affine.Index»)
(read_inputs: Fn [mem.M 0, «d: Ro + Rr; Idx (Sr#d)»] → [mem.M 0, «i: nis; Tis#i»],
read_post: Fn [mem.M 0, «d: Ro; Idx (So#d)»] → [mem.M 0, «j: nps; Tps#j»],
write_output: Fn [mem.M 0, buffer.Buf (Ro, So, Tp), «d: Ro; Idx (So#d)», Tp]
→ mem.M 0)
(m: mem.M 0, dst: buffer.Buf (Ro, So, Tp)): [mem.M 0, buffer.Buf (Ro, So, Tp)] =
let Rn = Ro + Rr;
let Output = buffer.Buf (Ro, So, Tp);
// Explicit runtime parameters keep upstream memory expressions outside the loop builders.
fun run (m: mem.M 0, dst: Output)@tt: [mem.M 0, Output] =
con cell (m: mem.M 0, acc: To, ivs: «Rn; I32», k: Cn [mem.M 0, To])@tt =
let indices = ‹d: Rn; core.conv.u (Sr#d) (ivs#d)›;
con combine (m: mem.M 0, values: «i: nis; Tis#i»)@tt = f ((m, acc, values), k);
read_inputs ((m, indices), combine);
con wb (m: mem.M 0, output: Output, acc: To, ivs: «Rn; I32», k: Cn [mem.M 0, Output])@tt =
let indices = ‹d: Rn; core.conv.u (Sr#d) (ivs#d, 0I32)#(Ro <= core.bitcast Nat d)›;
let (m, coords) = affine.map @(Ro, Rn) @(Sr, So) map_out indices @0 m;
con write (m: mem.M 0, value: Tp)@tt =
con written (m: mem.M 0)@tt = k (m, output);
write_output ((m, output, coords, value), written);
con epilogue (m: mem.M 0, values: «j: nps; Tps#j»)@tt = post ((m, acc, values), write);
read_post ((m, coords), epilogue);
let nest = sched (NestT (Ro, Rr, Sr, To, Output)) (mr_nest (Ro, Rr, Sr, To, Output));
nest (init, cell, wb) (m, dst, return);
cps.cps2ds run (m, dst);

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;