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