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