MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
fuse.cpp
Go to the documentation of this file.
2
3#include <optional>
4
5#include <fe/bitset.h>
6
7#include <mim/def.h>
8#include <mim/lam.h>
9#include <mim/tuple.h>
10
11#include <mim/util/types.h>
12
15#include <mim/plug/core/core.h>
16#include <mim/plug/cps/cps.h>
17
19
21
22/// If `e` reads coordinate `var#i` injectively, returns `i`:
23/// a plain extract, possibly strided (`affine.semiop.mul` by a non-zero literal) and/or shifted
24/// (`affine.op.add`/`sub` with a loop-invariant `affine.lit` on the other side).
25/// Everything else — `mod`/`div`, sums of two loop indices (convolution windows), symbolic strides
26/// (which may be 0 at runtime) — yields nothing.
27static std::optional<u64> injective_coord(const Def* var, const Def* e) {
28 // A one-loop domain «1; affine.Index» collapses to a plain affine.Index, so the var itself is coordinate 0.
29 if (e == var) return 0;
30 if (auto ex = e->isa<Extract>(); ex && ex->tuple() == var) return Lit::isa<u64>(ex->index());
31
32 if (auto semiop = Axm::isa(affine::semiop::mul, e)) {
33 auto [x, c] = semiop->args<2>();
34 if (auto lit = Lit::isa<u64>(c); lit && *lit != 0) return injective_coord(var, x);
35 return {};
36 }
37
38 if (auto op = Axm::isa<affine::op>(e)) {
39 if (op.id() != affine::op::add && op.id() != affine::op::sub) return {};
40 auto [a, b] = op->args<2>();
41 bool a_const = static_cast<bool>(Axm::isa<affine::lit>(a));
42 bool b_const = static_cast<bool>(Axm::isa<affine::lit>(b));
43 if (a_const == b_const) return {};
44 return injective_coord(var, a_const ? b : a);
45 }
46
47 return {};
48}
49
50/// Checks that `map` provably reads through *every* loop index of its domain: its body is the
51/// identity, or each result coordinate is an injective read of one loop index (see `injective_coord`)
52/// and together they cover all indices.
53/// Such a map is injective over the iteration domain, so each element of the input behind it is read
54/// (and, after fusion, computed) *at most* once — strided/shifted reads skip elements entirely, so
55/// fusing behind them even drops computations the consumer never looks at.
56/// Anything not provably injective — a dropped loop index, a wrapped coordinate (`mod`, ...), a
57/// non-literal rank — is conservatively rejected.
58static bool reads_injectively(const Def* map) {
59 auto lam = map->isa_mut<Lam>();
60 if (!lam || !lam->is_set()) return false;
61 auto var = lam->var();
62 auto body = lam->body();
63 if (body == var) return true; // identity: every loop index passes through
64
65 auto n = Lit::isa<u64>(var->type()->arity());
66 if (!n) return false;
67
68 auto used = fe::Bitset();
69 auto mark = [&](const Def* elem) {
70 auto i = injective_coord(var, elem);
71 if (!i || *i >= *n) return false;
72 used.set(*i);
73 return true;
74 };
75
76 if (auto tuple = body->isa<Tuple>()) {
77 for (auto elem : tuple->ops())
78 if (!mark(elem)) return false;
79 } else if (auto pack = body->isa<Pack>()) {
80 // A pack repeats a single coordinate, so it can only cover a one-loop domain.
81 if (!pack->is_set() || !mark(pack->body())) return false;
82 } else if (!mark(body)) {
83 return false;
84 }
85
86 return used.count() == *n;
87}
88
89/// `inner ∘ outer`: feeds the outer op's read coordinates for one input into the inner op's access map.
90static const Def* compose_map(World& w, const Def* inner, const Def* outer) {
91 auto dom = outer->type()->as<Pi>()->dom();
92 auto codom = inner->type()->as<Pi>()->codom();
93 auto lam = w.mut_lam(dom, codom)->set("fused_map");
94 lam->set(true, w.app(inner, w.app(outer, lam->var())));
95 return lam;
96}
97
98/// The five parallel per-slot lists of a `map_reduce_post` input group: element type, rank, shape,
99/// access map, and the tensor itself.
100struct Slots {
102
103 void push(const Def* t, const Def* r, const Def* s, const Def* m, const Def* v) {
104 T.emplace_back(t), R.emplace_back(r), S.emplace_back(s), maps.emplace_back(m), is.emplace_back(v);
105 }
106};
107
108/// Does a slot that declares @p value in shape @p S read it in the shape it is produced in?
109/// Size-1 axes collapse out of the type, so they may differ - and then their maps do not compose.
110static bool reads_as_produced(const Def* value, const Def* S) {
111 if (auto mr = Axm::isa<tensor::map_reduce_post>(value)) {
112 auto shapes = mr->uncurry_args<8>()[2];
113 return shapes->proj(3, 0) == S;
114 }
115 if (auto bc = Axm::isa<tensor::broadcast>(value)) return bc->arg()->proj(3, 1) == S;
116 return true;
117}
118
119/// If `value` is a pure re-indexed read — a copy-combiner map_reduce (reshape/transpose/slice/
120/// flip/repeat lower to these) or a `tensor.broadcast` — returns its source and access map, to be
121/// composed behind the consuming slot's map. Such reads perform no computation, so reading through
122/// them needs neither an injectivity gate nor a consumer count.
123static std::optional<PureRead> read_through(World& w, const Def* value, const Def* slot_map, const Def* S) {
124 if (!reads_as_produced(value, S)) return {};
125 if (auto pr = is_pure_read(value)) {
126 // A source whose axes are all size 1 type-collapses to a plain scalar («1; T» ≡ T): the
127 // fused op would carry an input the bufferization cannot represent - keep the copy instead.
128 if (!pr->src->type()->isa<Arr>()) return {};
129 return pr;
130 }
131
132 if (auto bc = Axm::isa<tensor::broadcast>(value)) {
133 auto [T, r] = bc->callee()->as<App>()->args<2>();
134 auto [s_in, s_out, input] = bc->arg()->projs<3>();
135 auto r_l = Lit::isa<u64>(r);
136 if (!r_l) return {};
137 // See above: an all-size-1 source is a type-collapsed scalar - not readable through.
138 if (!input->type()->isa<Arr>()) return {};
139
140 // Source axis d reads o#d where the sizes agree and index 0 where the source axis is 1
141 // (expressed as `o#d · 0`, like `bid_map`). Bail on axes where neither is provable.
142 auto vec_ty = slot_map->type()->as<Pi>()->codom(); // «r; affine.Index»
143 auto lam = w.mut_lam(vec_ty, vec_ty)->set("bcast_map");
144 DefVec elems(*r_l);
145 for (u64 d = 0; d < *r_l; ++d) {
146 auto in_d = s_in->proj(*r_l, d);
147 auto o_d = lam->var(*r_l, d);
148 if (in_d == s_out->proj(*r_l, d))
149 elems[d] = o_d;
150 else if (auto l = Lit::isa<u64>(in_d); l && *l == 1)
151 elems[d] = w.call(affine::semiop::mul, Defs{o_d, w.lit_nat(0)});
152 else
153 return {};
154 }
155 lam->set(true, w.tuple(elems));
156
157 return PureRead{input, lam, T, r, s_in};
158 }
159
160 return {};
161}
162
163// Fuses an outer `tensor.map_reduce` with any number of its inputs — and, recursively, any
164// fusible inputs of those inputs — whenever each such input is itself a `tensor.map_reduce`
165// without reduction loops (`Rr = 0`) that writes its full loop domain through the identity output
166// map (`Sr = So`, `map_out = affine.id`), *and* the outer reads that input injectively (its
167// access map uses every loop index, see `reads_injectively`). Reading such an inner tensor at a
168// position is then just a single call to the inner combination function, with each inner access
169// map composed behind the outer's access map for that input; injectivity guarantees that this
170// inlining performs each inner computation at most once, so fusion never duplicates work.
171//
172// Outer: map_reduce nis_o (To, Ro, Rr) (So, Sr) (Tis_o, Ris_o, Sis_o) (f_o, init_o) map_out maps_o is_o
173// Inner: map_reduce nis_k (To_k, Ro_k, 0) (So_k, So_k) (Tis_k, Ris_k, Sis_k) (f_k, init_k) id maps_k is_k
174// for every fusible input — possibly nested inside another fusible input
175//
176// Result: map_reduce nis_new (To, Ro, Rr) (So, Sr) (Tis_new, Ris_new, Sis_new) (f_new, init_o)
177// map_out maps_new is_new
178//
179// The collection phase walks the tree of fusible inner ops below `app` once, producing a flat list
180// of *leaves* (the surviving tensor inputs of the fused op) and *inner nodes* (the inner combiners
181// that must run before `f_o`). Each fusible input is replaced by its inner's inputs, with access
182// maps composed behind the outer map at that position; the composition nests across levels. The
183// new combination function `f_new` invokes every inner combiner in post-order — each starting from
184// its own init — and finally invokes `f_o`, threading inner results into the corresponding outer
185// input slots.
186const Def* Fuse::fuse_map_reduce(const App* app) {
187 auto mr = btensor::MapRedPost::of(rewrite(app->callee())->as<App>());
188 auto [is, post_is] = rewrite(app->arg())->projs<2>();
189
190 log().d("consider for fusion: comb = {}, init = {}, To = {}, Ro = {}", mr.comb, mr.init, mr.To, mr.Ro);
191 log().d(" inputs: nis = {}, Tis = {}, Ris = {}, Sis = {}, is = {}", mr.nis, mr.Tis, mr.Ris, mr.Sis, is);
192
193 auto& w = new_world();
194
195 auto nis_lit = Lit::isa<u64>(mr.nis);
196 if (!nis_lit) return nullptr;
197 auto nis_nat = *nis_lit;
198
199 struct InnerInfo {
200 const Def* comb;
201 const Def* init;
202 const Def* Tis;
203 const Def* Ris;
204 const Def* Sis;
205 const Def* To;
206 const Def* maps;
207 u64 nis;
208 const Def* is;
209 };
210
211 fe::Vector<std::optional<InnerInfo>> infos(nis_nat);
212
213 for (u64 k = 0; k < nis_nat; ++k) {
214 auto inner = Axm::isa<tensor::map_reduce_post>(is->proj(nis_nat, k));
215 if (!inner) continue;
216
217 auto im = btensor::MapRedPost::of(inner->callee()->as<App>());
218 auto inner_is = inner->arg()->proj(2, 0);
219
220 auto inner_nis_nat = Lit::isa<u64>(im.nis);
221 if (!inner_nis_nat || im.So != mr.Sis->proj(nis_nat, k)) continue; // see reads_as_produced
222
223 // Epilogue inputs on the inner would have to be re-read inside the fused combiner; only
224 // epilogue-free inners fuse (their identity post is checked below).
225 auto inner_nps_nat = Lit::isa<u64>(im.nps);
226 if (!inner_nps_nat || *inner_nps_nat != 0) continue;
227
228 // We can only fuse when the inner has no reduction loops and writes every cell of its full
229 // loop domain through the identity output map. In that case the inner tensor at any
230 // position is just a single call of `inner_comb` at that position.
231 // The identity map (`affine.id`) is recognized structurally (a lam returning its own var),
232 // since the rewrite into this phase's world rebuilds mutables and breaks pointer equality.
233 auto inner_ro = Lit::isa<u64>(im.Ro);
234 auto inner_rn = Lit::isa<u64>(im.Rn);
235 if (!inner_ro || !inner_rn || *inner_ro != *inner_rn) continue;
236 if (im.Sr != im.So) continue;
237 if (!is_identity_map(im.map_out)) continue;
238
239 // A non-identity inner epilogue cannot be dropped when the inner combiner is inlined;
240 // threading it through the fused combiner chain is future work.
241 if (!is_identity_post(im.post)) continue;
242
243 // Fusing inlines the inner combiner once per iteration of the outer's *full* loop nest.
244 // Unless the outer reads input k injectively, the same inner element is recomputed once
245 // for every iteration of each loop its access map ignores — e.g. a matrix product reads
246 // its first input at `(i, k)`, so a fused producer would be recomputed for every `j`.
247 // In that case keep the producer materialized instead.
248 if (!reads_injectively(mr.maps->proj(nis_nat, k))) continue;
249
250 infos[k] = InnerInfo{.comb = im.comb,
251 .init = im.init,
252 .Tis = im.Tis,
253 .Ris = im.Ris,
254 .Sis = im.Sis,
255 .To = im.To,
256 .maps = im.maps,
257 .nis = *inner_nis_nat,
258 .is = inner_is};
259 }
260
261 if (std::ranges::none_of(infos, [](const auto& info) { return info.has_value(); })) return nullptr;
262
263 // Each fusible outer input k is replaced by `infos[k].nis` slots in the fused input list;
264 // every non-fusible input retains exactly one slot. `new_pos[i]` is the start of input i's
265 // slot range in the fused list.
266 fe::Vector<u64> new_pos(nis_nat);
267 u64 new_nis_nat = 0;
268 for (u64 i = 0; i < nis_nat; ++i) {
269 new_pos[i] = new_nis_nat;
270 new_nis_nat += infos[i] ? infos[i]->nis : 1;
271 }
272
273 auto slots = Slots();
274 for (u64 i = 0; i < nis_nat; ++i) {
275 if (auto& info = infos[i]) {
276 auto outer_map_i = mr.maps->proj(nis_nat, i);
277 // Inlining a shared inner forwards its consumers to its inputs: they are no longer
278 // sole-consumed for the epilogue guard.
279 bool shared = !sole_consumer(is->proj(nis_nat, i));
280 for (u64 l = 0; l < info->nis; ++l) {
281 auto v = info->is->proj(info->nis, l);
282 if (shared) shared_.insert(v);
283 // The inner reads at its own output coordinates; those are the outer's read
284 // coordinates for input i, so the fused access map is the composition.
285 slots.push(info->Tis->proj(info->nis, l), info->Ris->proj(info->nis, l), info->Sis->proj(info->nis, l),
286 compose_map(w, info->maps->proj(info->nis, l), outer_map_i), v);
287 }
288 } else {
289 slots.push(mr.Tis->proj(nis_nat, i), mr.Ris->proj(nis_nat, i), mr.Sis->proj(nis_nat, i),
290 mr.maps->proj(nis_nat, i), is->proj(nis_nat, i));
291 }
292 }
293
294 auto new_Tis = w.tuple(slots.T);
295 auto new_Ris = w.tuple(slots.R);
296 auto new_Sis = w.tuple(slots.S);
297 auto new_maps = w.tuple(slots.maps);
298 auto new_is = w.tuple(slots.is);
299
300 auto new_nis_def = w.lit_nat(new_nis_nat);
301
302 // Build the fused combination function:
303 //
304 // cn f_new(data: [To, [new_Tis ...]], ret: cn To) =
305 // cn inner_ret_<r>(value_<r>: inner_To_<r>) = ...
306 // f_<fused[0]>((init_<fused[0]>, inner_inputs_<fused[0]>), inner_ret_0)
307 //
308 // inner_ret_<r>(value_<r>):
309 // if r is not the last fused input:
310 // f_<fused[r+1]>((init_<fused[r+1]>, inner_inputs_<fused[r+1]>), inner_ret_<r+1>)
311 // else:
312 // f_o((acc, outer_inputs), ret)
313 //
314 // `outer_inputs[i]` is `value_<r>` when input i is the r-th fused input, and the
315 // corresponding `new_in` slot otherwise. Each `inner_ret_<r>` closes over the prior
316 // `value_<j>`s as free variables — those are bound by the dynamic call chain.
317 auto inputs_sigma = w.sigma(slots.T);
318 auto data_sigma = w.sigma({mr.To, inputs_sigma});
319 auto ret_cn_type = w.cn(mr.To);
320 auto new_comb = w.mut_con({data_sigma, ret_cn_type})->set("fused_comb");
321 auto [new_data, new_ret] = new_comb->vars<2>();
322 auto [new_acc, new_in] = new_data->projs<2>();
323
324 fe::Vector<u64> fused_indices;
325 for (u64 i = 0; i < nis_nat; ++i)
326 if (infos[i]) fused_indices.emplace_back(i);
327
328 fe::Vector<Lam*> inner_rets(fused_indices.size(),
329 [&](size_t r) { return w.mut_con(infos[fused_indices[r]]->To)->set("inner_ret"); });
330
331 // Map each outer input position to its value at the f_o call site.
332 DefVec outer_inputs_vec(nis_nat);
333 for (u64 i = 0, r = 0; i < nis_nat; ++i)
334 outer_inputs_vec[i] = infos[i] ? inner_rets[r++]->var() : new_in->proj(new_nis_nat, new_pos[i]);
335
336 // Chain: caller for fused step r is new_comb (r==0) or inner_rets[r-1] (otherwise).
337 for (size_t r = 0; r < fused_indices.size(); ++r) {
338 const auto& info = *infos[fused_indices[r]];
339 DefVec inner_inputs_vec(info.nis,
340 [&](size_t l) { return new_in->proj(new_nis_nat, new_pos[fused_indices[r]] + l); });
341 Lam* caller = r == 0 ? new_comb : inner_rets[r - 1];
342 caller->app(true, info.comb, {w.tuple({info.init, w.tuple(inner_inputs_vec)}), inner_rets[r]});
343 }
344
345 // After every inner combiner has produced its value, call the outer combiner.
346 inner_rets.back()->app(true, mr.comb, {w.tuple({new_acc, w.tuple(outer_inputs_vec)}), new_ret});
347
348 // Construct the fused map_reduce; the loop domain, output map, init and epilogue (including the
349 // epilogue inputs) are the outer's.
350 auto res = w.annex<tensor::map_reduce_post>();
351 res = w.app(res, {new_nis_def, mr.nps});
352 res = w.app(res, mr.meta);
353 res = w.app(res, mr.shapes);
354 res = w.app(res, {new_Tis, new_Ris, new_Sis, mr.Tps, mr.Rps, mr.Sps});
355 res = w.app(res, {new_comb, mr.init, mr.post});
356 res = w.app(res, mr.map_out);
357 res = w.app(res, {new_maps, mr.post_maps});
358 res = w.app(res, {new_is, post_is});
359
360 return res;
361}
362
363/// Is `map` the row-major reshape read `tensor.reshape_map (s_in, s_out)` — the map that reads a
364/// PACKED producer (its output strip-mined to `s_in`) at the unpacked coordinates `s_out`?
365/// Decided by normalization: both `map` and the canonical unpack map are applied to the same probe
366/// variable; the reduced bodies are hash-consed, so pointer equality decides alpha-equivalence.
367static bool
368is_unpack_read(World& w, const Def* map, const Def* r_in, const Def* s_in, const Def* r_out, const Def* s_out) {
369 auto pi = map->type()->isa<Pi>();
370 if (!pi) return false;
371 auto expected = w.app(w.app(w.annex<tensor::reshape_map>(), {r_in, r_out}), {s_in, s_out});
372 auto epi = expected->type()->isa<Pi>();
373 if (!epi || epi->dom() != pi->dom() || epi->codom() != pi->codom()) return false;
374 auto probe = w.mut_lam(pi->dom(), pi->codom()); // scratch binder: its var stands in for the cell vector
375 return w.app(map, probe->var()) == w.app(expected, probe->var());
376}
377
378// Is `d` (a new-world map_reduce) consumed by exactly one node? Resolved via the old-world app it
379// replaces; `new2old_` covers every map_reduce the phase has rewritten. Defs in `shared_` gained
380// consumers by a read-through of a shared read, which the old-world count cannot see.
381bool Fuse::sole_consumer(const Def* d) const {
382 if (shared_.contains(d)) return false;
383 auto old_it = new2old_.find(d);
384 if (old_it == new2old_.end()) return false;
385 auto cnt = mr_consumers_.find(old_it->second);
386 return cnt != mr_consumers_.end() && cnt->second == 1;
387}
388
389// Fuses a trailing elementwise map into the reducing `tensor.map_reduce` producing one of its
390// inputs — the reverse of `fuse_map_reduce` and the direction the producer gate deliberately
391// rejects for `Rr > 0`: a reduction must not be inlined into a consumer's combiner (it would rerun
392// per fold step), but it *can* absorb the consumer into its per-output-cell `post` epilogue (the
393// GEMM-epilogue pattern, e.g. `relu(add(conv, bias))`).
394//
395// The producer input `k0` must be read elementwise over the map's whole domain (identity access
396// map, `Sis#k0 = So`, with the map's `Rr = 0`, `Sr = So` and identity `map_out`) — anything else
397// changes coordinates or drops cells and would need map inversion. All *other* inputs of the map —
398// and the map's own epilogue inputs — become epilogue inputs of the producer, read once per output
399// cell at their (arbitrary) access maps: those maps take the map's loop vector, which under the
400// identity-read gate is exactly the producer's output-cell coordinate system.
401//
402// Result: the producer with
403// post := (x, extras) ↦ post_o(f_o(init_o, extras[k0 ↦ post_i(x, extras_i)]), extras_o)
404// post_is := inner post_is ++ outer is \ k0 ++ outer post_is (maps concatenated likewise)
405//
406// The producer must be consumed by this map alone (checked via its old-world consumer count):
407// otherwise it stays materialized for the other consumers and additionally runs fused —
408// duplicating the whole reduction loop nest.
409//
410// `callee`/`arg` are new-world (already rewritten): the caller iterates this on freshly fused apps.
411const Def* Fuse::fuse_epilogue(const App* callee, const Def* arg) {
412 auto mr = btensor::MapRedPost::of(callee);
413
414 auto& w = new_world();
415
416 auto nis_lit = Lit::isa<u64>(mr.nis);
417 auto nps_lit = Lit::isa<u64>(mr.nps);
418 auto ro_lit = Lit::isa<u64>(mr.Ro);
419 auto rn_lit = Lit::isa<u64>(mr.Rn);
420 if (!nis_lit || !nps_lit || !ro_lit || !rn_lit || *ro_lit != *rn_lit) return nullptr;
421 auto nis_nat = *nis_lit;
422 auto nps_nat = *nps_lit;
423 if (nis_nat == 0) return nullptr;
424 if (mr.Sr != mr.So) return nullptr;
425
426 if (!is_identity_map(mr.map_out)) return nullptr;
427
428 // A consumer that computes nothing (a pure re-indexed read) is read-through's job — sinking it
429 // would re-wrap the producer forever (the unpack path below emits exactly such a copy on top).
430 if (nis_nat == 1 && nps_nat == 0 && is_copy_comb(mr.comb) && is_identity_post(mr.post)) return nullptr;
431
432 auto [is, ps] = arg->projs<2>();
433
434 // Find the producer: the first input that is a map_reduce read elementwise over the whole
435 // domain — at identity coordinates (input shape == So), or through the row-major reshape that
436 // unpacks a strip-mined (packed) producer — and consumed by this map alone.
437 u64 k0 = 0;
438 const Def* inner_def = nullptr;
439 bool unpack = false;
440 for (u64 k = 0; k < nis_nat && !inner_def; ++k) {
441 auto cand = Axm::isa<tensor::map_reduce_post>(is->proj(nis_nat, k));
442 if (!cand || !sole_consumer(cand) || !reads_as_produced(cand, mr.Sis->proj(nis_nat, k))) continue;
443
444 auto m = mr.maps->proj(nis_nat, k);
445 if (is_identity_map(m) && mr.Sis->proj(nis_nat, k) == mr.So) {
446 k0 = k;
447 inner_def = cand;
448 } else if (mr.Sis->proj(nis_nat, k) != mr.So
449 && is_unpack_read(w, m, mr.Ris->proj(nis_nat, k), mr.Sis->proj(nis_nat, k), mr.Ro, mr.So)) {
450 k0 = k;
451 inner_def = cand;
452 unpack = true;
453 }
454 }
455 if (!inner_def) return nullptr;
456
457 // Reading through the unpack: the fused epilogue runs at the producer's PACKED cell
458 // coordinates, so every map carried over from this consumer — its other combiner inputs and
459 // its own epilogue inputs, all taking unpacked cell coordinates — is composed behind the
460 // inverse reshape (packed cell → unpacked cell).
461 const Def* to_cells = nullptr;
462 if (unpack)
463 to_cells = w.app(w.app(w.annex<tensor::reshape_map>(), {mr.Ro, mr.Ris->proj(nis_nat, k0)}),
464 {mr.So, mr.Sis->proj(nis_nat, k0)});
465 auto inner = Axm::isa<tensor::map_reduce_post>(inner_def);
466
467 auto im = btensor::MapRedPost::of(inner->callee()->as<App>());
468 auto [i_is, i_ps] = inner->arg()->projs<2>();
469
470 auto i_nps_lit = Lit::isa<u64>(im.nps);
471 if (!i_nps_lit) return nullptr;
472 auto i_nps_nat = *i_nps_lit;
473
474 log().d("fuse trailing map {} {} into the epilogue of {}", callee, arg, inner_def);
475
476 // Concatenated epilogue inputs: the inner's own, then the map's other combiner inputs, then the
477 // map's epilogue inputs. The maps' access maps carry over verbatim — their domain (the map's
478 // loop vector) is the producer's output-cell coordinate system.
479 auto new_nps = i_nps_nat + (nis_nat - 1) + nps_nat;
480 auto to_cell = [&](const Def* m) { return to_cells ? compose_map(w, m, to_cells) : m; };
481 auto eps = Slots();
482 for (u64 j = 0; j < i_nps_nat; ++j)
483 eps.push(im.Tps->proj(i_nps_nat, j), im.Rps->proj(i_nps_nat, j), im.Sps->proj(i_nps_nat, j),
484 im.post_maps->proj(i_nps_nat, j), i_ps->proj(i_nps_nat, j));
485 for (u64 i = 0; i < nis_nat; ++i)
486 if (i != k0)
487 eps.push(mr.Tis->proj(nis_nat, i), mr.Ris->proj(nis_nat, i), mr.Sis->proj(nis_nat, i),
488 to_cell(mr.maps->proj(nis_nat, i)), is->proj(nis_nat, i));
489 for (u64 j = 0; j < nps_nat; ++j)
490 eps.push(mr.Tps->proj(nps_nat, j), mr.Rps->proj(nps_nat, j), mr.Sps->proj(nps_nat, j),
491 to_cell(mr.post_maps->proj(nps_nat, j)), ps->proj(nps_nat, j));
492
493 // The composed epilogue as a CPS chain: inner post → map combiner → map post. Identity hops are
494 // called through anyway — their always-true filters inline them.
495 auto fused_post = w.mut_con({w.sigma({im.To, w.sigma(eps.T)}), w.cn(mr.Tp)})->set("fused_post");
496 auto after_ip = w.mut_con(im.Tp)->set("afterInnerPost");
497 auto after_comb = w.mut_con(mr.To)->set("afterComb");
498 auto [fused_data, fused_ret] = fused_post->vars<2>();
499 auto [x, extras] = fused_data->projs<2>();
500
501 DefVec i_extras(i_nps_nat, [&](size_t j) { return extras->proj(new_nps, j); });
502 fused_post->app(true, im.post, {w.tuple({x, w.tuple(i_extras)}), after_ip});
503
504 DefVec comb_inputs(nis_nat);
505 for (u64 i = 0, r = 0; i < nis_nat; ++i)
506 comb_inputs[i] = i == k0 ? after_ip->var() : extras->proj(new_nps, i_nps_nat + r++);
507 after_ip->app(true, mr.comb, {w.tuple({mr.init, w.tuple(comb_inputs)}), after_comb});
508
509 DefVec o_extras(nps_nat, [&](size_t j) { return extras->proj(new_nps, i_nps_nat + (nis_nat - 1) + j); });
510 after_comb->app(true, mr.post, {w.tuple({after_comb->var(), w.tuple(o_extras)}), fused_ret});
511
512 // The producer, keeping its loop nest untouched; only the epilogue (inputs) and the out-element
513 // type change.
514 auto res = w.annex<tensor::map_reduce_post>();
515 res = w.app(res, {im.nis, w.lit_nat(new_nps)});
516 res = w.app(res, {im.To, mr.Tp, im.Ro, im.Rn, im.TSched});
517 res = w.app(res, im.shapes);
518 res = w.app(res, {im.Tis, im.Ris, im.Sis, w.tuple(eps.T), w.tuple(eps.R), w.tuple(eps.S)});
519 res = w.app(res, {im.comb, im.init, fused_post});
520 res = w.app(res, im.map_out);
521 res = w.app(res, {im.maps, w.tuple(eps.maps)});
522 res = w.app(res, {i_is, w.tuple(eps.is)});
523
524 if (unpack) {
525 // The fused op still materializes PACKED; re-wrap it in the unpacking reshape — expanded
526 // via the impl so the wrapper is again a pure copy-mr that consumers read through (or a
527 // plain copy at a graph boundary). The copy-consumer guard above keeps this wrapper from
528 // being sunk right back.
529 auto impl = w.annex<tensor::reshape_impl>();
530 impl = w.app(impl, {mr.Tp, mr.Ris->proj(nis_nat, k0), mr.Ro});
531 impl = w.app(impl, mr.Sis->proj(nis_nat, k0));
532 impl = w.app(impl, mr.So);
533 res = w.app(impl, res);
534 }
535
536 return res;
537}
538
539// Rewires every input slot — combiner and epilogue alike — that is a pure re-indexed read
540// (a copy-combiner map_reduce or a `tensor.broadcast`, see `read_through`) to read the underlying
541// source directly, with the read's access map composed behind the slot's map. This absorbs
542// reshape/transpose/slice/flip/repeat/broadcast chains into the access maps of the consuming
543// map_reduce, so they never materialize; the bypassed op dies with cleanup unless someone else
544// still reads it (in which case the rewiring is still free — it removes no sharing, only a copy).
545const Def* Fuse::fuse_read_through(const App* callee, const Def* arg) {
546 auto mr = btensor::MapRedPost::of(callee);
547
548 auto nis_lit = Lit::isa<u64>(mr.nis);
549 auto nps_lit = Lit::isa<u64>(mr.nps);
550 if (!nis_lit || !nps_lit) return nullptr;
551 auto nis_nat = *nis_lit;
552 auto nps_nat = *nps_lit;
553
554 auto& w = new_world();
555
556 auto [is, ps] = arg->projs<2>();
557
558 bool changed = false;
559 auto rewire = [&](u64 n, const Def* Ts, const Def* Rs, const Def* Ss, const Def* ms, const Def* vs) {
560 auto slots = Slots();
561 for (u64 i = 0; i < n; ++i) {
562 auto T = Ts->proj(n, i), R = Rs->proj(n, i), S = Ss->proj(n, i);
563 auto m = ms->proj(n, i), v = vs->proj(n, i);
564 if (auto rt = read_through(w, v, m, S)) {
565 log().d("read input {} of {} through {}", i, callee, v);
566 // The bypassed read forwards its consumers to the source: a shared (or uncounted,
567 // e.g. broadcast) read leaves the source multiply-consumed for the epilogue guard.
568 if (!sole_consumer(v)) shared_.insert(rt->src);
569 T = rt->T, R = rt->R, S = rt->S, m = compose_map(w, rt->map, m), v = rt->src;
570 changed = true;
571 }
572 slots.push(T, R, S, m, v);
573 }
574 return slots;
575 };
576
577 auto ins = rewire(nis_nat, mr.Tis, mr.Ris, mr.Sis, mr.maps, is);
578 auto eps = rewire(nps_nat, mr.Tps, mr.Rps, mr.Sps, mr.post_maps, ps);
579 if (!changed) return nullptr;
580
581 auto res = w.annex<tensor::map_reduce_post>();
582 res = w.app(res, mr.nis_nps);
583 res = w.app(res, mr.meta);
584 res = w.app(res, mr.shapes);
585 res = w.app(res, {w.tuple(ins.T), w.tuple(ins.R), w.tuple(ins.S), w.tuple(eps.T), w.tuple(eps.R), w.tuple(eps.S)});
586 res = w.app(res, mr.comb_init);
587 res = w.app(res, mr.map_out);
588 res = w.app(res, {w.tuple(ins.maps), w.tuple(eps.maps)});
589 res = w.app(res, {w.tuple(ins.is), w.tuple(eps.is)});
590
591 return res;
592}
593
594namespace {
595
596bool is_mr(const Def* d) { return static_cast<bool>(Axm::isa<tensor::map_reduce_post>(d)); }
597
598} // namespace
599
601 // The epilogue direction needs to know whether a map_reduce is consumed by exactly one other node.
602 mr_consumers_ = count_consumers(old_world(), is_mr);
604}
605
606const Def* Fuse::rewrite_imm_App(const App* app) {
608 const App* cur = nullptr;
609 if (auto res = fuse_map_reduce(app)) {
610 log().d("fused map_reduce {} → {}", app, res);
611 cur = res->as<App>();
612 }
613 // Read-throughs and the epilogue direction, to a fixpoint: each round may absorb pure
614 // re-indexed reads into access maps or sink the current map — first the plainly rewritten
615 // one, then the producer-fused result, then each fused result — into the producer of one
616 // of its inputs. Terminates quickly: rewiring walks strictly down the producer DAG, and a
617 // fused reduction fails the Rr = 0 outer gate on the next round.
618 auto callee = cur ? cur->callee()->as<App>() : rewrite(app->callee())->as<App>();
619 auto arg = cur ? cur->arg() : rewrite(app->arg());
620 for (bool progress = true; progress;) {
621 progress = false;
622 const Def* res = fuse_read_through(callee, arg);
623 if (!res) {
624 res = fuse_epilogue(callee, arg);
625 if (res) log().d("fused trailing map {} into its producer's epilogue → {}", app, res);
626 }
627 if (res) {
628 cur = res->as<App>();
629 callee = cur->callee()->as<App>();
630 arg = cur->arg();
631 progress = true;
632 }
633 }
634 auto result = cur ? cur : RWPhase::rewrite_imm_App(app);
635 // Remember which old app this (possibly fused) map_reduce replaces, so later epilogue
636 // rounds of its consumers can look up its consumer count.
637 if (is_mr(result)) {
638 new2old_[result] = app;
639 // An epilogue sink into a PACKED producer wraps the fused op in an unpack copy (see
640 // fuse_epilogue). Register the wrapped producer under the same old app — its sole
641 // consumer is the wrapper, which stands for this app — so the NEXT trailing map (which
642 // producer-fuses the wrapper away and reads the packed op directly) can sink too.
643 if (auto pr = is_pure_read(result); pr && is_mr(pr->src) && !new2old_.contains(pr->src))
644 new2old_[pr->src] = app;
645 }
646 return result;
647 }
648 return RWPhase::rewrite_imm_App(app);
649}
650
651} // 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
Base class for all Defs.
Definition def.h:289
const Def * var(nat_t a, nat_t i) noexcept
Definition def.h:492
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
Extracts from a Sigma or Array-typed Extract::tuple the element at position Extract::index.
Definition tuple.h:360
const Def * tuple() const
Definition tuple.h:370
A function.
Definition lam.h:91
static std::optional< T > isa(const Def *def)
Definition def.h:976
A (possibly paramterized) Tuple.
Definition tuple.h:332
const fe::Log & log() const
Definition phase.h:60
A dependent function type.
Definition lam.h:14
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(const Def *)
Definition rewrite.cpp:62
Data constructor for a Sigma.
Definition tuple.h:84
The World represents the whole program and manages creation of MimIR nodes (Defs).
Definition world.h:43
void start() override
Actual entry.
Definition fuse.cpp:600
const Def * rewrite_imm_App(const App *) final
Definition fuse.cpp:606
static std::optional< PureRead > read_through(World &w, const Def *value, const Def *slot_map, const Def *S)
If value is a pure re-indexed read — a copy-combiner map_reduce (reshape/transpose/slice/ flip/repeat...
Definition fuse.cpp:123
static const Def * compose_map(World &w, const Def *inner, const Def *outer)
inner ∘ outer: feeds the outer op's read coordinates for one input into the inner op's access map.
Definition fuse.cpp:90
static bool is_unpack_read(World &w, const Def *map, const Def *r_in, const Def *s_in, const Def *r_out, const Def *s_out)
Is map the row-major reshape read tensor.reshape_map (s_in, s_out) — the map that reads a PACKED prod...
Definition fuse.cpp:368
static bool reads_as_produced(const Def *value, const Def *S)
Does a slot that declares value in shape S read it in the shape it is produced in?
Definition fuse.cpp:110
static std::optional< u64 > injective_coord(const Def *var, const Def *e)
If e reads coordinate var#i injectively, returns i: a plain extract, possibly strided (affine....
Definition fuse.cpp:27
static bool reads_injectively(const Def *map)
Checks that map provably reads through every loop index of its domain: its body is the identity,...
Definition fuse.cpp:58
bool is_identity_post(const Def *post)
Is post the (rebuilt) CPS identity tensor.id, i.e.
Definition tensor.h:32
DefMap< u64 > count_consumers(const World &world, std::predicate< const Def * > auto pred)
Counts the consumers of every def of world matched by pred.
Definition tensor.h:109
std::optional< PureRead > is_pure_read(const Def *value)
If value is a pure re-indexed read — a copy-combiner map_reduce without reduction loops that writes i...
Definition tensor.h:51
bool is_copy_comb(const Def *comb)
Recognizes the (rebuilt) tensor_copy combiner (acc, ys) ↦ ys#0: the result is exactly the single inpu...
Definition tensor.h:25
bool is_identity_map(const Def *map)
Is map the identity access map λ x. x?
Definition tensor.h:18
A pure re-indexed read: the source tensor, the access map into it (over the read's output coordinates...
Definition tensor.h:39
The tuple Plugin
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
@ Lam
Definition def.h:125
static MapRedPost of(const App *callee)
Definition btensor.h:25
The five parallel per-slot lists of a map_reduce_post input group: element type, rank,...
Definition fuse.cpp:100
void push(const Def *t, const Def *r, const Def *s, const Def *m, const Def *v)
Definition fuse.cpp:103