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