MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
lower_to_mem.cpp
Go to the documentation of this file.
2
3#include <fe/worklist.h>
4
5#include <mim/axm.h>
6#include <mim/def.h>
7#include <mim/lam.h>
8
12#include <mim/plug/core/core.h>
13#include <mim/plug/cps/cps.h>
14#include <mim/plug/mem/mem.h>
15
18
20
21namespace {
22
23/// Does `t` (recursively through immutable sigmas) contain a Pi?
24bool contains_pi(const Def* t) {
25 if (t->isa<Pi>()) return true;
26 if (auto sig = t->isa_imm<Sigma>())
27 for (auto op : sig->ops())
28 if (contains_pi(op)) return true;
29 return false;
30}
31
32/// Peels nested `Pack`s off `d`; returns the innermost body iff `d` is a constant splat — every axis a
33/// `Pack` (so `d` is uniform, not e.g. a `Tuple` of distinct rows), bottoming out in a closed scalar
34/// (index-independent, non-array). So `‹784, 1024; 1e-3›` yields the scalar `1e-3`, while an index-dependent
35/// pack `‹i; f i›` or a genuine literal `((1, 2), (3, 4))` yields `nullptr`.
36const Def* splat_scalar(const Def* d) {
37 if (!d->isa<Pack>()) return nullptr;
38 while (auto pack = d->isa<Pack>())
39 d = pack->body();
40 return (d->is_closed() && !d->type()->isa<Arr>()) ? d : nullptr;
41}
42
43/// Is `app` a tensor op whose lowering consumes a LowerToMem::fresh_mem?
44bool wants_fresh_mem(const App* app) {
48}
49
50/// Is `app` one of the tensor ops this phase bufferizes?
51bool is_tensor_op(const App* app) { return Axm::isa<tensor::splat>(app) || wants_fresh_mem(app); }
52
53/// The Lam a - possibly projected - parameter belongs to; `nullptr` for anything else.
54Lam* param_of(const Def* d) {
55 while (auto ex = d->isa<Extract>())
56 d = ex->tuple();
57 if (auto var = d->isa<Var>()) return var->binder()->isa_mut<Lam>();
58 return nullptr;
59}
60
61/// Does @p d - a value or an aggregate of them - contain a `buffer.Buf`?
62bool holds_buf(const Def* d) {
63 if (Axm::isa<buffer::Buf>(d->type())) return true;
64 if (auto tuple = d->isa<Tuple>()) return std::ranges::any_of(tuple->ops(), holds_buf);
65 return false;
66}
67
68/// @p buf, or its sole element if @p app's result has been folded to a scalar.
69const Def* scalar_result(const App* app, const Def* mem, const Def* buf) {
70 if (app->type()->isa<Arr>()) return buf;
71 return buffer::op_read(mem, buf, buf->world().tuple())->proj(2, 1);
72}
73
74} // namespace
75
76void LowerToMem::collect_tensor_types() {
77 // The default pipeline lowers tensors exclusively through buffers — there is no value-semantics
78 // fallback. A program shape the conversion cannot handle is a hard error, not silent residue.
79 auto gate = [](const char* why, const Def* culprit) { fe::throwf("cannot bufferize: {} (`{}`)", why, culprit); };
80 auto elem = [&gate](const Def* T) {
81 if (T->isa<Arr>()) gate("tensor with array element type", T);
82 };
83 // Fully folded shapes (`«1; T»` ≡ `T`) denote plain scalars — recording them would poison every
84 // function whose signature mentions the element type, so only genuine array types count as tensors.
85 auto add_tensor_ty = [this](const Def* t) {
86 if (t->isa<Arr>()) tensor_ty_.emplace(t);
87 };
88
89 fe::BFSWorklist<DefSet> wl;
90 auto push = [&wl](const Def* d) {
91 if (d) wl.push(d);
92 };
93
94 for (auto mut : old_world().externals().muts())
95 push(mut);
96
97 while (!wl.empty()) {
98 auto def = wl.pop();
99
100 if (auto app = def->isa<App>()) {
101 auto [axm, curry, trip] = Axm::get(app);
102 bool tensor_op = axm && curry == 0 && axm->plugin() == tensor::Plugin_Id;
103 ops_seen_ |= tensor_op;
104 if (Axm::isa<tensor::buf>(app)) {
105 // What an array value *is* cannot be read off its type - an `(x y: I32)` group *is* a
106 // `«2; I32»` - so a tensor no other tensor op mentions says so itself.
107 add_tensor_ty(app->type());
108 } else if (Axm::isa<tensor::splat>(app)) {
109 auto [T, r] = app->callee()->as<App>()->args<2>();
110 elem(T);
111 add_tensor_ty(app->type());
112 } else if (Axm::isa<tensor::generate>(app)) {
113 auto [meta, s_out] = app->callee()->as<App>()->uncurry_args<2>();
114 auto [T, r] = meta->projs<2>();
115 elem(T);
116 if (!Lit::isa<u64>(r)) gate("non-literal rank of `tensor.generate`", app);
117 add_tensor_ty(app->type());
118 } else if (Axm::isa<tensor::map_reduce_post>(app)) {
119 // result and each of the `nis` inputs / `nps` epilogue inputs are tensors.
120 add_tensor_ty(app->type());
121 auto mr = btensor::MapRedPost::of(app->callee()->as<App>());
122 auto [is, post_is] = app->arg()->projs<2>();
123 elem(mr.To), elem(mr.Tp);
124 if (auto nis_l = Lit::isa<u64>(mr.nis))
125 for (u64 i = 0; i < *nis_l; ++i) {
126 elem(mr.Tis->proj(*nis_l, i));
127 add_tensor_ty(is->proj(*nis_l, i)->type());
128 }
129 if (auto nps_l = Lit::isa<u64>(mr.nps))
130 for (u64 j = 0; j < *nps_l; ++j) {
131 elem(mr.Tps->proj(*nps_l, j));
132 add_tensor_ty(post_is->proj(*nps_l, j)->type());
133 }
134 } else if (Axm::isa<tensor::broadcast>(app)) {
135 // result «s_out; T» and input «s_in; T» (the 3rd argument) are tensors.
136 auto [T, r] = app->callee()->as<App>()->args<2>();
137 elem(T);
138 add_tensor_ty(app->type());
139 add_tensor_ty(app->arg()->proj(3, 2)->type());
140 } else if (Axm::isa<tensor::pad>(app)) {
141 // callee: pad {T, r} [s_in] [mode, lo, hi]; result «s_out; T» and `input` are tensors.
142 auto [Tr, s_in, params] = app->callee()->as<App>()->uncurry_args<3>();
143 auto [T, r] = Tr->projs<2>();
144 auto [mode, lo, hi] = params->projs<3>();
145 elem(T);
146 if (!Lit::isa<u64>(r) || !Lit::isa<u64>(mode)) gate("non-literal rank/mode of `tensor.pad`", app);
147 add_tensor_ty(app->type());
148 add_tensor_ty(app->arg()->proj(2, 0)->type());
149 } else if (Axm::isa<tensor::concat>(app)) {
150 // callee: concat {T, nis, r} [ax] {Sis}; result «s_out; T» and each input are tensors.
151 auto [TnisR, ax, Sis] = app->callee()->as<App>()->uncurry_args<3>();
152 auto [T, nis, r] = TnisR->projs<3>();
153 elem(T);
154 auto nis_l = Lit::isa<u64>(nis);
155 auto r_l = Lit::isa<u64>(r);
156 auto ax_l = Lit::isa<u64>(ax);
157 if (!nis_l || !r_l || !ax_l) {
158 gate("non-literal nis/rank/axis of `tensor.concat`", app);
159 } else {
160 add_tensor_ty(app->type());
161 for (u64 i = 0; i < *nis_l; ++i) {
162 // The loop generation needs literal extents along `ax` for the prefix offsets.
163 if (!Lit::isa<u64>(Sis->proj(*nis_l, i)->proj(*r_l, *ax_l)))
164 gate("non-literal extent along the concat axis", app);
165 add_tensor_ty(app->arg()->proj(*nis_l, i)->type());
166 }
167 }
168 } else if (Axm::isa<tensor::if_static>(app)) {
169 // Value-level binding-time dispatch; this phase's rewrite residualizes it to its
170 // dynamic branch - not a tensor op.
171 } else if (Axm::isa<tensor::gather>(app)) {
172 auto [Tr, shapes, dim] = app->callee()->as<App>()->uncurry_args<3>();
173 auto [T, r] = Tr->projs<2>();
174 elem(T);
175 if (!Lit::isa<u64>(r) || !Lit::isa<u64>(dim)) gate("non-literal rank/axis of `tensor.gather`", app);
176 add_tensor_ty(app->type());
177 add_tensor_ty(app->arg()->proj(2, 0)->type());
178 add_tensor_ty(app->arg()->proj(2, 1)->type());
179 } else if (Axm::isa<tensor::scatter>(app)) {
180 auto [Tr, shapes, dim] = app->callee()->as<App>()->uncurry_args<3>();
181 auto [T, r] = Tr->projs<2>();
182 elem(T);
183 if (!Lit::isa<u64>(r) || !Lit::isa<u64>(dim)) gate("non-literal rank/axis of `tensor.scatter`", app);
184 add_tensor_ty(app->type());
185 add_tensor_ty(app->arg()->proj(3, 0)->type());
186 add_tensor_ty(app->arg()->proj(3, 1)->type());
187 add_tensor_ty(app->arg()->proj(3, 2)->type());
188 } else if (tensor_op) {
189 // Any other tensor op (a symbolic `shape`, …) has no buffer-world lowering.
190 gate("unbufferizable tensor op", app);
191 }
192 // Lams passed inside a tensor op's curry chain (combiners, affine index maps, schedule
193 // nests) are element-level — TRANSITIVELY, including their local helper lams: e.g. the
194 // loop-vector prefixes «r; I32» inside a schedule nest must not be mistaken for value
195 // tensors of a recorded «r; I32» tensor type.
196 if (is_tensor_op(app)) {
197 auto args = fe::BFSWorklist<DefSet>();
198 for (const App* a = app; a; a = a->callee()->isa<App>())
199 args.push(a->arg());
200 while (!args.empty()) {
201 auto d = args.pop();
202 if (auto k = d->isa_mut<Lam>()) op_args_.emplace(k);
203 for (auto op : d->ops())
204 if (op) args.push(op);
205 }
206 }
207 }
208
209 for (auto op : def->ops())
210 push(op);
211 push(def->type());
212 }
213
214 for (auto mut : old_world().externals().muts())
215 if (auto lam = mut->isa_mut<Lam>(); lam && is_tensor_fn(lam)) tensor_fns_.emplace(lam);
216 // No tensor boundaries AND no tensor ops: nothing for the sweeps below to check.
217 // (Ops without boundaries still bufferize: their value-world operands are materialized.)
218 if (tensor_fns_.empty() && !ops_seen_) return;
219
220 // Higher-order bufferized functions: a continuation parameter whose domain itself contains a Pi would
221 // need boundary conversion inside nested continuation types, which `conv_boundary` does not perform.
222 for (auto old_fn : tensor_fns_) {
223 auto dom = old_fn->type()->dom();
224 auto n = dom->num_projs();
225 for (size_t i = 0; i != n; ++i)
226 if (auto pi = Pi::isa_cn(dom->proj(n, i)); pi && contains_pi(pi->dom()))
227 return gate("higher-order bufferized function", old_fn);
228 }
229
230 // A partial access - an index that stops short of the element - denotes a sub-tensor, which has no buffer
231 // of its own; `tensor.get`/`set` used to reject this by construction.
232 auto covers_every_axis = [this](const Def* agg, const Def* index) {
233 auto arr = agg->type()->isa<Arr>();
234 if (!arr || !tensor_ty_.contains(arr)) return true;
235 auto r = arr->shape().rank();
236 auto k = Shape(index).rank();
237 return !r || !k || *k >= *r;
238 };
239
240 // Second sweep: shapes the conversion cannot adapt — the value-semantics path lowers everything instead.
241 fe::BFSWorklist<DefSet> wl2;
242 for (auto mut : old_world().externals().muts())
243 wl2.push(mut);
244 while (!wl2.empty()) {
245 auto def = wl2.pop();
246
247 if (auto ex = def->isa<Extract>(); ex && !covers_every_axis(ex->tuple(), ex->index()))
248 ex->blame("cannot bufferize: partial tensor read yields a sub-tensor").bail();
249 if (auto in = def->isa<Insert>(); in && !covers_every_axis(in->tuple(), in->index()))
250 in->blame("cannot bufferize: partial tensor write targets a sub-tensor").bail();
251
252 for (auto op : def->ops()) {
253 if (!op) continue;
254 if (auto fn = op->isa_mut<Lam>()) {
255 // A bufferized function referenced as a value (not as the callee of a call, and not the
256 // binder back-reference of its own variable) cannot be adapted.
257 if (tensor_fns_.contains(fn))
258 if (!(def->isa<App>() && def->as<App>()->callee() == op) && !def->isa<Var>())
259 return gate("bufferized function used as a value", fn);
260 if (!tensor_fns_.contains(fn) && !op_args_.contains(fn) && mentions_tensor(fn->type()->dom())) {
261 // A tensor-typed function the conversion does not rewrite: an unset external keeps its
262 // value ABI, a direct-style local cannot be rebuilt as a continuation — either way a
263 // bufferized caller would pass a buffer against a value-array signature.
264 if (fn->is_external() || !Pi::isa_cn(fn->type()))
265 return gate("unconvertible tensor-typed function", fn);
266 }
267 }
268 wl2.push(op);
269 }
270 if (def->type()) wl2.push(def->type());
271 }
272}
273
275 collect_tensor_types(); // hard-errors on shapes the conversion cannot handle
276 // Nothing tensor-related in the program: skip the whole-world rebuild entirely.
277 if (tensor_fns_.empty() && !ops_seen_) return;
279 // Every fresh-memory continuation must have found an enclosing lam to be chained into.
280 assert(pending_.empty());
281}
282
283const Def* LowerToMem::buf_of(const Def* arr_ty) {
284 auto& w = new_world();
285 auto arr = arr_ty->isa<Arr>();
286 if (!arr) return buffer::type_buf(w.lit_nat_0(), w.tuple(), rewrite(arr_ty));
287 auto shape = rewrite(*arr->shape());
288 return buffer::type_buf(shape->arity(), shape, rewrite(arr->body()));
289}
290
291const Def* LowerToMem::bot_mem() {
292 auto& w = new_world();
293 return w.bot(w.call<mem::M>(0));
294}
295
296const Def* LowerToMem::fresh_mem() {
297 auto k = mem::mut_con(new_world())->set("fresh_mem");
298 pending_.push_back(k);
299 return k->var();
300}
301
302const Def* LowerToMem::wrap_fresh_mem(const Def* body) {
303 auto& w = new_world();
304 // The last-minted continuation carries the original body; the lam ends up requesting the first memory.
305 for (auto k : pending_ | std::views::reverse) {
306 k->set(true, body); // filter `tt`: k vanishes as soon as AddMem substitutes the real memory
307 body = w.app(w.annex<mem::fresh>(), w.tuple({w.lit_nat_0(), k}));
308 }
309 return body;
310}
311
312const Def* LowerToMem::rewrite(const Def* old_def) {
313 // An op lowering that consumes a fresh memory references its receiving continuation's var (see
314 // fresh_mem). The global memo would share such a lowering with every other function that mentions the
315 // same (closed) old op - where that var would dangle - so these ops are memoized per enclosing lam.
316 auto app = old_def->isa<App>();
317 auto ins = old_def->isa<Insert>(); // allocates the copy it writes into
318 if ((app && wants_fresh_mem(app)) || (ins && tensor_ty_.contains(ins->tuple()->type()))) {
319 if (auto new_def = fe::lookup(fresh_memo_, old_def)) return new_def;
320 auto new_def = app ? rewrite_imm_App(app) : rewrite_imm_Insert(ins);
321 fresh_memo_.emplace(old_def, new_def);
322 return new_def;
323 }
324 return RWPhase::rewrite(old_def);
325}
326
327bool LowerToMem::mentions_tensor(const Def* t) const {
328 if (tensor_ty_.contains(t)) return true;
329 if (auto sig = t->isa<Sigma>()) {
330 for (auto op : sig->ops())
331 if (mentions_tensor(op)) return true;
332 } else if (auto pi = t->isa<Pi>()) {
333 return mentions_tensor(pi->dom()); // descend into continuation domains, but never into `Arr` elements
334 }
335 return false;
336}
337
338bool LowerToMem::is_tensor_fn(Lam* lam) const {
339 return lam->is_external() && lam->is_set() && mentions_tensor(lam->type()->dom());
340}
341
342const Def* LowerToMem::conv_boundary(const Def* t) {
343 if (tensor_ty_.contains(t)) return buf_of(t);
344 if (auto sig = t->isa_imm<Sigma>(); sig && mentions_tensor(sig)) {
345 auto n = sig->num_ops();
346 DefVec ops(n);
347 for (size_t i = 0; i != n; ++i)
348 ops[i] = conv_boundary(sig->op(i));
349 return new_world().sigma(ops);
350 }
351 return rewrite(t);
352}
353
355 // Scope the fresh-memory bookkeeping: ops lowered while this body is rewritten mint their receiving
356 // continuations into pending_, which are chained in front of the finished body. Nested lams anchor
357 // their own requests (and their own per-lam op memo).
358 auto _ = fe::Restore(pending_, {});
359 auto __ = fe::Restore(fresh_memo_, {});
360 auto new_lam = new_world().mut_lam(conv_type(lam));
361 map(lam, new_lam);
362 if (!lam->is_set()) return new_lam;
363
364 auto ___ = enter(lam);
365 auto filter = rewrite(lam->filter());
366 auto body = wrap_fresh_mem(rewrite(lam->body()));
367 new_lam->set(filter, body);
368 return seal(lam, new_lam);
369}
370
371const Pi* LowerToMem::conv_type(Lam* lam) {
372 auto& w = new_world();
373 // A bufferized function: convert tensor-typed parameters to `buffer.Buf`, including inside grouped
374 // sigma parameters and continuation domains. No memory is introduced here — AddMem does that.
375 if (is_tensor_fn(lam)) {
376 auto dom = lam->type()->dom();
377 auto n = dom->num_projs();
378 DefVec doms(n, [&](size_t i) -> const Def* {
379 auto d = dom->proj(n, i);
380 if (auto pi = Pi::isa_cn(d)) return w.cn(conv_boundary(pi->dom()));
381 return conv_boundary(d);
382 });
383 return w.cn(w.sigma(doms));
384 }
385
386 // A local continuation carrying tensor types (a return continuation of a bufferized call, a join point,
387 // an error continuation): convert its domain the same way. This is context-independent, so the order in
388 // which references reach it does not matter.
389 if (!lam->is_external() && lam->is_set() && !op_args_.contains(lam))
390 if (auto pi = Pi::isa_cn(lam->type()); pi && mentions_tensor(pi->dom())) return w.cn(conv_boundary(pi->dom()));
391
392 return rewrite(lam->type())->as<Pi>();
393}
394
396 if (Axm::isa<tensor::buf>(app)) return rewrite(app->arg()); // consumed: the buffer *is* the tensor now
397 // A `tensor.if_static` still stuck at lowering time guards a runtime value: residualize to
398 // its dynamic branch.
399 if (Axm::isa<tensor::if_static>(app)) return rewrite(app->arg(3, 2));
400 if (Axm::isa<tensor::splat>(app)) return lower_splat(app);
401 if (Axm::isa<tensor::generate>(app)) return lower_generate(app);
402 if (Axm::isa<tensor::broadcast>(app)) return lower_broadcast(app);
403 if (Axm::isa<tensor::map_reduce_post>(app)) return lower_map_reduce(app);
404 if (Axm::isa<tensor::pad>(app)) return lower_pad(app);
405 if (Axm::isa<tensor::concat>(app)) return lower_concat(app);
406 if (Axm::isa<tensor::gather>(app)) return lower_gather(app);
407 if (Axm::isa<tensor::scatter>(app)) return lower_scatter(app);
408
409 // Call of a bufferized function: adapt the call site.
410 if (auto callee = app->callee()->isa_mut<Lam>(); callee && tensor_fns_.contains(callee))
411 return lower_call(app, callee);
412
413 // Call of a converted continuation (a local lam or a parameter var whose domain mentions a tensor):
414 // materialize value-world tensor arguments into buffers. Element-level lams (op_args_) — and their
415 // continuation PARAMETERS (e.g. a schedule nest's `cell`, whose «r; I32» loop vector may look like
416 // a recorded tensor type) — keep value ABI.
417 if (auto pi = Pi::isa_cn(app->callee()->type()); pi && mentions_tensor(pi->dom())) {
418 auto elementwise = [&](const Def* callee) {
419 auto lam = callee->isa_mut<Lam>();
420 if (!lam) lam = param_of(callee);
421 return lam && op_args_.contains(lam);
422 };
423 if (elementwise(app->callee())) return RWPhase::rewrite_imm_App(app);
424 auto& w = new_world();
425 return w.app(rewrite(app->callee()), materialize(pi->dom(), app->arg()));
426 }
427
428 return RWPhase::rewrite_imm_App(app);
429}
430
431const Def* LowerToMem::lower_call(const App* app, Lam* old_callee) {
432 auto& w = new_world();
433 auto new_callee = rewrite(old_callee);
434 auto dom = old_callee->type()->dom();
435 auto n = dom->num_projs();
436
437 DefVec args(n);
438 for (size_t i = 0; i != n; ++i) {
439 auto d = dom->proj(n, i);
440 auto a = app->arg()->proj(n, i);
441 // Continuations pass through: their domains are converted by `rewrite_mut_Lam` to exactly the
442 // domain the callee's new signature expects.
443 args[i] = Pi::isa_cn(d) ? rewrite(a) : materialize(d, a);
444 }
445 return w.app(new_callee, w.tuple(args));
446}
447
448const Def* LowerToMem::splat_buffer(const Def* arr_ty, const Def* scalar) {
449 // `buffer.lit` sets every element to `scalar`; `btensor.lower_map_reduce` fills it with a loop rather
450 // than storing a monolithic literal array (which the LLVM backend cannot digest for large shapes).
451 auto [bro, bso, boT] = Axm::isa<buffer::Buf>(buf_of(arr_ty))->args<3>();
452 auto [m, out] = buffer::op_lit(bro, bso, boT, bot_mem(), scalar)->projs<2>();
453 return out;
454}
455
456const Def* LowerToMem::materialize(const Def* old_ty, const Def* old_arg) {
457 auto& w = new_world();
458 if (tensor_ty_.contains(old_ty)) {
459 // A constant splat `‹s; c›` (e.g. a learning-rate or bias literal): a `buffer.init` would store the
460 // whole array as one giant LLVM constant. Emit `buffer.lit` (lowered to a fill loop) instead.
461 if (auto c = splat_scalar(old_arg)) return splat_buffer(old_ty, rewrite(c));
462 auto v = rewrite(old_arg);
463 if (Axm::isa<buffer::Buf>(v->type())) return v; // already a buffer
464 if (holds_buf(v)) {
465 const Def* culprit = old_arg;
466 if (old_arg->num_ops() != 0)
467 if (auto lam = param_of(old_arg->op(0))) culprit = lam;
468 culprit->blame("cannot bufferize: tensor parameter was split into parts; annotate it with `tensor.buf`")
469 .bail();
470 }
471 auto [br, bs, bT] = Axm::isa<buffer::Buf>(buf_of(old_ty))->args<3>();
472 auto [m, buf] = buffer::op_init(br, bs, bT, bot_mem(), v)->projs<2>();
473 return buf;
474 }
475 if (auto sig = old_ty->isa_imm<Sigma>(); sig && mentions_tensor(sig)) {
476 auto n = sig->num_ops();
477 DefVec ops(n);
478 for (size_t i = 0; i != n; ++i)
479 ops[i] = materialize(sig->op(i), old_arg->proj(n, i));
480 return w.tuple(ops);
481 }
482 return rewrite(old_arg);
483}
484
485const Def* LowerToMem::to_buffer(const Def* val, const Def* old) {
486 if (Axm::isa<buffer::Buf>(val->type())) return val;
487 return materialize(old->type(), old);
488}
489
490const Def* LowerToMem::buffer_list(const Def* list, const Def* old_list, const Def* n) {
491 auto n_l = Lit::isa<u64>(n);
492 if (!n_l) return list;
493
494 DefVec ins(*n_l, [&](size_t i) { return to_buffer(list->proj(*n_l, i), old_list->proj(*n_l, i)); });
495 for (auto in : ins)
496 if (!Axm::isa<buffer::Buf>(in->type())) return nullptr;
497 return new_world().tuple(ins);
498}
499
500std::pair<const Def*, const Def*> LowerToMem::peel_tensor(const Def* d) {
501 // One Extract carries every axis, so there is no chain to walk back up and no sub-tensor to mistake the
502 // base for: an index that stops short of the element yields an array type, which the caller rejects.
503 auto ex = d->isa<Extract>();
504 if (!ex || !tensor_ty_.contains(ex->tuple()->type())) return {nullptr, nullptr};
505 return {ex->tuple(), rewrite(ex->index())};
506}
507
509 // A partial read yields a sub-tensor, which has no buffer of its own; `collect_tensor_types` rejects it.
510 if (extract->type()->isa<Arr>()) return RWPhase::rewrite_imm_Extract(extract);
511
512 auto [old_arr, index] = peel_tensor(extract);
513 if (!old_arr) return RWPhase::rewrite_imm_Extract(extract);
514
515 // Only read through a tensor that *is* a buffer already - materializing one here would bufferize a plain
516 // value tuple whose type merely coincides with some tensor's.
517 auto arr = rewrite(old_arr);
518 if (!Axm::isa<buffer::Buf>(arr->type())) return RWPhase::rewrite_imm_Extract(extract);
519
520 return buffer::op_read(bot_mem(), arr, index)->proj(2, 1); // the loaded value
521}
522
524 if (!tensor_ty_.contains(insert->tuple()->type())) return RWPhase::rewrite_imm_Insert(insert);
525
526 auto arr = rewrite(insert->tuple()); // cf. rewrite_imm_Extract: no materialization here
527 auto buf = Axm::isa<buffer::Buf>(arr->type());
528 if (!buf) return RWPhase::rewrite_imm_Insert(insert);
529 auto [br, bs, bT] = buf->args<3>(); // actual (folded) buffer metadata
530 auto x = rewrite(insert->value());
531 auto idx = rewrite(insert->index()); // one write, every axis
532
533 // AlwaysAllocate policy: allocate a fresh buffer, copy the source in, then write the element.
534 // This local chain is properly threaded; AddMem splices its placeholder root into the global chain.
535 auto [m1, q] = buffer::op_alloc(br, bs, bT, fresh_mem())->projs<2>();
536 auto m2 = buffer::op_copy(m1, q, arr);
537 return buffer::op_write(m2, q, idx, x)->proj(2, 1);
538}
539
540const Def* LowerToMem::lower_splat(const App* app) {
541 auto value = app->arg()->proj(2, 1);
542
543 // MimIR folds rank-zero tensors and tensors whose literal dimensions are all
544 // one into their scalar element type. Such results have no tensor boundary to
545 // bufferize, so lower the splat directly to its scalar value.
546 if (!app->type()->isa<Arr>()) return rewrite(value);
547 return splat_buffer(app->type(), rewrite(value));
548}
549
550const Def* LowerToMem::lower_generate(const App* app) {
551 auto& w = new_world();
552 auto [meta, s_out] = rewrite(app->callee())->as<App>()->uncurry_args<2>();
553 auto [T, r] = meta->projs<2>();
554 auto r_l = Lit::isa<u64>(r);
555 if (!r_l) return RWPhase::rewrite_imm_App(app);
556 auto rn = *r_l;
557 auto body = rewrite(app->arg());
558
559 // A logical rank-zero tensor, or one consisting only of literal singleton
560 // axes, is represented by its scalar element in MimIR.
561 if (!app->type()->isa<Arr>()) {
562 DefVec zeros(rn, [&](size_t) { return w.lit_i64(0); });
563 return w.call(body, w.tuple(zeros));
564 }
565
566 auto out_ty = buf_of(app->type());
567 auto [br, bs, bT] = Axm::isa<buffer::Buf>(out_ty)->args<3>();
568 auto result_ty = w.sigma({w.call<mem::M>(0), out_ty});
569 auto compute = [&](Defs iters, const Def*, const Def* m) -> std::pair<const Def*, const Def*> {
570 return {m, w.call(body, iters)};
571 };
572 auto [am, dst] = buffer::op_alloc(br, bs, bT, fresh_mem())->projs<2>();
573 return btensor::build_pointwise(result_ty, am, dst, w.tuple(), s_out, rn, "generate", compute)->proj(2, 1);
574}
575
576const Def* LowerToMem::lower_broadcast(const App* app) {
577 // Thin bufferization: map the SSA `tensor.broadcast` onto the buffer-world `btensor.broadcast`.
578 // The loop generation lives in the btensor plugin (`btensor.lower_map_reduce`).
579 auto& w = new_world();
580 auto c = rewrite(app->callee())->as<App>();
581 auto arg = rewrite(app->arg());
582 auto [s_in, s_out, input] = arg->projs<3>();
583 auto [T, r] = c->args<2>();
584
585 // No-op broadcast (already normalized away in most cases).
586 if (s_in == s_out) return input;
587
588 input = to_buffer(input, app->arg()->proj(3, 2));
589 auto in_buf = Axm::isa<buffer::Buf>(input->type());
590
591 // Rank-0 source: an all-size-1 input shape folds to a plain scalar that is never recorded as a tensor
592 // type, so `materialize` leaves it as a value. Broadcasting a scalar fills every element with it.
593 if (!in_buf) return splat_buffer(app->type(), input);
594
595 // Actual (size-1-folded) input/output buffer shapes — `btensor.broadcast` is parameterised by them.
596 auto [bri, bsi, biT] = in_buf->args<3>();
597 auto [bro, bso, boT] = Axm::isa<buffer::Buf>(buf_of(app->type()))->args<3>();
598
599 // NB: no `w.call` here — `{ro, so}` (the *output* buffer shape) occur nowhere but in the result type, so
600 // there is no argument for inference to read them off.
601 auto op = w.annex<btensor::broadcast>();
602 op = w.app(op, w.tuple({T, bri, bsi, bro, bso, r}));
603 op = w.app(op, w.tuple({s_in, s_out}));
604 auto [am, dst] = buffer::op_alloc(bro, bso, boT, fresh_mem())->projs<2>();
605 auto [m, out] = w.app(op, w.tuple({am, dst, input}))->projs<2>();
606 return out;
607}
608
609const Def* LowerToMem::lower_map_reduce(const App* app) {
610 // Thin bufferization: map the SSA `tensor.map_reduce_post` onto the buffer-world `btensor.map_reduce_post`,
611 // reusing the (rewritten) meta. The loop generation lives in the btensor plugin (`btensor.lower_map_reduce`).
612 auto& w = new_world();
613 auto c = rewrite(app->callee())->as<App>();
614 auto args = rewrite(app->arg()); // (is, post_is): the (bufferized) input buffers
615 auto [is, post_is] = args->projs<2>();
616
617 auto mr = btensor::MapRedPost::of(c);
618
619 // The generic rewrite rebuilds the argument tuple with its stale value-array type even when its
620 // elements were converted to buffers, which would not be assignable to the op's
621 // `«nis; buffer.Buf …»` domain - hence the re-tupling in `buffer_list`.
622 auto [old_is, old_post_is] = app->arg()->projs<2>();
623 is = buffer_list(is, old_is, mr.nis);
624 post_is = buffer_list(post_is, old_post_is, mr.nps);
625 if (!is || !post_is) return RWPhase::rewrite_imm_App(app); // leave it alone
626
627 // Wrap the pure tensor combiner `Fn [To, «nis; Tis»] → To` into the mem-threaded combiner
628 // `Fn [mem.M 0, To, «nis; Tis»] → [mem.M 0, To]` that `btensor.map_reduce_post` expects.
629 auto mem_ty = w.call<mem::M>(0);
630 auto inner = mr.comb->type()->as<Pi>()->dom()->proj(2, 0); // [To, «nis; Tis»]
631 auto [cTo, ins_ty] = inner->projs<2>();
632 auto memcomb = w.mut_fun(w.sigma({mem_ty, cTo, ins_ty}), w.sigma({mem_ty, cTo}))->set("memComb");
633 auto [cdata, cret] = memcomb->vars<2>();
634 auto [cm, cacc, cins] = cdata->projs<3>();
635 auto after = w.mut_con(cTo)->set("afterComb");
636 after->app(true, cret, w.tuple({cm, after->var()}));
637 memcomb->set(true, w.app(mr.comb, w.tuple({w.tuple({cacc, cins}), after})));
638
639 // Likewise wrap the pure epilogue `Fn [To, «nps; Tps»] → Tp` into
640 // `Fn [mem.M 0, To, «nps; Tps»] → [mem.M 0, Tp]`.
641 auto pTp = mr.Tp;
642 auto pin = mr.post->type()->as<Pi>()->dom()->proj(2, 0); // [To, «nps; Tps»]
643 auto [pTo, pexts_ty] = pin->projs<2>();
644 auto mempost = w.mut_fun(w.sigma({mem_ty, pTo, pexts_ty}), w.sigma({mem_ty, pTp}))->set("memPost");
645 auto [pdata, pret] = mempost->vars<2>();
646 auto [pm, pacc, pexts] = pdata->projs<3>();
647 auto pafter = w.mut_con(pTp)->set("afterPost");
648 pafter->app(true, pret, w.tuple({pm, pafter->var()}));
649 mempost->set(true, w.app(mr.post, w.tuple({w.tuple({pacc, pexts}), pafter})));
650
651 // NB: no `w.call` here — the meta groups are *forwarded* from the tensor op on purpose. Leaving them to
652 // inference would re-derive `{Tis, Ris, Sis}` from the `buffer.Buf` operands, whose literal size-1 axes are
653 // already folded away, so they would no longer agree with the logical shapes the loop generation iterates.
654 auto op = w.annex<btensor::map_reduce_post>();
655 op = w.app(op, mr.nis_nps);
656 op = w.app(op, mr.meta);
657 op = w.app(op, mr.shapes);
658 op = w.app(op, mr.in_tys);
659 op = w.app(op, w.tuple({memcomb, mr.init, mempost}));
660 op = w.app(op, mr.map_out);
661 op = w.app(op, mr.maps_all);
662 auto [am, dst] = buffer::op_alloc(mr.Ro, mr.So, mr.Tp, fresh_mem())->projs<2>();
663 auto [m, out] = w.app(op, w.tuple({am, dst, is, post_is}))->projs<2>();
664 return scalar_result(app, m, out);
665}
666
667const Def* LowerToMem::lower_pad(const App* app) {
668 // Thin bufferization: map the SSA `tensor.pad` onto the buffer-world `btensor.pad`.
669 // The loop generation lives in the btensor plugin (`btensor.lower_map_reduce`).
670 auto& w = new_world();
671 auto c = rewrite(app->callee())->as<App>();
672 auto [input, value] = rewrite(app->arg())->projs<2>();
673
674 auto [Tr, s_in, params] = c->uncurry_args<3>();
675 auto [T, r] = Tr->projs<2>();
676 auto [mode, lo, hi] = params->projs<3>();
677
678 input = to_buffer(input, app->arg()->proj(2, 0));
679 if (!Axm::isa<buffer::Buf>(input->type()))
680 return RWPhase::rewrite_imm_App(app); // not a recorded tensor type: leave it alone
681
682 auto r_l = Lit::isa<u64>(r);
683 if (!r_l) return RWPhase::rewrite_imm_App(app);
684
685 // The LOGICAL output shape `s_out#d = lo#d + s_in#d + hi#d` — the loop generation iterates it, so it
686 // must keep size-1 axes (the result type's `Buf` folds them away and cannot be used here).
687 auto add = [&](const Def* a, const Def* b) { return w.call(core::nat::add, Defs{a, b}); };
688 DefVec so(*r_l, [&](size_t d) { return add(add(lo->proj(*r_l, d), s_in->proj(*r_l, d)), hi->proj(*r_l, d)); });
689 auto s_out = w.tuple(so);
690
691 // `{T, r}` is inferred: `s_in` pins `r`, and `input`'s `buffer.Buf (r, s_in, T)` pins `T`.
692 auto [am, dst] = buffer::op_alloc(r, s_out, T, fresh_mem())->projs<2>();
693 auto [m, out] = w.call<btensor::pad>(s_in, Defs{mode, lo, hi}, s_out, Defs{am, dst, input, value})->projs<2>();
694 return out;
695}
696
697const Def* LowerToMem::lower_concat(const App* app) {
698 // Thin bufferization: map the SSA `tensor.concat` onto the buffer-world `btensor.concat`.
699 // The loop generation lives in the btensor plugin (`btensor.lower_map_reduce`).
700 auto& w = new_world();
701 auto c = rewrite(app->callee())->as<App>();
702 auto arg = rewrite(app->arg());
703
704 auto [TnisR, ax, Sis] = c->uncurry_args<3>();
705 auto [T, nis, r] = TnisR->projs<3>();
706
707 auto nis_l = Lit::isa<u64>(nis);
708 auto r_l = Lit::isa<u64>(r);
709 auto ax_l = Lit::isa<u64>(ax);
710 if (!nis_l || !r_l || !ax_l) return RWPhase::rewrite_imm_App(app);
711
712 auto inputs = buffer_list(arg, app->arg(), nis);
713 if (!inputs) return RWPhase::rewrite_imm_App(app); // not a recorded tensor type: leave it alone
714
715 // The LOGICAL output shape: the summed extent along `ax` (literal, gated in `collect_tensor_types`),
716 // the shared extents elsewhere — the loop generation iterates it, so it must keep size-1 axes (the
717 // result type's `Buf` folds them away and cannot be used here).
718 u64 sum_ax = 0;
719 for (u64 i = 0; i < *nis_l; ++i) {
720 auto e = Lit::isa<u64>(Sis->proj(*nis_l, i)->proj(*r_l, *ax_l));
721 if (!e) return RWPhase::rewrite_imm_App(app);
722 sum_ax += *e;
723 }
724 DefVec so(*r_l, [&](size_t d) { return d == *ax_l ? w.lit_nat(sum_ax) : Sis->proj(*nis_l, 0)->proj(*r_l, d); });
725 auto s_out = w.tuple(so);
726
727 // NB: `{T, nis, r}` and `{Sis}` cannot be left to inference (hence no `w.call` here). `Sis` holds the
728 // *logical* per-input shapes, but the operands are `buffer.Buf` handles with their literal size-1 axes
729 // already folded away — unifying `buffer.Buf (r, Sis#i, T)` against a folded handle fails, because the
730 // left-hand side only folds once `Sis#i` is known. Passing the logical shapes makes both sides fold alike.
731 auto op = w.annex<btensor::concat>();
732 op = w.app(op, w.tuple({T, nis, r}));
733 op = w.app(op, ax);
734 op = w.app(op, Sis);
735 op = w.app(op, s_out);
736 auto [am, dst] = buffer::op_alloc(r, s_out, T, fresh_mem())->projs<2>();
737 auto [m, out] = w.app(op, w.tuple({am, dst, inputs}))->projs<2>();
738 return out;
739}
740
741const Def* LowerToMem::make_buffer(const Def* old_arg, const Def* r, const Def* shape) {
742 auto value = materialize(old_arg->type(), old_arg);
743 if (Axm::isa<buffer::Buf>(value->type())) return value;
744 return buffer::op_lit(r, shape, value->type(), bot_mem(), value)->proj(2, 1);
745}
746
747const Def* LowerToMem::lower_gather(const App* app) {
748 auto& w = new_world();
749 auto c = rewrite(app->callee())->as<App>();
750
751 auto [Tr, shapes, dim] = c->uncurry_args<3>();
752 auto [T, r] = Tr->projs<2>();
753 auto [s_src, s_idx] = shapes->projs<2>();
754 if (!check_gather_shape_constraints(app, r, dim, s_src, s_idx)) return RWPhase::rewrite_imm_App(app);
755 auto [old_input, old_index] = app->args<2>();
756 auto input = make_buffer(old_input, r, s_src);
757 auto index = make_buffer(old_index, r, s_idx);
758
759 auto op = w.app(w.annex<btensor::gather>(), {T, r});
760 auto [am, dst] = buffer::op_alloc(r, s_idx, T, fresh_mem())->projs<2>();
761 auto [out_mem, out_buf] = w.call(op, Defs{s_src, s_idx}, dim, Defs{am, dst, input, index})->projs<2>();
762 return scalar_result(app, out_mem, out_buf);
763}
764
765const Def* LowerToMem::lower_scatter(const App* app) {
766 auto& w = new_world();
767 auto c = rewrite(app->callee())->as<App>();
768
769 auto [Tr, shapes, dim] = c->uncurry_args<3>();
770 auto [T, r] = Tr->projs<2>();
771 auto [s_src, s_idx, s_updates] = shapes->projs<3>();
772 if (!check_scatter_shape_constraints(app, r, dim, s_src, s_idx, s_updates)) return RWPhase::rewrite_imm_App(app);
773 auto [old_input, old_index, old_updates] = app->args<3>();
774 auto input = make_buffer(old_input, r, s_src);
775 auto index = make_buffer(old_index, r, s_idx);
776 auto updates = make_buffer(old_updates, r, s_updates);
777
778 auto op = w.app(w.annex<btensor::scatter>(), {T, r});
779 auto [am, dst] = buffer::op_alloc(r, s_src, T, fresh_mem())->projs<2>();
780 auto [out_mem, out_buf]
781 = w.call(op, Defs{s_src, s_idx, s_updates}, dim, Defs{am, dst, input, index, updates})->projs<2>();
782 return scalar_result(app, out_mem, out_buf);
783}
784
785} // namespace mim::plug::tensor::phase
const Def * callee() const
Definition lam.h:237
const Def * arg() const
Definition lam.h:251
A (possibly paramterized) Array.
Definition tuple.h:305
static auto isa(const Def *def)
Definition axm.h:112
static std::tuple< const Axm *, u8, u8 > get(const Def *def)
Yields currying counter of def.
Definition axm.cpp:38
Base class for all Defs.
Definition def.h:289
bool is_set() const
Definition def.h:396
const Def * proj(nat_t a, nat_t i) const
Similar to World::extract while assuming an arity of a, but also works on Sigmas and Arrays.
Definition def.cpp:647
T * isa_mut() const
If this is mutable, it will cast constness away and perform a dynamic_cast to T.
Definition def.h:588
const Def * op(size_t i) const noexcept
Definition def.h:377
auto projs(Projector auto f) const
Splits this Def via Def::projections into an Array (if A == std::dynamic_extent) or std::array (other...
Definition def.h:457
const Def * type() const noexcept
Yields the "raw" type of this Def (maybe nullptr).
Definition def.h:1119
fe::Error & blame(fe::cite_string< Args... > s, Args &&... args) const
Reports an error that blames this; chain Error::n for Notes and Error::bail to throw.
Definition def.h:333
bool is_external() const noexcept
Definition def.h:569
nat_t num_projs() const
Yields Def::arity(), if it is a Lit, or 1 otherwise.
Definition def.h:1140
const T * isa_imm() const
Definition def.h:582
constexpr size_t num_ops() const noexcept
Definition def.h:378
Extracts from a Sigma or Array-typed Extract::tuple the element at position Extract::index.
Definition tuple.h:360
Creates a new Tuple / Pack by inserting Insert::value at position Insert::index into Insert::tuple.
Definition tuple.h:385
A function.
Definition lam.h:91
const Def * filter() const
Definition lam.h:103
Lam * set(Filter filter, const Def *body)
Definition lam.cpp:34
const Pi * type() const
Definition lam.h:112
const Def * body() const
Definition lam.h:104
static std::optional< T > isa(const Def *def)
Definition def.h:976
const fe::Vector< std::string > & args()
Command-line arguments passed to this Phase's plugin via -X <plugin>:<arg>.
Definition phase.cpp:23
A dependent function type.
Definition lam.h:14
static const Pi * isa_cn(const Def *d)
Definition lam.h:50
const Def * dom() const
Definition lam.h:38
World & new_world()
Create new Defs into this.
Definition phase.h:379
void start() override
Actual entry.
Definition phase.cpp:184
World & old_world()
Get old Defs from here.
Definition phase.h:378
const Def * rewrite_imm_App(const App *) override
Definition phase.cpp:223
virtual const Def * rewrite_imm_Extract(const Extract *)
virtual const Def * rewrite_imm_Insert(const Insert *)
const Def * map(const Def *old_def, const Def *new_def)
Definition rewrite.cpp:29
const Def * seal(Def *old_mut, Def *new_mut)
The tail of rewrite_stub: immutabilizes new_mut in hindsight, as rewriting may have made it vacuous.
Definition rewrite.cpp:280
virtual const Def * rewrite(const Def *)
Definition rewrite.cpp:62
auto enter(Def *new_mut)
Updates curr_mut() to new_mut and restores it at the end of the scope.
Definition rewrite.h:115
void push(Vars old_vars={}, Vars new_vars={})
Definition rewrite.h:40
constexpr Shape() noexcept=default
A dependent tuple type.
Definition tuple.h:39
const Def * sigma(Defs ops)
Definition world.cpp:506
const Def * tuple(Defs ops)
Definition world.cpp:516
Lam * mut_lam(const Pi *pi)
Definition world.h:415
const Def * rewrite_imm_Insert(const Insert *) override
const Def * rewrite_mut_Lam(Lam *) override
const Def * rewrite(const Def *) override
void start() override
Actual entry.
const Def * rewrite_imm_App(const App *) override
const Def * rewrite_imm_Extract(const Extract *) override
const Def * build_pointwise(const Def *result_ty, const Def *mem, const Def *dst, const Def *ins, const Def *s_out, u64 rn, const std::string &name, auto compute)
A [mem, dst, ins] → [mem, Buf] fun spliced via cps.cps2ds_dep that loops over s_out carrying [mem,...
Definition btensor.h:172
const Def * type_buf(const Def *r, const Def *s, const Def *T)
The buffer type buffer.Buf (r, s, T).
Definition buffer.h:11
const Def * op_write(const Def *r, const Def *s, const Def *T, const Def *mem, const Def *buf, const Def *idx, const Def *val)
buffer.write (r, s, T) (mem, buf, idx, val) ↦ [mem.M 0, buffer.Buf (r, s, T)].
Definition buffer.h:30
const Def * op_lit(const Def *r, const Def *s, const Def *T, const Def *mem, const Def *val)
buffer.lit (r, s, T) (mem, val) ↦ [mem.M 0, buffer.Buf (r, s, T)] (every element initialised to val).
Definition buffer.h:72
const Def * op_read(const Def *r, const Def *s, const Def *T, const Def *mem, const Def *buf, const Def *idx)
buffer.read (r, s, T) (mem, buf, idx) ↦ [mem.M 0, T].
Definition buffer.h:23
const Def * op_init(const Def *r, const Def *s, const Def *T, const Def *mem, const Def *val)
buffer.init (r, s, T) (mem, val) ↦ [mem.M 0, buffer.Buf (r, s, T)] (initialised with the array value ...
Definition buffer.h:66
const Def * op_alloc(const Def *r, const Def *s, const Def *T, const Def *mem)
buffer.alloc (r, s, T) mem ↦ [mem.M 0, buffer.Buf (r, s, T)].
Definition buffer.h:17
const Def * op_copy(const Def *r, const Def *s, const Def *T, const Def *mem, const Def *dst, const Def *src)
buffer.copy (r, s, T) (mem, dst, src) ↦ mem.M 0 (copies the whole buffer src into dst).
Definition buffer.h:36
Lam * mut_con(World &w, nat_t a=0)
Yields con[mem.M 0].
Definition mem.h:16
plug::core::M M
Definition autogen.h:14
bool check_scatter_shape_constraints(const App *app, const Def *rank, const Def *dim, const Def *src_shape, const Def *idx_shape, const Def *updates_shape)
Checks statically decidable scatter constraints, blaming app for a violation.
bool check_gather_shape_constraints(const App *app, const Def *rank, const Def *dim, const Def *src_shape, const Def *idx_shape)
Checks statically decidable gather constraints, blaming app for a violation.
fe::View< const Def * > Defs
Definition def.h:96
fe::Vector< const Def * > DefVec
Definition def.h:98
uint64_t u64
Definition types.h:27
@ Pi
Definition def.h:125
@ Lam
Definition def.h:125
@ Arr
Definition def.h:125
@ Extract
Definition def.h:125
@ App
Definition def.h:125
static MapRedPost of(const App *callee)
Definition btensor.h:25