MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
lower_map_reduce.cpp
Go to the documentation of this file.
2
3#include "mim/def.h"
4#include "mim/lam.h"
5
6#include "mim/util/types.h"
7
10#include "mim/plug/cps/cps.h"
11#include "mim/plug/mem/mem.h"
13
15
16const Def* LowerMapReduce::rec_broadcast(const Def* s_in, const Def* s_out, const Def* input, u64 r, u64 i) {
17 auto& w = new_world();
18 // Base case: all dimensions have been processed; `input` is the final scalar.
19 if (i == r) return input;
20
21 auto s_in_ri = s_in->proj(r, i), s_out_ri = s_out->proj(r, i);
22 DLOG("rec_broadcast");
23 DLOG(" r = {}", r);
24 DLOG(" i = {}", i);
25 DLOG(" s_in_ri = {} : {}", s_in_ri, s_in_ri->type());
26 DLOG(" s_out_ri = {} : {}", s_out_ri, s_out_ri->type());
27 DLOG(" input = {} : {}", input, input->type());
28
29 if (s_in_ri == s_out_ri) {
30 if (auto s_in_lit = Lit::isa<u64>(s_in_ri)) {
31 DefVec inputs(*s_in_lit, [&](size_t j) { return rec_broadcast(s_in, s_out, input->proj(j), r, i + 1); });
32 return w.tuple(inputs);
33 } else {
34 // TODO: we could probably support non-literal sizes as well, but we would need to generate loops to copy
35 // the data instead of just packing it.
36 WLOG("dimension {} of the input and output are equal but not literal: {} : {}", i, s_in_ri,
37 s_in_ri->type());
38 return nullptr;
39 }
40 }
41
42 if (auto s_in_lit = Lit::isa<u64>(s_in_ri); s_in_lit && *s_in_lit == 1) {
43 DLOG("dimension {} of the input is 1, can be broadcasted to dimension {} of the output", i, s_out_ri);
44 return w.pack(s_out_ri, rec_broadcast(s_in, s_out, input, r, i + 1));
45 }
46
47 WLOG("cannot broadcast dimension {} of size {} to size {}", i, s_in_ri, s_out_ri);
48 return nullptr;
49}
50
51const Def* LowerMapReduce::lower_broadcast(const App* app) {
52 auto& w = new_world();
53 auto c = rewrite(app->callee());
54 auto arg = rewrite(app->arg());
55
56 auto [s_in, s_out, input] = arg->projs<3>();
57 auto callee = c->as<App>();
58 auto [T, r] = callee->args<2>();
59 DLOG("lower_broadcast");
60 DLOG(" s_out = {} : {}", s_out, s_out->type());
61 DLOG(" input = {} : {}", input, input->type());
62 DLOG(" T = {} : {}", T, T->type());
63 DLOG(" r = {} : {}", r, r->type());
64 DLOG(" s_in = {} : {}", s_in, s_in->type());
65
66 auto r_nat = Lit::isa<u64>(r);
67 if (!r_nat) {
68 WLOG("{} doesn't have a lowering-time known rank: {}", app, r);
69 return nullptr;
70 }
71 // r_nat will never be 0, as we would have normalized this case away already
72 if (s_in == s_out) return input;
73
74 if (*r_nat == 1) {
75 if (auto s_in_lit = Lit::isa<u64>(s_in)) {
76 assert(*s_in_lit == 1 && "input dimensions must be 1 or equal to the output dimension");
77 return w.pack(s_out, input);
78 }
79 }
80
81 auto result = rec_broadcast(s_in, s_out, input, *r_nat, 0);
82 DLOG("result of rec_broadcast = {} : {}", result, result->type());
83 return result;
84}
85
86static std::pair<Lam*, const Def*> counting_for(const Def* bound, const Def* acc, const Def* exit, Sym name) {
87 auto& w = bound->world();
88 auto acc_ty = acc->type();
89 auto body = w.mut_con({/* iter */ w.type_i64(), /* acc */ acc_ty, /* return */ w.cn(acc_ty)})->set(name);
90 auto for_loop = w.call<affine::For>(body, exit, Defs{w.lit_i64(0), bound, w.lit_i64(1), acc});
91 return {body, for_loop};
92}
93
94static const Def* get_element_type(const Def* type, u64 r) {
95 auto cur = type;
96 for (u64 i = 0; i < r; ++i)
97 if (auto seq = cur->isa<Seq>())
98 cur = seq->body();
99 else
100 break;
101 return cur;
102}
103
104static const Def* nested_extract(World& w, const Def* matrix, const Def* coords, const Def* shape, u64 r) {
105 auto T = get_element_type(matrix->type(), r);
106 return op_get(T, w.lit_nat(r), shape, matrix, coords);
107}
108
109static const Def*
110nested_insert(World& w, const Def* matrix, const Def* coords, const Def* shape, u64 r, const Def* elem) {
111 auto T = get_element_type(matrix->type(), r);
112 return op_set(T, w.lit_nat(r), shape, matrix, coords, elem);
113}
114
115const Def* LowerMapReduce::lower_map_reduce(const App* app) {
116 // meta arguments:
117 // * nis = in-count (nat)
118 // * To = out-type (*), Ro = #output loops = result rank, Rr = #reduction loops
119 // * So = result shape (Ro*nat)
120 // * Sr = the full loop bounds (Ro+Rr)*nat: the leading Ro are the output-loop bounds, the trailing Rr the
121 // reductions
122 // * Tis/Ris/Sis = input types/ranks/shapes
123 // arguments:
124 // * f = combination function (CPS), init = accumulator init
125 // * acc_out = affine map from the (Ro+Rr) loop vector to the Ro write coordinates in the result «So» (the reduction
126 // part is not in scope at write-back, so acc_out must depend only on the leading Ro output indices)
127 // * accs = per-input affine map from the (Ro+Rr) loop vector to the input's read coordinates
128 // * is = input tensors
129 auto& w = new_world();
130 auto c = rewrite(app->callee())->as<App>();
131 auto inputs = rewrite(app->arg());
132 auto type = rewrite(app->type());
133
134 auto [nis, meta, shapes, TisRisSis, comb_init, acc_out, accs] = c->uncurry_args<7>();
135 auto [To, Ro, Rr] = meta->projs<3>();
136 auto [So, Sr] = shapes->projs<2>();
137 auto [Tis, Ris, Sis] = TisRisSis->projs<3>();
138 auto [comb, init] = comb_init->projs<2>();
139
140 auto nis_l = Lit::isa<u64>(nis);
141 auto ro_l = Lit::isa<u64>(Ro), rr_l = Lit::isa<u64>(Rr);
142 if (!nis_l || !ro_l || !rr_l) {
143 WLOG("{} doesn't have lowering-time known rank counts (nis/Ro/Rr)", app);
144 return nullptr;
145 }
146 auto nis_nat = *nis_l;
147 auto ro = *ro_l, rr = *rr_l;
148 auto nloops = ro + rr; // length of the full loop vector (= length of Sr)
149 auto n = w.lit_nat(nloops); // passed as the affine maps' domain length
150
151 // ranks of each input must be literal so that we know how many `extract`s to emit
152 Vector<u64> ris_nat(nis_nat);
153 for (u64 i = 0; i < nis_nat; ++i) {
154 auto l = Lit::isa<u64>(Ris->proj(nis_nat, i));
155 if (!l) {
156 WLOG("input {} of {} has a non-literal rank", i, app);
157 return nullptr;
158 }
159 ris_nat[i] = *l;
160 }
161
162 // Builds `%affine.map @(m, n) @(sin, sout) f idxs mem` and returns the result coordinates (dropping the returned
163 // mem). The emitted `%affine.map` is lowered to %core arithmetic by the subsequent %affine.lower_index. We
164 // invent a fresh `⊥ : %mem.M 0` for the mem operand here; real mem threading is wired up later by `add_mem`.
165 auto mem0 = w.app(w.annex<mem::M>(), w.lit_nat(0));
166 auto affine_map = [&](const Def* f, const Def* m, const Def* n, const Def* sin, const Def* sout, const Def* idxs) {
167 auto a = w.app(w.annex<affine::map>(), w.tuple({m, n}));
168 a = w.app(a, w.tuple({sin, sout}));
169 a = w.app(a, f);
170 a = w.app(a, idxs);
171 return w.app(a, w.bot(mem0))->proj(2, 1); // drop the returned mem at proj 0
172 };
173
174 try {
175 auto fun = w.mut_fun(inputs->type(), type)->set("mapRed");
176 auto ds_fun = cps::op_cps2ds_dep(fun)->set("dsFun");
177 auto call = w.app(ds_fun, inputs)->set("call");
178
179 auto new_inputs = fun->var(0)->set("is");
180
181 // Outer (parallel) loops over the leading Ro bounds of `Sr`, collecting the output iteration indices.
182 auto cont = fun->var(1);
183 auto init_mat = w.bot(cont->type()->as<Pi>()->dom());
184 auto acc = init_mat;
185 auto current_mut = fun;
186 DefVec out_iters;
187 out_iters.reserve(ro);
188 for (u64 i = 0; i < ro; ++i) {
189 auto dim = Sr->proj(nloops, i);
190 auto bound = w.call<core::bitcast>(w.type_i64(), dim);
191 auto [body, for_call] = counting_for(bound, acc, cont, w.sym("forOut_" + std::to_string(i)));
192 auto [iter, new_acc, yield] = body->vars<3>();
193 cont = yield;
194 out_iters.push_back(w.call(core::conv::u, dim, iter));
195 acc = new_acc;
196 current_mut->set(true, for_call);
197 current_mut = body;
198 }
199 auto wb_matrix = acc;
200
201 // Write-back: narrow the accumulated element into the result at the affine write coordinates `acc_out`.
202 // acc_out takes the full (Ro+Rr) loop vector, but the reduction loops have already been folded away here, so we
203 // pass 0 for those slots; acc_out must depend only on the leading Ro output indices.
204 auto write_back = w.mut_con(To)->set("writeBack");
205 auto element_final = write_back->var(0);
206 DefVec wb_iters = out_iters;
207 for (u64 j = 0; j < rr; ++j)
208 wb_iters.push_back(w.call(core::conv::u, Sr->proj(nloops, ro + j), w.lit(w.type_i64(), 0)));
209 auto write_coords = affine_map(acc_out, Ro, n, Sr, So, w.tuple(wb_iters)); // «Ro; Idx (So#k)»
210 write_back->app(true, cont, nested_insert(w, wb_matrix, write_coords, So, ro, element_final));
211
212 // Inner (reduction) loops over the trailing Rr bounds of `Sr`, collecting the reduction iteration indices.
213 acc = init;
214 cont = write_back;
215 DefVec red_iters;
216 red_iters.reserve(rr);
217 for (u64 j = 0; j < rr; ++j) {
218 auto dim = Sr->proj(nloops, ro + j);
219 auto bound = w.call<core::bitcast>(w.type_i64(), dim);
220 auto [body, for_call] = counting_for(bound, acc, cont, w.sym("forIn_" + std::to_string(j)));
221 auto [iter, new_acc, yield] = body->vars<3>();
222 cont = yield;
223 red_iters.push_back(w.call(core::conv::u, dim, iter));
224 acc = new_acc;
225 current_mut->set(true, for_call);
226 current_mut = body;
227 }
228 auto element_acc = acc;
229
230 // The full loop iteration vector `(o…, r…)`; its moduli are exactly `Sr`.
231 DefVec iters_v = out_iters;
232 iters_v.insert(iters_v.end(), red_iters.begin(), red_iters.end());
233 auto iters = w.tuple(iters_v);
234
235 // Read one element from each input at its affine read coordinates.
236 DefVec input_elements(nis_nat);
237 for (u64 i = 0; i < nis_nat; ++i) {
238 auto input_matrix = new_inputs->proj(nis_nat, i);
239 auto sis_i = Sis->proj(nis_nat, i);
240 auto coords = affine_map(accs->proj(nis_nat, i), Ris->proj(nis_nat, i), n, Sr, sis_i, iters);
241 input_elements[i] = nested_extract(w, input_matrix, coords, sis_i, ris_nat[i]);
242 }
243
244 comb->set("comb");
245 current_mut->app(true, comb, {w.tuple({element_acc, w.tuple(input_elements)}), cont});
246 return call;
247 } catch (const std::exception& e) { fe::throwf("error during lowering map_reduce: {}", e.what()); }
248}
249
250const Def* LowerMapReduce::build_pointwise(const Def* inputs,
251 const Def* type,
252 const Def* So,
253 u64 ro,
254 std::function<const Def*(const DefVec&, const Def*)> compute) {
255 auto& w = new_world();
256
257 auto fun = w.mut_fun(inputs->type(), type)->set("pointwise");
258 auto ds_fun = cps::op_cps2ds_dep(fun)->set("dsFun");
259 auto call = w.app(ds_fun, inputs)->set("call");
260
261 auto new_inputs = fun->var(0)->set("is");
262
263 // Output loops over `So`, collecting the raw i64 iteration indices for `compute`.
264 auto cont = fun->var(1);
265 auto acc = w.bot(cont->type()->as<Pi>()->dom());
266 auto current_mut = fun;
267 DefVec out_iters; // raw i64 loop counters
268 out_iters.reserve(ro);
269 for (u64 i = 0; i < ro; ++i) {
270 auto dim = So->proj(ro, i);
271 auto bound = w.call<core::bitcast>(w.type_i64(), dim);
272 auto [body, for_call] = counting_for(bound, acc, cont, w.sym("forOut_" + std::to_string(i)));
273 auto [iter, new_acc, yield] = body->vars<3>();
274 cont = yield;
275 out_iters.push_back(iter);
276 acc = new_acc;
277 current_mut->set(true, for_call);
278 current_mut = body;
279 }
280 auto wb_matrix = acc;
281
282 // Write the computed element at the (identity) output coordinates; convert the i64 counters to `Idx (So#k)`.
283 DefVec write_coords(ro);
284 for (u64 i = 0; i < ro; ++i)
285 write_coords[i] = w.call(core::conv::u, So->proj(ro, i), out_iters[i]);
286 auto element = compute(out_iters, new_inputs);
287 current_mut->app(true, cont, nested_insert(w, wb_matrix, w.tuple(write_coords), So, ro, element));
288 return call;
289}
290
291const Def* LowerMapReduce::lower_pad(const App* app) {
292 auto& w = new_world();
293 auto c = rewrite(app->callee())->as<App>();
294 auto args = rewrite(app->arg()); // (input, value)
295 auto type = rewrite(app->type());
296
297 // callee: pad {T, r} [s_in] [mode, lo, hi]
298 auto [Tr, s_in, params] = c->uncurry_args<3>();
299 auto [T, r] = Tr->projs<2>();
300 auto [mode, lo, hi] = params->projs<3>();
301
302 auto r_l = Lit::isa<u64>(r);
303 auto mode_l = Lit::isa<u64>(mode);
304 if (!r_l || !mode_l) {
305 WLOG("{} doesn't have a lowering-time known rank/mode", app);
306 return nullptr;
307 }
308 auto rn = *r_l;
309 auto mode_nat = *mode_l;
310 auto i64 = w.type_i64();
311
312 // Deduce the output shape: s_out#d = lo#d + s_in#d + hi#d.
313 DefVec so(rn);
314 auto inner_type = type;
315 for (u64 d = 0; d < rn; ++d) {
316 auto inner_type_seq = inner_type->as<Seq>();
317 so[d] = inner_type_seq->arity();
318 inner_type = inner_type_seq->body();
319 }
320 auto s_out = w.tuple(so);
321
322 // select(cond, t, f) == `(f, t)#cond` (cf. %core.select); cond : Bool.
323 auto sel = [&](const Def* cond, const Def* t, const Def* f) { return w.extract(w.tuple({f, t}), cond); };
324
325 auto compute = [&](const DefVec& out_iters, const Def* new_inputs) -> const Def* {
326 auto [input, value] = new_inputs->projs<2>();
327 DefVec clamped(rn); // per-axis read index, kept in range, as `Idx (s_in#d)`
328 DefVec valid; // per-axis in-bounds flag (constant mode only)
329 for (u64 d = 0; d < rn; ++d) {
330 auto lo_d = w.call<core::bitcast>(i64, lo->proj(rn, d));
331 auto sin_d = w.call<core::bitcast>(i64, s_in->proj(rn, d));
332 auto in_d = w.call(core::wrap::sub, core::Mode::none, Defs{out_iters[d], lo_d}); // o#d − lo#d
333 const Def* idx_i64;
334 if (mode_nat == 0) { // constant: a single unsigned `<` covers both bounds (underflow wraps high)
335 auto v_d = w.call(core::icmp::ul, w.tuple({in_d, sin_d}));
336 valid.push_back(v_d);
337 idx_i64 = sel(v_d, in_d, w.lit_i64(0));
338 } else { // replicate: clamp the read to the nearest edge [0, s_in#d − 1]
339 auto sin_m1 = w.call(core::wrap::sub, core::Mode::none, Defs{sin_d, w.lit_i64(1)});
340 idx_i64 = w.call(core::extrema::smax,
341 w.tuple({w.lit_i64(0), w.call(core::extrema::smin, w.tuple({in_d, sin_m1}))}));
342 }
343 clamped[d] = w.call(core::conv::u, s_in->proj(rn, d), idx_i64);
344 }
345 auto elem = nested_extract(w, input, w.tuple(clamped), s_in, rn);
346 if (mode_nat != 0) return elem; // replicate: always a (clamped) read
347 auto all_valid = valid.empty() ? w.lit_tt() : valid[0];
348 for (u64 d = 1; d < valid.size(); ++d)
349 all_valid = w.call(core::bit2::and_, w.lit_nat(2), w.tuple({all_valid, valid[d]}));
350 return sel(all_valid, elem, value); // constant: fill out-of-region cells with `value`
351 };
352
353 return build_pointwise(args, type, s_out, rn, compute);
354}
355
356const Def* LowerMapReduce::lower_concat(const App* app) {
357 auto& w = new_world();
358 auto c = rewrite(app->callee())->as<App>();
359 auto args = rewrite(app->arg()); // the `is` input tuple
360 auto type = rewrite(app->type());
361
362 // callee: concat {T, nis, r} [ax] {Sis}
363 auto [TnisR, ax, Sis] = c->uncurry_args<3>();
364 auto [T, nis, r] = TnisR->projs<3>();
365
366 auto nis_l = Lit::isa<u64>(nis);
367 auto r_l = Lit::isa<u64>(r);
368 auto ax_l = Lit::isa<u64>(ax);
369 if (!nis_l || !r_l || !ax_l) {
370 WLOG("{} doesn't have lowering-time known nis/r/ax", app);
371 return nullptr;
372 }
373 auto nisn = *nis_l, rn = *r_l, axn = *ax_l;
374 auto i64 = w.type_i64();
375
376 // Prefix offsets along `ax`: off#i = Σ_{j<i} Sis#j#ax (literal extents required).
377 DefVec off(nisn);
378 u64 acc_off = 0;
379 for (u64 i = 0; i < nisn; ++i) {
380 off[i] = w.lit_i64(acc_off);
381 auto ei = Lit::isa<u64>(Sis->proj(nisn, i)->proj(rn, axn));
382 if (!ei) {
383 WLOG("{} input {} has a non-literal extent along the concat axis", app, i);
384 return nullptr;
385 }
386 acc_off += *ei;
387 }
388
389 // Deduce the output shape: the summed extent along `ax`, the shared extents elsewhere.
390 DefVec so(rn);
391 for (u64 d = 0; d < rn; ++d)
392 so[d] = (d == axn) ? w.lit_nat(acc_off) : Sis->proj(nisn, 0)->proj(rn, d);
393 auto s_out = w.tuple(so);
394
395 auto sel = [&](const Def* cond, const Def* t, const Def* f) { return w.extract(w.tuple({f, t}), cond); };
396
397 auto compute = [&](const DefVec& out_iters, const Def* new_inputs) -> const Def* {
398 auto o_ax = out_iters[axn];
399 // Read input `i` at `out_iters`, but with the `ax` coordinate shifted by off#i and clamped into input `i`.
400 auto read_i = [&](u64 i) -> const Def* {
401 auto Sis_i = Sis->proj(nisn, i);
402 auto e_i = w.call<core::bitcast>(i64, Sis_i->proj(rn, axn));
403 auto e_i_m1 = w.call(core::wrap::sub, core::Mode::none, Defs{e_i, w.lit_i64(1)});
404 auto loc = w.call(core::wrap::sub, core::Mode::none, Defs{o_ax, off[i]});
405 auto clamp = w.call(core::extrema::smax,
406 w.tuple({w.lit_i64(0), w.call(core::extrema::smin, w.tuple({loc, e_i_m1}))}));
407 DefVec coords(rn);
408 for (u64 d = 0; d < rn; ++d) {
409 auto idx_i64 = (d == axn) ? clamp : out_iters[d];
410 coords[d] = w.call(core::conv::u, Sis_i->proj(rn, d), idx_i64);
411 }
412 return nested_extract(w, new_inputs->proj(nisn, i), w.tuple(coords), Sis_i, rn);
413 };
414 // Select chain: the highest `i` with off#i ≤ o_ax owns the cell (offsets increase, later wins).
415 auto result = read_i(0);
416 for (u64 i = 1; i < nisn; ++i) {
417 auto cond = w.call(core::icmp::uge, w.tuple({o_ax, off[i]}));
418 result = sel(cond, read_i(i), result);
419 }
420 return result;
421 };
422
423 return build_pointwise(args, type, s_out, rn, compute);
424}
425
427 if (auto bc = Axm::isa<tensor::broadcast>(app)) {
428 if (auto res = lower_broadcast(bc)) return res;
429 } else if (auto mr = Axm::isa<tensor::map_reduce>(app)) {
430 if (auto res = lower_map_reduce(mr)) return res;
431 } else if (auto pad = Axm::isa<tensor::pad>(app)) {
432 if (auto res = lower_pad(pad)) return res;
433 } else if (auto cat = Axm::isa<tensor::concat>(app)) {
434 if (auto res = lower_concat(cat)) return res;
435 }
436 return RWPhase::rewrite_imm_App(app);
437}
438
439} // namespace mim::plug::tensor::phase
const Def * callee() const
Definition lam.h:276
const Def * arg() const
Definition lam.h:285
static auto isa(const Def *def)
Definition axm.h:107
Base class for all Defs.
Definition def.h:261
Def * set(size_t i, const Def *)
Successively set from left to right.
Definition def.cpp:276
World & world() const noexcept
Definition def.cpp:483
const Def * type() const noexcept
Yields the "raw" type of this Def (maybe nullptr).
Definition def.cpp:491
static std::optional< T > isa(const Def *def)
Definition def.h:878
const Vector< std::string > & args()
Command-line arguments passed to this Phase's plugin via -X <plugin>:<arg>.
Definition phase.cpp:23
World & new_world()
Create new Defs into this.
Definition phase.h:368
virtual const Def * rewrite(const Def *)
Definition rewrite.cpp:56
Base class for Arr and Pack.
Definition tuple.h:86
This is a thin wrapper for absl::InlinedVector<T, N, A> which is a drop-in replacement for std::vecto...
Definition vector.h:18
The World represents the whole program and manages creation of MimIR nodes (Defs).
Definition world.h:36
const Def * rewrite_imm_App(const App *) final
#define WLOG(...)
Definition log.h:89
#define DLOG(...)
Vaporizes to nothingness in Debug build.
Definition log.h:94
@ none
Wrap around.
Definition core.h:16
const Def * op_cps2ds_dep(const Def *k)
Definition cps.h:16
The matrix Plugin
Definition matrix.h:8
static const Def * nested_insert(World &w, const Def *matrix, const Def *coords, const Def *shape, u64 r, const Def *elem)
static const Def * nested_extract(World &w, const Def *matrix, const Def *coords, const Def *shape, u64 r)
static const Def * get_element_type(const Def *type, u64 r)
static std::pair< Lam *, const Def * > counting_for(const Def *bound, const Def *acc, const Def *exit, Sym name)
const Def * op_set(const Def *T, const Def *r, const Def *s, const Def *arr, const Def *index, const Def *x)
Definition tensor.h:17
const Def * op_get(const Def *T, const Def *r, const Def *s, const Def *arr, const Def *index)
Definition tensor.h:9
View< const Def * > Defs
Definition def.h:78
Vector< const Def * > DefVec
Definition def.h:79
uint64_t u64
Definition types.h:27
DefVec cat(Defs, Defs)
Definition tuple.cpp:81
@ Pi
Definition def.h:109
@ App
Definition def.h:109