MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
reassoc.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5
6#include <fe/format.h>
7
8#include <mim/def.h>
9#include <mim/plugin.h>
10#include <mim/tuple.h>
11
12#include <mim/util/types.h>
13
14#include <mim/plug/core/core.h>
15
17
19
20namespace {
21
22/// Beyond this the eager enumeration of `Catalan(n − 1)` bracketings is worth a warning.
23constexpr u64 Loud_max_dispatch = 8;
24
25/// @p n rounded up to a whole number of @p vec lanes; the tail iteration's idle lanes are charged.
26u64 lanes(u64 n, u64 vec) { return (n + vec - 1) / vec * vec; }
27
28/// Is @p mat read with its contraction unit-stride?
29/// `Lower` answers `tensor.fastest_axis` 0 for a transposed operand - the read-through absorbs the
30/// transpose into the access map - which selects `dot_schedule_kvec`.
31bool is_kvec(Leaf mat) { return mat.t != (isa_transpose_2d(mat.def) != nullptr); }
32
33/// Does `i … s … j` vectorize its contraction instead of its trailing extent?
34/// Only a leaf can: a subchain's result is materialized row-major.
35bool kvec_split(fe::View<Leaf> mats, u64 s, u64 j) { return s + 1 == j && is_kvec(mats[j]); }
36
37/// The symbolic extents of one `dims[i] · dims[s + 1] · dims[j + 1]` cost term, sorted by Def::gid and
38/// padded with `nullptr`; literal extents fold into the coefficient instead.
39using Mono = std::array<const Def*, 3>;
40
41/// A chain cost as a polynomial in the symbolic extents.
42class Poly {
43public:
44 void add(Mono mono, u64 coeff) {
45 for (auto& [m, c] : terms_)
46 if (m == mono) {
47 c += coeff;
48 return;
49 }
50 terms_.emplace_back(mono, coeff);
51 }
52
53 void add(const Poly& other) {
54 for (const auto& [m, c] : other.terms_)
55 add(m, c);
56 }
57
58 u64 coeff(Mono mono) const {
59 for (const auto& [m, c] : terms_)
60 if (m == mono) return c;
61 return 0;
62 }
63
64 /// Is `this` at most @p other under *every* instantiation of the symbolic extents?
65 /// Extents are non-negative, so a coefficient-wise `≤` is sufficient - but not necessary, which is
66 /// what makes this order partial.
67 bool dominates(const Poly& other) const {
68 for (const auto& [m, c] : terms_)
69 if (c > other.coeff(m)) return false;
70 return true;
71 }
72
73 std::string str() const {
74 if (terms_.empty()) return "0";
75 auto s = std::string();
76 for (const auto& [mono, coeff] : terms_) {
77 if (!s.empty()) s += " + ";
78 s += std::format("{}", coeff);
79 for (auto d : mono)
80 if (d) s += std::format("·{}", d);
81 }
82 return s;
83 }
84
85private:
86 fe::Vector<std::pair<Mono, u64>> terms_;
87};
88
89/// The cost of one product `«x, y» · «y, z»`, in vector-lane slots.
90/// The vector loop is @p z (`dot_schedule`) or, under @p kvec, the contraction @p y
91/// (`dot_schedule_kvec`); a literal extent pads to a whole number of @p vec lanes, a symbolic one is
92/// assumed to vectorize perfectly.
93Poly mul_cost(const Def* x, const Def* y, const Def* z, u64 vec, bool kvec) {
94 auto coeff = 1_u64;
95 auto syms = DefVec();
96 auto extent = [&](const Def* d, u64 pad) {
97 if (auto l = Lit::isa<u64>(d))
98 coeff *= lanes(*l, pad);
99 else
100 syms.emplace_back(d);
101 };
102
103 extent(x, 1);
104 extent(y, kvec ? vec : 1);
105 extent(z, kvec ? 1 : vec);
106 std::ranges::sort(syms, [](const Def* a, const Def* b) { return a->gid() < b->gid(); });
107
108 auto mono = Mono{};
109 std::ranges::copy(syms, mono.begin());
110
111 auto poly = Poly();
112 poly.add(mono, coeff);
113 return poly;
114}
115
116Poly cost_of(fe::View<Split> splits, fe::View<Leaf> mats, Defs dims, u64 vec) {
117 // An operand is read transposed iff its orientation differs from its consumer's; a subchain's is its node's.
118 auto kvec = [&](u64 i, u64 j, bool t) {
119 if (i == j) return is_kvec(mats[i]) != t;
120 return std::ranges::find_if(splits, [&](const Split& n) { return n.i == i && n.j == j; })->t != t;
121 };
122
123 auto poly = Poly();
124 for (auto [i, s, j, t] : splits)
125 poly.add(t ? mul_cost(dims[j + 1], dims[s + 1], dims[i], vec, kvec(i, s, t))
126 : mul_cost(dims[i], dims[s + 1], dims[j + 1], vec, kvec(s + 1, j, t)));
127 return poly;
128}
129
130/// Every bracketing of `lo … hi`.
131fe::Vector<Splits> bracketings(u64 lo, u64 hi) {
132 if (lo == hi) return {Splits()};
133
134 auto res = fe::Vector<Splits>();
135 for (auto s = lo; s != hi; ++s)
136 for (const auto& l : bracketings(lo, s))
137 for (const auto& r : bracketings(s + 1, hi)) {
138 auto b = l;
139 b.append_range(r);
140 b.emplace_back(Split{lo, s, hi});
141 res.emplace_back(std::move(b));
142 }
143 return res;
144}
145
146/// Drops every bracketing that another one provably beats; equal costs keep the first.
147/// A single survivor is hence the optimum under *every* instantiation of the symbolic extents.
148/// Several survivors need not each win for some instantiation - domination is only sufficient for `≤`.
149fe::Vector<Splits> pareto(fe::View<Splits> cands, fe::View<Leaf> mats, Defs dims, u64 vec) {
150 auto keep = fe::Vector<Splits>();
151 auto costs = fe::Vector<Poly>();
152
153 for (const auto& cand : cands) {
154 auto cost = cost_of(cand, mats, dims, vec);
155 if (std::ranges::any_of(costs, [&](const Poly& k) { return k.dominates(cost); })) continue;
156 for (auto i = costs.size(); i-- != 0;)
157 if (cost.dominates(costs[i])) keep.erase(keep.begin() + i), costs.erase(costs.begin() + i);
158 keep.emplace_back(cand);
159 costs.emplace_back(std::move(cost));
160 }
161
162 return keep;
163}
164
165fe::Vector<u64> split_table(const Splits& splits, u64 n) {
166 auto table = fe::Vector<u64>(n * n, 0);
167 for (auto [i, s, j, _] : splits)
168 table[i * n + j] = s;
169 return table;
170}
171
172/// Matrix-chain order: matrix `i` of the chain has shape `dims[i] × dims[i + 1]`.
173/// @returns the split table - `split[i * n + j]` is the last matrix of the left factor of the cheapest
174/// parenthesization of `i … j` - together with that parenthesization's cost, or nothing at all if some
175/// subchain has no candidate that provably beats all the others.
176std::optional<std::pair<fe::Vector<u64>, Poly>> matrix_chain_order(fe::View<Leaf> mats, Defs dims, u64 vec) {
177 auto n = dims.size() - 1;
178 auto cost = fe::Vector<Poly>(n * n);
179 auto split = fe::Vector<u64>(n * n, 0);
180
181 for (auto len = 2_u64; len <= n; ++len) {
182 for (auto i = 0_u64; i + len <= n; ++i) {
183 auto j = i + len - 1;
184 auto cands = fe::Vector<Poly>();
185 for (auto s = i; s != j; ++s) {
186 auto c = mul_cost(dims[i], dims[s + 1], dims[j + 1], vec, kvec_split(mats, s, j));
187 c.add(cost[i * n + s]);
188 c.add(cost[(s + 1) * n + j]);
189 cands.emplace_back(std::move(c));
190 }
191
192 auto best = std::ranges::find_if(cands, [&](const Poly& a) {
193 return std::ranges::all_of(cands, [&](const Poly& b) { return a.dominates(b); });
194 });
195 if (best == cands.end()) return {};
196
197 split[i * n + j] = i + (best - cands.begin());
198 cost[i * n + j] = std::move(*best);
199 }
200 }
201
202 return std::pair{std::move(split), std::move(cost[n - 1])};
203}
204
205} // namespace
206
208 if (auto n = driver().arg_nat("tensor", "reassoc-max")) {
209 max_dispatch_ = *n;
210 log().d("dispatch chains of up to {} matrices", *n);
211 if (*n > Loud_max_dispatch)
212 log().w("`-X tensor:reassoc-max={}` enumerates up to Catalan({}) bracketings", *n, *n - 1);
213 }
214
215 // Ask `tensor.vec_width`, so that the cost model charges the lanes the schedules actually use.
216 if (auto n = Lit::isa(old_world().call<tensor::vec_width>(old_world().lit_nat(Default_vec)))) vec_ = *n;
217 log().d("charge the vector loop in units of {} lanes", vec_);
218
219 // flatten() may only pull a product apart where doing so cannot leave it materialized for another
220 // consumer as well.
221 consumers_ = count_consumers(old_world(),
222 [](const Def* d) { return Axm::isa<tensor::product_2d>(d) || isa_transpose_2d(d); });
223
225}
226
227std::optional<Reassoc::Link> Reassoc::isa_link(const Def* def, const Def* ring) const {
228 auto app = Axm::isa<tensor::product_2d>(def);
229 if (!app) return {};
230
231 // The curry chain, outermost app first: [t1, t2] {m k l} [R].
232 auto groups = app->callee()->as<App>();
233 if (groups->callee()->as<App>()->arg() != ring) return {};
234
235 auto [m, k, l] = groups->args<3>();
236 return Link{app, m, k, l};
237}
238
239void Reassoc::flatten(const Def* def,
240 const Def* ring,
241 const Def* rows,
242 bool t,
243 Leaves& mats,
244 DefVec& dims,
245 Splits& orig) {
246 auto i = consumers_.find(def);
247 if (i != consumers_.end() && i->second == 1) {
248 // `(A · B)ᵀ = Bᵀ · Aᵀ`, so a transposed chain flattens reversed with each leaf transposed.
249 if (auto in = isa_transpose_2d(def)) return flatten(in, ring, rows, !t, mats, dims, orig);
250
251 if (auto link = isa_link(def, ring)) {
252 auto lo = mats.size();
253 auto [t1, t2] = link->app->args<2>();
254 if (t) std::swap(t1, t2);
255 flatten(t1, ring, t ? link->l : link->m, t, mats, dims, orig);
256 auto mid = mats.size();
257 flatten(t2, ring, link->k, t, mats, dims, orig);
258 orig.emplace_back(Split{lo, mid - 1, mats.size() - 1, t});
259 return;
260 }
261 }
262
263 mats.emplace_back(Leaf{def, t});
264 dims.emplace_back(rows);
265}
266
267const Def* Reassoc::build(const Def* head, fe::View<Leaf> mats, Defs dims, fe::View<u64> split, u64 i, u64 j) {
268 auto& w = new_world();
269 if (i == j) {
270 auto mat = rewrite(mats[i].def);
271 if (!mats[i].t) return mat;
272 auto T = head->as<App>()->arg()->proj(4, 0);
273 auto s = w.tuple({rewrite(dims[i + 1]), rewrite(dims[i])});
274 return w.app(w.app(w.app(w.annex<tensor::transpose_2d>(), T), s), mat);
275 }
276
277 auto s = split[i * mats.size() + j];
278 auto t1 = build(head, mats, dims, split, i, s);
279 auto t2 = build(head, mats, dims, split, s + 1, j);
280 auto mkl = DefVec{rewrite(dims[i]), rewrite(dims[s + 1]), rewrite(dims[j + 1])};
281 return w.app(w.app(head, mkl), {t1, t2});
282}
283
284const Def* Reassoc::cost_expr(fe::View<Leaf> mats, Defs dims, const Splits& splits) {
285 auto& w = new_world();
286 const Def* sum = nullptr;
287
288 // Pads as in mul_cost, so the run-time tournament ranks by the same cost model.
289 auto extent = [&](const Def* d, u64 pad) -> const Def* {
290 if (auto l = Lit::isa<u64>(d)) return w.lit_nat(lanes(*l, pad));
291 return rewrite(d);
292 };
293
294 for (auto [i, s, j, _] : splits) {
295 auto kvec = kvec_split(mats, s, j);
296 auto p = w.app(w.annex(core::nat::mul), {rewrite(dims[i]), extent(dims[s + 1], kvec ? vec_ : 1)});
297 p = w.app(w.annex(core::nat::mul), {p, extent(dims[j + 1], kvec ? 1 : vec_)});
298 sum = sum ? w.app(w.annex(core::nat::add), {sum, p}) : p;
299 }
300
301 return sum;
302}
303
304const Def*
305Reassoc::dispatch(const Def* head, const Def* res_ty, fe::View<Leaf> mats, Defs dims, fe::View<Splits> cands) {
306 auto& w = new_world();
307 auto n = mats.size();
308 auto pi = w.pi(w.sigma(), res_ty);
309
310 // Each bracketing goes behind a thunk so that only the selected one runs. The filter is `tt`, so once
311 // the comparison folds - a caller that knows the extents, `compile.lam_spec` - the winner inlines and
312 // the losers become unreachable.
313 const Def* best = nullptr;
314 const Def* best_cost = nullptr;
315 for (const auto& cand : cands) {
316 auto thunk = w.mut_lam(pi)->set(true, build(head, mats, dims, split_table(cand, n), 0, n - 1));
317 auto cost = cost_expr(mats, dims, cand);
318
319 if (!best) {
320 best = thunk, best_cost = cost;
321 } else {
322 auto cheaper = w.app(w.annex(core::ncmp::l), {cost, best_cost});
323 best = w.select(cheaper, thunk, best);
324 best_cost = w.select(cheaper, cost, best_cost);
325 }
326 }
327
328 return w.app(best, w.tuple());
329}
330
331const Def* Reassoc::reassoc(const App* app) {
332 auto head = app->callee()->as<App>()->callee()->as<App>();
333 auto link = isa_link(app, head->arg());
334 if (!link) return nullptr;
335
336 auto [t1, t2] = app->args<2>();
337 auto mats = Leaves();
338 auto dims = DefVec();
339 auto orig = Splits();
340 flatten(t1, head->arg(), link->m, false, mats, dims, orig);
341 auto mid = mats.size();
342 flatten(t2, head->arg(), link->k, false, mats, dims, orig);
343 dims.emplace_back(link->l);
344 orig.emplace_back(Split{0_u64, mid - 1, mats.size() - 1});
345
346 // Two matrices admit only one parenthesization.
347 auto n = mats.size();
348 if (n < 3) return nullptr;
349
350 auto orig_cost = cost_of(orig, mats, dims, vec_);
351
352 if (n <= max_dispatch_) {
353 auto cands = pareto(bracketings(0, n - 1), mats, dims, vec_);
354 if (cands.size() != 1) {
355 log().d("dispatch chain {} over {} bracketings, written as {}", fe::Join(dims, "×"), cands.size(),
356 orig_cost.str());
357 return dispatch(rewrite(head), rewrite(app->type()), mats, dims, cands);
358 }
359
360 auto cost = cost_of(cands.front(), mats, dims, vec_);
361 if (orig_cost.dominates(cost)) return nullptr;
362 log().d("reassociate chain {}: {} → {} lane slots", fe::Join(dims, "×"), orig_cost.str(), cost.str());
363 return build(rewrite(head), mats, dims, split_table(cands.front(), n), 0, n - 1);
364 }
365
366 auto order = matrix_chain_order(mats, dims, vec_);
367 if (!order) return nullptr;
368
369 auto& [split, cost] = *order;
370 if (orig_cost.dominates(cost)) return nullptr;
371
372 log().d("reassociate chain {}: {} → {} lane slots", fe::Join(dims, "×"), orig_cost.str(), cost.str());
373 return build(rewrite(head), mats, dims, split, 0, n - 1);
374}
375
376const Def* Reassoc::rewrite_imm_App(const App* app) {
378 if (auto res = reassoc(app)) return res;
379 return RWPhase::rewrite_imm_App(app);
380}
381
382} // namespace mim::plug::tensor::phase
auto args(Projector auto f) const noexcept
Definition lam.h:252
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
static std::optional< T > isa(const Def *def)
Definition def.h:976
const fe::Log & log() const
Definition phase.h:60
Driver & driver()
Definition phase.h:59
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
const Def * rewrite_imm_App(const App *) final
Definition reassoc.cpp:376
void start() override
Actual entry.
Definition reassoc.cpp:207
fe::Vector< Split > Splits
A bracketing of a matrix chain, innermost node first.
Definition reassoc.h:18
fe::Vector< Leaf > Leaves
Definition reassoc.h:26
A chain operand; the chain reads its transpose if t.
Definition reassoc.h:21
One node of a bracketing: i … j splits after s.
Definition reassoc.h:12
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
const Def * isa_transpose_2d(const Def *def)
The input of a 2-D transpose transpose (1, 0), or nullptr.
Definition tensor.h:93
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
@ App
Definition def.h:125