MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
lower_map_reduce.cpp
Go to the documentation of this file.
2
3#include <numeric>
4#include <optional>
5
6#include <mim/def.h>
7#include <mim/lam.h>
8
9#include <mim/util/types.h>
10
13#include <mim/plug/core/core.h>
14#include <mim/plug/cps/cps.h>
15#include <mim/plug/mem/mem.h>
16
19
21
22const Def* LowerMapReduce::rec_broadcast(const Def* s_in, const Def* s_out, const Def* input, u64 r, u64 i) {
23 auto& w = new_world();
24 // Base case: all dimensions have been processed; `input` is the final scalar.
25 if (i == r) return input;
26
27 auto s_in_ri = s_in->proj(r, i), s_out_ri = s_out->proj(r, i);
28 log().d("broadcast dimension {} of {}: {} → {}, input = {}: {}", i, r, s_in_ri, s_out_ri, input, input->type());
29
30 if (s_in_ri == s_out_ri) {
31 if (auto s_in_lit = Lit::isa<u64>(s_in_ri)) {
32 DefVec inputs(*s_in_lit,
33 [&](size_t j) { return rec_broadcast(s_in, s_out, input->proj(*s_in_lit, j), r, i + 1); });
34 return w.tuple(inputs);
35 } else {
36 // TODO: we could probably support non-literal sizes as well, but we would need to generate loops to copy
37 // the data instead of just packing it.
38 log().w("dimension {} has equal but non-literal extent: {}", i, s_in_ri);
39 return nullptr;
40 }
41 }
42
43 if (auto s_in_lit = Lit::isa<u64>(s_in_ri); s_in_lit && *s_in_lit == 1) {
44 log().d("dimension {}: packing the size-1 input to {}", i, s_out_ri);
45 return w.pack(s_out_ri, rec_broadcast(s_in, s_out, input, r, i + 1));
46 }
47
48 log().w("cannot broadcast dimension {}: {} → {}", i, s_in_ri, s_out_ri);
49 return nullptr;
50}
51
52const Def* LowerMapReduce::lower_broadcast(const App* app) {
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 log().d("lower broadcast: input = {}: {}, T = {}, r = {}, s_in = {}, s_out = {}", input, input->type(), T, r, s_in,
60 s_out);
61
62 auto r_nat = Lit::isa<u64>(r);
63 if (!r_nat) {
64 log().w("rank {} of {} is not known at lowering time", r, app);
65 return nullptr;
66 }
67 // r_nat will never be 0, as we would have normalized this case away already
68 if (s_in == s_out) return input;
69
70 auto result = rec_broadcast(s_in, s_out, input, *r_nat, 0);
71 log().d("broadcast result: {}", result);
72 return result;
73}
74
75const Def* LowerMapReduce::lower_map_reduce(const App* app) {
76 // meta arguments:
77 // * nis = in-count, nps = epilogue-input count (nat)
78 // * To = accumulator type, Tp = out-element type (post: Fn [To, «nps; Tps»] → Tp), Ro = #output loops =
79 // result rank, Rn = #loops in total
80 // * So = result shape (Ro*nat)
81 // * Sr = the full loop bounds Rn*nat: the leading Ro are the output-loop bounds, the trailing Rn - Ro the
82 // reductions
83 // * Tis/Ris/Sis, Tps/Rps/Sps = (epilogue) input types/ranks/shapes
84 // arguments:
85 // * f = combination function (CPS), init = accumulator init, post = per-output-cell epilogue (CPS),
86 // applied to the folded accumulator and the epilogue elements right before the write-back
87 // * map_out = affine map from the Rn loop vector to the Ro write coordinates in the result «So» (the reduction
88 // part is not in scope at write-back, so map_out must depend only on the leading Ro output indices)
89 // * maps = per-input affine map from the Rn loop vector to the input's read coordinates
90 // * post_maps = per-epilogue-input affine map from the Ro output-cell (write) coordinates to its read coordinates
91 // * is, post_is = input tensors
92 auto& w = new_world();
93 auto c = rewrite(app->callee())->as<App>();
94 auto inputs = rewrite(app->arg());
95 auto type = rewrite(app->type());
96
97 auto mr = btensor::MapRedPost::of(c);
98 auto Ro = mr.Ro, Sr = mr.Sr, So = mr.So;
99
100 auto ranks = mr.ranks();
101 if (!ranks) {
102 log().w("rank counts (nis/nps/Ro/Rn) of {} are not known at lowering time", app);
103 return nullptr;
104 }
105 auto [nis_nat, nps_nat, ro, rr] = *ranks;
106 auto nloops = ranks->nloops(); // length of the full loop vector (= length of Sr)
107 auto n = w.lit_nat(nloops); // passed as the affine maps' domain length
108
109 // the affine maps below take each input's rank as their domain length, so it must be literal
110 if (!lit_projs(mr.Ris, nis_nat) || !lit_projs(mr.Rps, nps_nat)) {
111 log().w("the input ranks of {} are not known at lowering time", app);
112 return nullptr;
113 }
114
115 // The result coordinates of `affine.map @(m, n) @(sin, sout) f idxs mem`, dropping the returned mem.
116 // We invent a fresh `⊥ : mem.M 0` for the mem operand here; real mem threading is wired up later by `add_mem`.
117 auto mem0 = w.call<mem::M>(0);
118 auto affine_map = [&](const Def* f, const Def* m, const Def* n, const Def* sin, const Def* sout, const Def* idxs) {
119 return btensor::op_affine_map(f, m, n, sin, sout, idxs, w.bot(mem0))->proj(2, 1);
120 };
121
122 try {
123 auto fun = w.mut_fun(inputs->type(), type)->set("mapRed");
124 auto ds_fun = cps::op_cps2ds_dep(fun)->set("dsFun");
125 auto call = w.app(ds_fun, inputs)->set("call");
126
127 auto [new_inputs, cont] = fun->vars<2>();
128 auto [new_is, new_post_is] = new_inputs->set("is")->projs<2>();
129 auto sr = Sr->projs(nloops);
130
131 // Outer (parallel) loops over the leading Ro bounds of `Sr`, collecting the output iteration indices.
132 const Def* acc = w.bot(cont->type()->as<Pi>()->dom());
133 auto current_mut = fun;
134 auto raw_out = affine::counting_nest(current_mut, cont, acc, Defs(sr).subspan(0, ro), "forOut");
135 auto out_iters = btensor::to_idxs(w, Defs(sr).subspan(0, ro), raw_out);
136 auto wb_matrix = acc;
137
138 // Write-back: run the `post` epilogue on the accumulated element, then narrow the result into the
139 // output at the affine write coordinates `map_out`.
140 // map_out takes the full (Ro+Rr) loop vector, but the reduction loops have already been folded away here, so we
141 // pass 0 for those slots; map_out must depend only on the leading Ro output indices.
142 auto write_back = w.mut_con(mr.To)->set("writeBack");
143 auto element_final = write_back->var();
144 DefVec wb_iters = out_iters;
145 for (u64 j = 0; j < rr; ++j)
146 wb_iters.emplace_back(w.call(core::conv::u, sr[ro + j], w.lit_i64(0)));
147 auto write_coords = affine_map(mr.map_out, Ro, n, Sr, So, w.tuple(wb_iters)); // «Ro; Idx (So#k)»
148
149 // Read one element from each epilogue input at its post_maps-mapped output-cell coordinates.
150 DefVec post_elements(nps_nat, [&](size_t j) {
151 auto sps_j = mr.Sps->proj(nps_nat, j);
152 auto coords
153 = affine_map(mr.post_maps->proj(nps_nat, j), mr.Rps->proj(nps_nat, j), Ro, So, sps_j, write_coords);
154 return w.extract(new_post_is->proj(nps_nat, j), coords);
155 });
156
157 auto after_post = w.mut_con(mr.Tp)->set("afterPost");
158 after_post->app(true, cont, w.insert(wb_matrix, write_coords, after_post->var()));
159 write_back->app(true, mr.post, {w.tuple({element_final, w.tuple(post_elements)}), after_post});
160
161 // Inner (reduction) loops over the trailing `Rr` bounds of `Sr`, collecting the reduction iteration
162 // indices.
163 acc = mr.init;
164 cont = write_back;
165 auto raw_red = affine::counting_nest(current_mut, cont, acc, Defs(sr).subspan(ro, rr), "forIn");
166 auto element_acc = acc;
167
168 // The full loop iteration vector `(o…, r…)`; its moduli are exactly `Sr`.
169 DefVec iters_v = out_iters;
170 for (auto red : btensor::to_idxs(w, Defs(sr).subspan(ro, rr), raw_red))
171 iters_v.emplace_back(red);
172 auto iters = w.tuple(iters_v);
173
174 // Read one element from each input at its affine read coordinates.
175 DefVec input_elements(nis_nat, [&](size_t i) {
176 auto sis_i = mr.Sis->proj(nis_nat, i);
177 auto coords = affine_map(mr.maps->proj(nis_nat, i), mr.Ris->proj(nis_nat, i), n, Sr, sis_i, iters);
178 return w.extract(new_is->proj(nis_nat, i), coords);
179 });
180
181 mr.comb->set("comb");
182 mr.post->set("post");
183 current_mut->app(true, mr.comb, {w.tuple({element_acc, w.tuple(input_elements)}), cont});
184 return call;
185 } catch (const fe::Error::Bail&) { throw; } catch (const std::exception& e) {
186 fe::throwf("failed to lower `tensor.map_reduce`: {}", e.what());
187 }
188}
189
190const Def* LowerMapReduce::build_pointwise(const Def* inputs,
191 const Def* type,
192 const Def* So,
193 u64 ro,
194 std::function<const Def*(Defs, const Def*)> compute) {
195 auto& w = new_world();
196
197 auto fun = w.mut_fun(inputs->type(), type)->set("pointwise");
198 auto ds_fun = cps::op_cps2ds_dep(fun)->set("dsFun");
199 auto call = w.app(ds_fun, inputs)->set("call");
200
201 auto [new_inputs, cont] = fun->vars<2>();
202 new_inputs->set("is");
203
204 // Output loops over `So`, collecting the raw i64 iteration indices for `compute`.
205 const Def* acc = w.bot(cont->type()->as<Pi>()->dom());
206 auto current_mut = fun;
207 auto so = So->projs(ro);
208 auto out_iters = affine::counting_nest(current_mut, cont, acc, so, "forOut"); // raw i64 loop counters
209 auto wb_matrix = acc;
210
211 // Write the computed element at the (identity) output coordinates; convert the i64 counters to `Idx (So#k)`.
212 auto write_coords = btensor::to_idxs(w, so, out_iters);
213 auto element = compute(out_iters, new_inputs);
214 current_mut->app(true, cont, w.insert(wb_matrix, w.tuple(write_coords), element));
215 return call;
216}
217
218const Def* LowerMapReduce::lower_generate(const App* app) {
219 auto& w = new_world();
220 auto c = rewrite(app->callee())->as<App>();
221 auto body = rewrite(app->arg());
222 auto type = rewrite(app->type());
223
224 auto [meta, s_out] = c->uncurry_args<2>();
225 auto [T, r] = meta->projs<2>();
226 auto r_l = Lit::isa<u64>(r);
227 if (!r_l) {
228 log().w("rank {} of {} is not known at lowering time", r, app);
229 return nullptr;
230 }
231 auto rn = *r_l;
232
233 // Nested arrays erase literal singleton axes. If every logical axis is
234 // erased (including rank zero), the sole element is body((0, ..., 0)).
235 if (!type->isa<Arr>()) {
236 DefVec zeros(rn, [&](size_t) { return w.lit_i64(0); });
237 return w.app(body, w.tuple(zeros));
238 }
239
240 auto unit = w.tuple();
241 auto compute = [&](Defs out_iters, const Def*) { return w.call(body, out_iters); };
242 return build_pointwise(unit, type, s_out, rn, compute);
243}
244
245const Def* LowerMapReduce::lower_pad(const App* app) {
246 auto& w = new_world();
247 auto c = rewrite(app->callee())->as<App>();
248 auto args = rewrite(app->arg()); // (input, value)
249 auto type = rewrite(app->type());
250
251 // callee: pad {T, r} [s_in] [mode, lo, hi]
252 auto [Tr, s_in, params] = c->uncurry_args<3>();
253 auto [T, r] = Tr->projs<2>();
254 auto [mode, lo, hi] = params->projs<3>();
255
256 auto r_l = Lit::isa<u64>(r);
257 auto mode_l = Lit::isa<u64>(mode);
258 if (!r_l || !mode_l) {
259 log().w("rank/mode of {} is not known at lowering time", app);
260 return nullptr;
261 }
262 auto rn = *r_l;
263 auto mode_nat = *mode_l;
264
265 // The output shape `s_out#d = lo#d + s_in#d + hi#d` is what `type` already carries.
266 auto out_arr = type->isa<Arr>();
267 if (!out_arr || out_arr->shape().rank() != rn) {
268 log().w("padded shape of {} is not a statically known rank-{} array", app, rn);
269 return nullptr;
270 }
271 auto s_out = *out_arr->shape();
272
273 auto compute = [&](Defs out_iters, const Def* new_inputs) -> const Def* {
274 auto [input, value] = new_inputs->projs<2>();
275 auto [coords, valid] = btensor::pad_coords(out_iters, lo, s_in, mode_nat != 0);
276 auto elem = w.extract(input, w.tuple(coords));
277 return valid ? w.select(valid, elem, value) : elem; // constant: fill out-of-region cells with `value`
278 };
279
280 return build_pointwise(args, type, s_out, rn, compute);
281}
282
283const Def* LowerMapReduce::lower_concat(const App* app) {
284 auto& w = new_world();
285 auto c = rewrite(app->callee())->as<App>();
286 auto args = rewrite(app->arg()); // the `is` input tuple
287 auto type = rewrite(app->type());
288
289 // callee: concat {T, nis, r} [ax] {Sis}
290 auto [TnisR, ax, Sis] = c->uncurry_args<3>();
291 auto [T, nis, r] = TnisR->projs<3>();
292
293 auto nis_l = Lit::isa<u64>(nis);
294 auto r_l = Lit::isa<u64>(r);
295 auto ax_l = Lit::isa<u64>(ax);
296 if (!nis_l || !r_l || !ax_l) {
297 log().w("nis/r/ax of {} are not known at lowering time", app);
298 return nullptr;
299 }
300 auto nisn = *nis_l, rn = *r_l, axn = *ax_l;
301
302 auto exts = btensor::concat_extents(Sis, nisn, rn, axn);
303 if (!exts) {
304 log().w("an extent of {} along the concat axis is not known at lowering time", app);
305 return nullptr;
306 }
307
308 // Deduce the output shape: the summed extent along `ax`, the shared extents elsewhere.
309 auto sum_ax = std::accumulate(exts->begin(), exts->end(), u64(0));
310 DefVec so(rn, [&](size_t d) { return d == axn ? w.lit_nat(sum_ax) : Sis->proj(nisn, 0)->proj(rn, d); });
311 auto s_out = w.tuple(so);
312
313 auto compute = [&](Defs out_iters, const Def* new_inputs) -> const Def* {
314 // Read input `i` at `out_iters`, but with the `ax` coordinate replaced by `ax_idx`.
315 auto read = [&](u64 i, const Def* ax_idx) -> const Def* {
316 auto Sis_i = Sis->proj(nisn, i);
317 DefVec coords(rn, [&](size_t d) {
318 return w.call(core::conv::u, Sis_i->proj(rn, d), d == axn ? ax_idx : out_iters[d]);
319 });
320 return w.extract(new_inputs->proj(nisn, i), w.tuple(coords));
321 };
322 return btensor::concat_select(out_iters[axn], *exts, read);
323 };
324
325 return build_pointwise(args, type, s_out, rn, compute);
326}
327
328const Def* LowerMapReduce::lower_gather(const App* app) {
329 auto& w = new_world();
330 auto c = rewrite(app->callee())->as<App>();
331 auto args = rewrite(app->arg());
332 auto type = rewrite(app->type());
333
334 auto [Tr, shapes, dim] = c->uncurry_args<3>();
335 auto [T, r] = Tr->projs<2>();
336 auto [s_src, s_idx] = shapes->projs<2>();
337 auto r_l = Lit::isa<u64>(r);
338 auto dim_l = Lit::isa<u64>(dim);
339 if (!r_l || !dim_l) return nullptr;
340
341 // On every axis other than `dim`, gather reuses the output coordinate as
342 // an input coordinate. The shared checker rejects statically provable
343 // violations before any source buffer access is emitted.
344 if (!check_gather_shape_constraints(app, r, dim, s_src, s_idx)) return nullptr;
345
346 auto compute = [&](Defs out_indices, const Def* inputs) -> const Def* {
347 auto element = w.app(w.annex<tensor::gather_pointwise_elem_impl>(), {T, r});
348 return w.call(element, Defs{s_src, s_idx}, dim, out_indices, inputs);
349 };
350 return build_pointwise(args, type, s_idx, *r_l, compute);
351}
352
353const Def* LowerMapReduce::lower_scatter(const App* app) {
354 auto& w = new_world();
355 auto c = rewrite(app->callee())->as<App>();
356 auto args = rewrite(app->arg());
357 auto type = rewrite(app->type());
358
359 auto [Tr, shapes, dim] = c->uncurry_args<3>();
360 auto [T, r] = Tr->projs<2>();
361 auto [s_src, s_idx, s_updates] = shapes->projs<3>();
362 auto r_l = Lit::isa<u64>(r);
363 auto dim_l = Lit::isa<u64>(dim);
364 if (!r_l || !dim_l) return nullptr;
365 auto rn = *r_l;
366
367 // Every scatter visit reads one update at the same coordinate as the
368 // index tensor, while non-dim coordinates are also reused in the source.
369 if (!check_scatter_shape_constraints(app, r, dim, s_src, s_idx, s_updates)) return nullptr;
370
371 auto fun = w.mut_fun(args->type(), type)->set("scatter");
372 auto ds_fun = cps::op_cps2ds_dep(fun)->set("dsFun");
373 auto call = w.app(ds_fun, args)->set("call");
374
375 auto [iiu, cont] = fun->vars<2>();
376 auto [input, index, updates] = iiu->projs<3>();
377 auto acc = input;
378 auto current = fun;
379 auto dims = s_idx->projs(rn);
380 auto visit_indices = affine::counting_nest(current, cont, acc, dims, "scatter");
381
382 auto step = w.app(w.annex<tensor::scatter_step_impl>(), {T, r});
383 auto next = w.call(step, Defs{s_src, s_idx, s_updates}, dim, visit_indices, Defs{acc, index, updates});
384 current->app(true, cont, next);
385 return call;
386}
387
389 // A `tensor.if_static` still stuck at lowering time guards a runtime value: residualize to
390 // its dynamic branch.
391 if (Axm::isa<tensor::if_static>(app)) return rewrite(app->arg(3, 2));
392 if (auto bc = Axm::isa<tensor::broadcast>(app)) {
393 if (auto res = lower_broadcast(bc)) return res;
394 } else if (auto mr = Axm::isa<tensor::map_reduce_post>(app)) {
395 if (auto res = lower_map_reduce(mr)) return res;
396 } else if (auto generate = Axm::isa<tensor::generate>(app)) {
397 if (auto res = lower_generate(generate)) return res;
398 } else if (auto pad = Axm::isa<tensor::pad>(app)) {
399 if (auto res = lower_pad(pad)) return res;
400 } else if (auto cat = Axm::isa<tensor::concat>(app)) {
401 if (auto res = lower_concat(cat)) return res;
402 } else if (auto gather = Axm::isa<tensor::gather>(app)) {
403 if (auto res = lower_gather(gather)) return res;
404 } else if (auto scatter = Axm::isa<tensor::scatter>(app)) {
405 if (auto res = lower_scatter(scatter)) return res;
406 }
407 return RWPhase::rewrite_imm_App(app);
408}
409
410} // namespace mim::plug::tensor::phase
const Def * arg() const
Definition lam.h:251
static auto isa(const Def *def)
Definition axm.h:112
Base class for all Defs.
Definition def.h:289
const Def * proj(nat_t a, nat_t i) const
Similar to World::extract while assuming an arity of a, but also works on Sigmas and Arrays.
Definition def.cpp:647
Def * set(size_t i, const Def *)
Successively set from left to right.
Definition def.cpp:188
static std::optional< T > isa(const Def *def)
Definition def.h:976
const fe::Log & log() const
Definition phase.h:60
World & new_world()
Create new Defs into this.
Definition phase.h:379
const Def * rewrite_imm_App(const App *) override
Definition phase.cpp:223
virtual const Def * rewrite(const Def *)
Definition rewrite.cpp:62
const Def * rewrite_imm_App(const App *) final
DefVec counting_nest(Lam *&cur, const Def *&exit, const Def *&acc, Defs dims, std::string_view name)
Nests one counting loop per bound of dims inside cur, threading cur, exit and acc down to the innermo...
Definition affine.h:24
const Def * op_affine_map(const Def *f, const Def *m, const Def *n, const Def *s_in, const Def *s_out, const Def *idxs, const Def *mem)
affine.map @(m, n) @(s_in, s_out) f idxs as mem with the address space as of mem ↦ [mem',...
Definition btensor.h:55
std::optional< fe::Vector< u64 > > concat_extents(const Def *Sis, u64 nis, u64 r, u64 ax)
The literal extents of the nis input shapes Sis along the concat axis ax; std::nullopt if one is not ...
Definition btensor.h:140
DefVec to_idxs(World &w, Defs extents, Defs iters)
Converts the raw i64 loop counters iters into indices Idx extents[d].
Definition btensor.h:93
const Def * build_pointwise(const Def *result_ty, const Def *mem, const Def *dst, const Def *ins, const Def *s_out, u64 rn, const std::string &name, auto compute)
A [mem, dst, ins] → [mem, Buf] fun spliced via cps.cps2ds_dep that loops over s_out carrying [mem,...
Definition btensor.h:172
std::pair< DefVec, const Def * > pad_coords(Defs iters, const Def *lo, const Def *s_in, bool replicate)
The read coordinates o#d − lo#d of a pad into Idx s_in#d - clamped to the nearest edge if replicate,...
Definition btensor.h:111
const Def * concat_select(const Def *o_ax, const fe::Vector< u64 > &exts, auto read)
The concat cell at the i64 axis coordinate o_ax: read(i, loc) of the input i owning it - the highest ...
Definition btensor.h:152
const Def * op_cps2ds_dep(const Def *k)
Definition cps.h:16
plug::core::M M
Definition autogen.h:14
bool check_scatter_shape_constraints(const App *app, const Def *rank, const Def *dim, const Def *src_shape, const Def *idx_shape, const Def *updates_shape)
Checks statically decidable scatter constraints, blaming app for a violation.
bool check_gather_shape_constraints(const App *app, const Def *rank, const Def *dim, const Def *src_shape, const Def *idx_shape)
Checks statically decidable gather constraints, blaming app for a violation.
std::optional< fe::Vector< u64 > > lit_projs(const Def *def, u64 n)
The literal values of def's n projections, or nothing if one of them is not a literal.
Definition tensor.h:71
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
@ Arr
Definition def.h:125
@ App
Definition def.h:125
static MapRedPost of(const App *callee)
Definition btensor.h:25