blob: a38ae3e93e5d8390c943855db2881649ef1850d3 [file] [edit]
//=- Explain.cpp - what a solve changed, considered and rejected -----------=//
//
// Part of the Poseidon Project, under the Apache License v2.0 with LLVM
// Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "Explain.h"
#include "llvm/ADT/MapVector.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Analysis/OptimizationRemarkEmitter.h"
#include "llvm/Demangle/Demangle.h"
#include "llvm/IR/DebugInfoMetadata.h"
#include "llvm/IR/DebugProgramInstruction.h"
#include "llvm/IR/DiagnosticInfo.h"
#include "llvm/IR/GetElementPtrTypeIterator.h"
#include "llvm/IR/InstIterator.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Operator.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include "CostModel.h"
#include "Flags.h"
#include "Herbie.h"
#include "Matmul.h"
#include "Optimize.h"
#include "Precision.h"
#include "Solvers.h"
#include "Types.h"
#include "Utils.h"
#include <cmath>
#include <map>
#include <optional>
#include <set>
using namespace llvm;
#if LLVM_VERSION_MAJOR >= 21
#define GET_COST(cost) ((int64_t)(cost).getValue())
#else
#define GET_COST(cost) ((int64_t)(cost).getValue().value())
#endif
namespace poseidon {
namespace {
constexpr const char *kPass = "poseidon";
constexpr size_t kMaxRejectedRemarks = 3;
constexpr size_t kRemarkExprChars = 160;
constexpr size_t kMaxListedOps = 6;
std::map<const Function *, DebugLoc> &siteCalls() {
static std::map<const Function *, DebugLoc> calls;
return calls;
}
struct SourceLoc {
std::string file;
unsigned line = 0, col = 0;
bool valid() const { return line != 0; }
};
SourceLoc locOf(const DebugLoc &DL) {
if (!DL)
return {};
return {DL->getFilename().str(), DL.getLine(), DL.getCol()};
}
SourceLoc locOf(const Value *V) {
if (auto *I = dyn_cast_or_null<Instruction>(V))
return locOf(I->getDebugLoc());
return {};
}
json::Object locJSON(const SourceLoc &L) {
json::Object o;
o["file"] = L.file;
o["line"] = (int64_t)L.line;
o["col"] = (int64_t)L.col;
return o;
}
void putLoc(json::Object &o, StringRef key, const SourceLoc &L) {
if (L.valid())
o[key] = locJSON(L);
}
void putNum(json::Object &o, StringRef key, double v) {
if (std::isfinite(v))
o[key] = v;
}
std::string num(double v, int digits = 2) {
if (!std::isfinite(v))
return "n/a";
return formatv("{0}", format("%.*g", digits, v)).str();
}
std::string costStr(int64_t c) { return std::to_string(c); }
std::string pctStr(int64_t before, int64_t after) {
if (before == 0)
return "";
double pct = 100.0 * (double)(after - before) / (double)before;
return formatv(" ({0}{1}%)", pct >= 0 ? "+" : "", format("%.0f", pct)).str();
}
std::string clip(const std::string &s, size_t n = kRemarkExprChars) {
if (s.size() <= n)
return s;
return s.substr(0, n) + "...";
}
std::string typeName(Type *T) {
if (T->isFloatTy())
return "float";
if (T->isDoubleTy())
return "double";
if (T->isHalfTy())
return "_Float16";
if (T->isBFloatTy())
return "__bf16";
if (T->isX86_FP80Ty())
return "long double";
if (T->isFP128Ty())
return "__float128";
std::string s;
raw_string_ostream os(s);
T->print(os);
return s;
}
std::string dtypeOf(PrecisionChangeType t) {
switch (t) {
case PrecisionChangeType::BF16:
return "bf16";
case PrecisionChangeType::FP16:
return "f16";
case PrecisionChangeType::FP32:
return "f32";
case PrecisionChangeType::FP64:
return "f64";
case PrecisionChangeType::FP80:
return "f80";
case PrecisionChangeType::FP128:
return "f128";
default:
return "";
}
}
std::string precisionWords(PrecisionChangeType t) {
if (unsigned k = expansionComponents(t))
return formatv("a {0}-component float expansion", k).str();
switch (t) {
case PrecisionChangeType::BF16:
return "bfloat16";
case PrecisionChangeType::FP16:
return "half";
case PrecisionChangeType::FP32:
return "float";
case PrecisionChangeType::FP64:
return "double";
case PrecisionChangeType::FP80:
return "long double";
case PrecisionChangeType::FP128:
return "quad";
default:
return getPrecisionChangeTypeString(t).str();
}
}
std::string opWord(StringRef op) {
if (op == "+" || op == "-" || op == "*" || op == "/")
return ("'" + op + "'").str();
if (op == "neg")
return "negation";
return op.str();
}
std::string kindWords(StringRef kind) {
if (kind == "algebraic")
return "the rewrite";
if (kind == "precision")
return "the precision change";
if (kind == "expansion")
return "the float expansion";
return "the product candidate";
}
std::string joinWords(ArrayRef<std::string> items) {
std::string out;
for (size_t k = 0; k < items.size(); ++k) {
if (k)
out += k + 1 == items.size() ? " and " : ", ";
out += items[k];
}
return out;
}
struct Linear {
SmallVector<std::pair<std::string, int64_t>, 2> terms;
int64_t c = 0;
bool ok = true;
Linear &add(const Linear &o, int64_t scale) {
ok = ok && o.ok;
c += scale * o.c;
for (const auto &term : o.terms) {
auto it = llvm::find_if(
terms, [&](const auto &t) { return t.first == term.first; });
if (it == terms.end())
terms.push_back({term.first, scale * term.second});
else
it->second += scale * term.second;
}
llvm::erase_if(terms, [](auto &t) { return t.second == 0; });
return *this;
}
bool divide(int64_t d) {
if (d <= 0 || c % d)
return false;
for (auto &t : terms)
if (t.second % d)
return false;
c /= d;
for (auto &t : terms)
t.second /= d;
return true;
}
std::string str() const {
std::string s;
for (const auto &[name, coeff] : terms) {
int64_t a = coeff < 0 ? -coeff : coeff;
std::string term = a == 1 ? name : std::to_string(a) + " * " + name;
if (s.empty())
s = coeff < 0 ? "-" + term : term;
else
s += (coeff < 0 ? " - " : " + ") + term;
}
if (s.empty())
return std::to_string(c);
if (c > 0)
s += " + " + std::to_string(c);
else if (c < 0)
s += " - " + std::to_string(-c);
return s;
}
};
class ValueNamer {
const DataLayout &DL;
DenseMap<const Value *, const DILocalVariable *> exact, any;
DenseMap<const DILocalVariable *, SmallPtrSet<const Value *, 2>> bound;
std::string anyName(const Value *V) const {
auto it = any.find(V);
return it == any.end() ? "" : it->second->getName().str();
}
Linear linear(Value *V, unsigned depth = 0) const {
Linear L;
if (depth > 8) {
L.ok = false;
return L;
}
if (auto *CI = dyn_cast<ConstantInt>(V)) {
L.c = CI->getSExtValue();
return L;
}
if (isa<SExtInst>(V) || isa<ZExtInst>(V) || isa<TruncInst>(V))
return linear(cast<Instruction>(V)->getOperand(0), depth + 1);
if (auto *BO = dyn_cast<BinaryOperator>(V)) {
Value *A = BO->getOperand(0), *B = BO->getOperand(1);
auto *CA = dyn_cast<ConstantInt>(A), *CB = dyn_cast<ConstantInt>(B);
switch (BO->getOpcode()) {
case Instruction::Add:
return linear(A, depth + 1).add(linear(B, depth + 1), 1);
case Instruction::Sub:
return linear(A, depth + 1).add(linear(B, depth + 1), -1);
case Instruction::Or:
if (cast<PossiblyDisjointInst>(BO)->isDisjoint())
return linear(A, depth + 1).add(linear(B, depth + 1), 1);
break;
case Instruction::Mul:
if (CB)
return Linear().add(linear(A, depth + 1), CB->getSExtValue());
if (CA)
return Linear().add(linear(B, depth + 1), CA->getSExtValue());
break;
case Instruction::Shl:
if (CB && CB->getZExtValue() < 62)
return Linear().add(linear(A, depth + 1),
(int64_t)1 << CB->getZExtValue());
break;
default:
break;
}
}
std::string name = anyName(V);
if (name.empty())
L.ok = false;
else
L.terms.push_back({name, 1});
return L;
}
std::string baseName(const Value *V) const {
if (auto *GV = dyn_cast<GlobalVariable>(V)) {
SmallVector<DIGlobalVariableExpression *, 1> gves;
GV->getDebugInfo(gves);
for (auto *gve : gves)
if (auto *var = gve->getVariable())
return var->getName().str();
return GV->getName().str();
}
return anyName(V);
}
public:
explicit ValueNamer(Function &F) : DL(F.getParent()->getDataLayout()) {
for (Instruction &I : instructions(F))
for (DbgVariableRecord &DVR : filterDbgVars(I.getDbgRecordRange())) {
if (!DVR.isDbgValue() || DVR.hasArgList() || DVR.isKillLocation())
continue;
Value *V = DVR.getVariableLocationOp(0);
const DILocalVariable *var = DVR.getVariable();
if (!V || !var || var->getName().empty())
continue;
bound[var].insert(V);
any.try_emplace(V, var);
if (DVR.getExpression()->getNumElements() == 0)
exact.try_emplace(V, var);
}
}
std::string element(Value *ptr, uint64_t elemSize) const {
Linear bytes;
Value *base = ptr;
while (auto *GEP = dyn_cast<GEPOperator>(base)) {
for (gep_type_iterator GTI = gep_type_begin(GEP), E = gep_type_end(GEP);
GTI != E; ++GTI) {
if (GTI.getStructTypeOrNull())
return "";
int64_t stride =
(int64_t)GTI.getSequentialElementStride(DL).getFixedValue();
bytes.add(linear(GTI.getOperand()), stride);
if (!bytes.ok)
return "";
}
base = GEP->getPointerOperand();
}
if (base == ptr || !bytes.divide((int64_t)elemSize))
return "";
std::string b = baseName(base);
if (b.empty())
return "";
return b + "[" + bytes.str() + "]";
}
std::string name(Value *V) const {
if (auto it = exact.find(V); it != exact.end()) {
auto b = bound.find(it->second);
if (b != bound.end() && b->second.size() == 1)
return it->second->getName().str();
}
if (auto *LI = dyn_cast<LoadInst>(V)) {
std::string s =
element(LI->getPointerOperand(), DL.getTypeStoreSize(LI->getType()));
if (!s.empty())
return s;
}
if (isa<Argument>(V) || isa<GlobalVariable>(V))
return baseName(V);
return "";
}
std::string outputName(Instruction *O) const {
for (User *U : O->users())
if (auto *SI = dyn_cast<StoreInst>(U))
if (SI->getValueOperand() == O) {
std::string s = element(SI->getPointerOperand(),
DL.getTypeStoreSize(O->getType()));
if (!s.empty())
return s;
}
return name(O);
}
};
bool isPolynomial(StringRef expr) {
static const StringSet<> arith = {"+", "-", "*", "/", "neg",
"fma", "pow", "literal", "approx"};
bool head = false;
size_t i = 0;
while (i < expr.size()) {
char c = expr[i];
if (c == '(' || expr.substr(i).starts_with("#s(")) {
i += c == '(' ? 1 : 3;
head = true;
continue;
}
if (c == ')' || std::isspace((unsigned char)c)) {
++i;
continue;
}
size_t j = i;
while (j < expr.size() && !std::isspace((unsigned char)expr[j]) &&
expr[j] != '(' && expr[j] != ')')
++j;
if (head && !arith.contains(expr.substr(i, j - i).split('.').first))
return false;
head = false;
i = j;
}
return true;
}
const Instruction *firstInIROrder(Function &F,
const SmallPtrSetImpl<const Value *> &set) {
for (Instruction &I : instructions(F))
if (set.contains(&I))
return &I;
return nullptr;
}
struct Alt {
std::string id, kind, summary, fpcore, precision, status, reason;
SmallVector<std::string, 1> c;
SmallVector<unsigned, 1> outputs;
bool approx = false, series = false;
std::string approxNote;
SmallVector<std::string, 2> approxRanges;
std::optional<int64_t> costDelta;
std::optional<double> accDelta, err, domainError;
std::optional<double> bitsBefore, bitsAfter;
json::Array changes;
SmallVector<std::string, 4> changedOps;
std::string changedTo;
const Instruction *at = nullptr;
const void *item = nullptr;
size_t index = 0;
int rank = 9;
double baseAcc = std::numeric_limits<double>::quiet_NaN();
std::optional<int64_t> baseCost;
};
struct Unit;
std::string exprWords(const Unit &U);
struct Unit {
std::string id, kind;
const Subgraph *sg = nullptr;
SourceLoc loc;
const Instruction *root = nullptr;
json::Array ops, inputs, outputs, herbie;
SmallVector<std::string, 2> outC;
SmallVector<std::string, 2> inputRanges;
std::vector<Alt> alts;
std::optional<int64_t> baseCost;
unsigned executions = 0;
double sensitivity = std::numeric_limits<double>::quiet_NaN();
std::string left, shape;
bool applied = false;
};
std::string exprWords(const Unit &U) {
if (!U.outC.empty() && !U.outC[0].empty())
return clip(U.outC[0]);
return formatv("the subgraph of {0} operations at {1}:{2}", U.ops.size(),
U.loc.line, U.loc.col)
.str();
}
class Explainer {
Function &F;
FunctionFPState &st;
ArrayRef<SolutionStep> steps;
const SolveRecord &rec;
ValueNamer namer;
std::unordered_map<Value *, std::shared_ptr<FPNode>> nodes;
std::unordered_map<std::string, Value *> symbols;
std::set<std::pair<const void *, size_t>> applied, skipped, onPareto,
inSelected;
std::map<std::pair<const void *, size_t>, int64_t> firstBudget;
std::vector<Unit> units;
double A0, S, tau;
static std::pair<const void *, size_t> keyOf(const SolutionStep &step) {
return {std::visit([](auto *p) -> const void * { return p; }, step.item),
step.candidateIndex};
}
std::string stepId(const SolutionStep &step) const {
std::string id;
std::visit(
[&](auto *item) {
using T = std::decay_t<decltype(*item)>;
if constexpr (std::is_same_v<T, CandidateOutput>) {
for (size_t i = 0; i < st.COs.size(); ++i)
if (&st.COs[i] == item)
id = formatv("R{0}_{1}", i, step.candidateIndex).str();
} else if constexpr (std::is_same_v<T, CandidateSubgraph>) {
for (size_t i = 0; i < st.CSs.size(); ++i)
if (&st.CSs[i] == item)
id = formatv("PT{0}_{1}", i, step.candidateIndex).str();
} else {
for (size_t i = 0; i < st.CMs.size(); ++i)
if (&st.CMs[i] == item)
id = formatv("M{0}_{1}", i, step.candidateIndex).str();
}
},
step.item);
return id;
}
double relErr(double acc) const {
if (!(S > 0.0) || !std::isfinite(A0) || !std::isfinite(acc))
return std::numeric_limits<double>::quiet_NaN();
return (A0 + acc) / S;
}
double share(double acc) const {
if (!(S > 0.0) || !std::isfinite(acc))
return std::numeric_limits<double>::quiet_NaN();
return acc / S;
}
FPNode *nodeOf(Value *V) const {
auto it = st.valueToNodeMap.find(V);
return it == st.valueToNodeMap.end() ? nullptr : it->second.get();
}
CRendering rendering(const Subgraph &sg) {
CRendering how;
how.inputs = &sg.inputs;
how.leafName = [this](Value *V) { return namer.name(V); };
return how;
}
std::string rangeOf(Value *V, const std::string &label) {
auto it = st.valueToNodeMap.find(V);
if (it == st.valueToNodeMap.end() || !it->second)
return "";
double lo = it->second->getLowerBound(), hi = it->second->getUpperBound();
if (!std::isfinite(lo) || !std::isfinite(hi) || lo > hi)
return "";
int digits = 3;
while (lo != hi && digits < 17 && num(lo, digits) == num(hi, digits))
++digits;
return label + " in [" + num(lo, digits) + ", " + num(hi, digits) + "]";
}
std::string leafLabel(Value *V) {
std::string n = namer.name(V);
if (!n.empty())
return n;
auto it = st.valueToNodeMap.find(V);
if (it != st.valueToNodeMap.end() && it->second &&
!it->second->symbol.empty())
return it->second->symbol;
return "";
}
void markSteps();
void buildSubgraph(size_t s);
void buildMatmul(size_t m);
void addCOAlternatives(Unit &U, const Subgraph &sg);
void addCSAlternatives(Unit &U, const Subgraph &sg);
void judge(Unit &U);
std::string leftReason(const Unit &U, const Subgraph *sg);
std::optional<int64_t> appliedDelta() const;
public:
Explainer(Function &F, FunctionFPState &st, ArrayRef<SolutionStep> steps,
const SolveRecord &rec)
: F(F), st(st), steps(steps), rec(rec), namer(F),
nodes(st.valueToNodeMap), symbols(st.symbolToValueMap),
A0(rec.baseline), S(rec.accScale), tau(rec.tau) {}
void build();
void emitRemarks();
json::Object toJSON();
};
void Explainer::markSteps() {
SmallPtrSet<Value *, 8> rewritten;
for (const SolutionStep &step : steps) {
if (auto *const *co = std::get_if<CandidateOutput *>(&step.item)) {
if (llvm::any_of((*co)->roots,
[&](Value *r) { return rewritten.contains(r); })) {
skipped.insert(keyOf(step));
continue;
}
rewritten.insert((*co)->roots.begin(), (*co)->roots.end());
}
applied.insert(keyOf(step));
}
for (const auto &[cost, sol] : rec.solutions)
for (const SolutionStep &step : sol) {
onPareto.insert(keyOf(step));
firstBudget.try_emplace(keyOf(step), GET_COST(cost));
}
if (rec.selected) {
auto it = rec.solutions.find(InstructionCost(*rec.selected));
if (it != rec.solutions.end())
for (const SolutionStep &step : it->second)
inSelected.insert(keyOf(step));
}
}
void Explainer::addCOAlternatives(Unit &U, const Subgraph &sg) {
CRendering how = rendering(sg);
for (size_t i = 0; i < st.COs.size(); ++i) {
CandidateOutput &CO = st.COs[i];
if (CO.subgraph != &sg)
continue;
json::Object core;
core["id"] = formatv("R{0}", i).str();
if (!CO.herbiePrecision.empty())
core["precision"] = CO.herbiePrecision;
if (!CO.herbieStatus.empty())
core["status"] = CO.herbieStatus;
core["candidates"] = (int64_t)CO.candidates.size();
if (!CO.unpricedReason.empty())
core["unpriced"] = CO.unpricedReason;
if (std::isfinite(CO.herbieBits) && std::isfinite(CO.initialHerbieAccuracy))
core["error_bits"] = (1.0 - CO.initialHerbieAccuracy) * CO.herbieBits;
U.herbie.push_back(std::move(core));
SmallVector<unsigned, 1> outs;
for (Value *root : CO.roots)
for (unsigned k = 0; k < sg.outputs.size(); ++k)
if (sg.outputs[k] == root)
outs.push_back(k);
std::string rdt = dtypeOfType(CO.oldOutput->getType());
std::optional<int64_t> base;
if (std::isfinite(CO.initialCompCost))
base = (int64_t)std::llround(CO.initialCompCost * (double)CO.executions);
auto fill = [&](Alt &A, const std::string &expr) {
A.kind = "algebraic";
A.precision = CO.herbiePrecision;
A.fpcore = expr;
A.outputs = outs;
A.at = cast<Instruction>(CO.oldOutput);
A.baseAcc = CO.initialAccCost;
A.baseCost = base;
SmallVector<std::pair<std::string, std::string>, 2> specs;
A.c = fpcoreToC(expr, nodes, symbols, how, rdt, &specs);
SmallVector<std::string, 2> notes;
std::set<std::string> seen;
for (const auto &[spec, impl] : specs) {
if (!seen.insert(spec).second)
continue;
SmallVector<std::string, 1> specC =
fpcoreToC(spec, nodes, symbols, how, "");
std::string specText = clip(specC.empty() ? spec : specC[0], 120);
if (!isPolynomial(impl)) {
SmallVector<std::string, 1> implC =
fpcoreToC(impl, nodes, symbols, how, "");
notes.push_back(clip(implC.empty() ? impl : implC[0], 120) +
" in place of " + specText);
continue;
}
A.series = true;
notes.push_back("series approximation of " + specText);
SmallSet<std::string, 8> args;
getUniqueArgs(spec, args);
for (const std::string &arg : args) {
auto sv = symbols.find(arg);
if (sv == symbols.end() || !sv->second)
continue;
std::string label = leafLabel(sv->second);
std::string r = rangeOf(sv->second, label.empty() ? arg : label);
if (!r.empty() && !llvm::is_contained(A.approxRanges, r))
A.approxRanges.push_back(r);
}
}
A.approx = !specs.empty();
if (StringRef(expr).contains("(if"))
notes.push_back("regime split");
A.approxNote = joinWords(notes);
std::string what = "Herbie rewrite";
if (!A.precision.empty())
what += " searched at " + A.precision;
if (A.series)
what += ", series approximation";
else if (A.approx)
what += ", approximation";
if (StringRef(expr).contains("(if"))
what += ", regime split";
A.summary = what;
};
for (size_t j = 0; j < CO.candidates.size(); ++j) {
const RewriteCandidate &cand = CO.candidates[j];
Alt A;
A.id = formatv("R{0}_{1}", i, j).str();
A.item = &CO;
A.index = j;
fill(A, cand.expr);
if (cand.CompCost != std::numeric_limits<double>::max())
A.costDelta = GET_COST(CO.getCompCostDelta(j));
double acc = CO.getAccCostDelta(j);
if (std::isfinite(acc)) {
A.accDelta = acc;
double e = relErr(acc);
if (std::isfinite(e))
A.err = e;
}
if (std::isfinite(CO.herbieBits)) {
if (std::isfinite(CO.initialHerbieAccuracy))
A.bitsBefore = (1.0 - CO.initialHerbieAccuracy) * CO.herbieBits;
if (std::isfinite(cand.herbieAccuracy))
A.bitsAfter = (1.0 - cand.herbieAccuracy) * CO.herbieBits;
}
U.alts.push_back(std::move(A));
}
for (const auto &[expr, why] : CO.dropped) {
Alt A;
fill(A, expr);
A.status = "rejected";
A.reason = "the accuracy model dropped it: " + why;
A.rank = 4;
U.alts.push_back(std::move(A));
}
}
}
void Explainer::addCSAlternatives(Unit &U, const Subgraph &sg) {
CRendering how = rendering(sg);
DenseMap<const FPNode *, unsigned> opIndex;
unsigned k = 0;
for (Instruction &I : instructions(F))
if (sg.operations.contains(&I)) {
if (FPNode *N = nodeOf(&I))
opIndex[N] = k;
++k;
}
for (size_t i = 0; i < st.CSs.size(); ++i) {
CandidateSubgraph &CS = st.CSs[i];
if (CS.subgraph != &sg)
continue;
auto describe = [&](Alt &A, PTCandidate &cand) {
bool expansion = false;
DenseMap<const FPNode *, std::string> dtypes;
SmallPtrSet<const Value *, 8> changedInsts;
SmallVector<std::string, 4> parts;
for (const PrecisionChange &ch : cand.changes) {
expansion |= expansionComponents(ch.newType) != 0;
json::Object c;
c["from"] = getPrecisionChangeTypeString(ch.oldType).str();
c["to"] = getPrecisionChangeTypeString(ch.newType).str();
json::Array idx;
SmallVector<std::string, 4> names;
MapVector<std::string, unsigned, std::map<std::string, unsigned>>
counts;
for (FPLLValue *node : ch.nodes) {
auto it = opIndex.find(node);
if (it != opIndex.end())
idx.push_back((int64_t)it->second);
changedInsts.insert(node->value);
SourceLoc L = locOf(node->value);
std::string w = opWord(node->op);
++counts[w];
if (L.valid())
w += formatv(" ({0}:{1})", L.line, L.col).str();
names.push_back(w);
A.changedOps.push_back(w);
std::string dt = dtypeOf(ch.newType);
if (!dt.empty())
dtypes[node] = dt;
}
c["ops"] = std::move(idx);
A.changes.push_back(std::move(c));
if (names.size() > kMaxListedOps) {
names.clear();
for (const auto &[w, n] : counts)
names.push_back(n == 1 ? w : formatv("{0} x {1}", n, w).str());
}
parts.push_back(joinWords(names) + " in " + precisionWords(ch.newType));
A.changedTo = precisionWords(ch.newType);
}
A.kind = expansion ? "expansion" : "precision";
A.summary = cand.desc + ": " + joinWords(parts) + " (" +
std::to_string(A.changedOps.size()) + " of " +
std::to_string(sg.operations.size()) + " operations)";
if (A.changedOps.size() == sg.operations.size() &&
cand.changes.size() == 1)
A.changedOps = {
formatv("all {0} operations", sg.operations.size()).str()};
else if (A.changedOps.size() > kMaxListedOps)
A.changedOps = {formatv("{0} of the {1} operations",
A.changedOps.size(), sg.operations.size())
.str()};
A.at = firstInIROrder(F, changedInsts);
if (!A.at)
A.at = sg.outputs[0];
if (!expansion) {
CRendering typed = how;
typed.dtypes = &dtypes;
for (Instruction *O : sg.outputs)
if (FPNode *N = nodeOf(O))
A.c.push_back(renderC(*N, typed, dtypeOfType(O->getType())));
}
A.baseAcc = CS.initialAccCost;
A.baseCost = U.baseCost;
};
for (size_t j = 0; j < CS.candidates.size(); ++j) {
PTCandidate &cand = CS.candidates[j];
Alt A;
A.id = formatv("PT{0}_{1}", i, j).str();
A.item = &CS;
A.index = j;
describe(A, cand);
for (unsigned o = 0; o < sg.outputs.size(); ++o)
A.outputs.push_back(o);
std::optional<int64_t> cost;
if (cand.CompCost != std::numeric_limits<double>::max())
cost = GET_COST(CS.getCompCostDelta(j));
double acc = CS.getAccCostDelta(j);
CandidateSubgraph::CandidateOutputSet cos;
for (const auto &[item, idx] : applied)
for (CandidateOutput &CO : st.COs)
if (&CO == item && CO.subgraph == &sg)
cos.insert(&CO);
if (applied.count({&CS, j}) && !cos.empty()) {
CandidateSubgraph::CacheKey key{j, cos};
auto c = CS.compCostDeltaCache.find(key);
cost = c == CS.compCostDeltaCache.end()
? std::nullopt
: std::optional<int64_t>(GET_COST(c->second));
auto a = CS.accCostDeltaCache.find(key);
acc = a == CS.accCostDeltaCache.end()
? std::numeric_limits<double>::quiet_NaN()
: a->second;
}
A.costDelta = cost;
if (std::isfinite(acc)) {
A.accDelta = acc;
double e = relErr(acc);
if (std::isfinite(e))
A.err = e;
}
U.alts.push_back(std::move(A));
}
for (const auto &[desc, why] : CS.dropped) {
Alt A;
A.kind = "precision";
A.summary = desc;
A.at = sg.outputs[0];
A.status = "rejected";
A.reason = "the accuracy model dropped it: " + why;
A.rank = 4;
U.alts.push_back(std::move(A));
}
}
}
void Explainer::judge(Unit &U) {
for (Alt &A : U.alts) {
if (!A.item)
continue;
auto key = std::make_pair(A.item, A.index);
if (applied.count(key)) {
A.status = "applied";
A.rank = -1;
U.applied = true;
if (rec.selector == "manual")
A.reason = "selected by -poseidon-apply-rewrites";
else if (A.kind == "matmul" && rec.selector == "error-budget")
A.reason = "the cheapest product candidate that meets the target";
else if (tau > 0.0)
A.reason = "part of the cheapest solution that meets the target";
else
A.reason = "part of the most accurate solution within the cost budget";
continue;
}
A.status = "rejected";
if (skipped.count(key)) {
A.reason = "another selected rewrite of the same output was applied "
"first";
A.rank = 3;
continue;
}
if (inSelected.count(key)) {
A.reason = "it edits instructions a selected matrix-product raise "
"consumes";
A.rank = 3;
continue;
}
if (A.kind == "matmul" && rec.selector == "error-budget") {
auto *CM = static_cast<const CandidateMatmul *>(A.item);
const CandidateMatmul::Option &o = CM->candidates[A.index];
if (tau > 0.0 && o.domainError > tau) {
A.reason = "modelled domain error " + num(o.domainError) +
" is above the target " + num(tau);
A.rank = A.costDelta && *A.costDelta < 0 ? 0 : 2;
} else if (o.compCost >= CM->initialCompCost) {
A.reason = "not cheaper than the product as written";
A.rank = 3;
} else {
A.reason = "a cheaper candidate also meets the target";
A.rank = 1;
}
continue;
}
if (tau > 0.0 && A.err && *A.err > tau) {
A.reason =
"modelled error " + num(*A.err) + " is above the target " + num(tau);
A.rank = A.costDelta && *A.costDelta < 0 ? 0 : 2;
continue;
}
if (A.costDelta && A.accDelta && *A.costDelta >= 0 && *A.accDelta >= 0) {
A.reason = "neither cheaper nor more accurate than as written";
A.rank = 3;
continue;
}
if (onPareto.count(key)) {
A.status = "pareto";
A.reason = "on the Pareto frontier";
auto b = firstBudget.find(key);
auto sol = b == firstBudget.end()
? rec.solutions.end()
: rec.solutions.find(InstructionCost(b->second));
if (sol != rec.solutions.end()) {
std::string ids;
for (const SolutionStep &step : sol->second)
ids += (ids.empty() ? "" : ",") + stepId(step);
A.reason += "; -poseidon-apply-rewrites=" + ids + " applies it";
}
A.rank = 1;
continue;
}
const Alt *dom = nullptr;
auto better = [&](const Alt &B, const Alt &D) {
bool b = applied.count({B.item, B.index}),
d = applied.count({D.item, D.index});
return b != d ? b : *B.costDelta < *D.costDelta;
};
if (A.costDelta && A.accDelta)
for (const Alt &B : U.alts)
if (&B != &A && B.item && B.costDelta && B.accDelta &&
*B.costDelta <= *A.costDelta && *B.accDelta <= *A.accDelta &&
(*B.costDelta < *A.costDelta || *B.accDelta < *A.accDelta) &&
(!dom || better(B, *dom)))
dom = &B;
if (dom) {
A.reason = "dominated by " + dom->id +
", which costs no more and is at least as accurate";
A.rank = 3;
continue;
}
const Alt *rival = nullptr;
for (const Alt &B : U.alts)
if (&B != &A && B.item && applied.count({B.item, B.index}) &&
B.kind == A.kind && B.outputs == A.outputs)
rival = &B;
if (rival) {
A.reason = A.kind == "algebraic"
? "the selected " + rival->id + " rewrites the same output"
: "the subgraph takes one precision assignment, and " +
rival->id + " was selected";
A.rank = 3;
continue;
}
if (tau == 0.0 && A.costDelta && *A.costDelta > flags::CompCostBudget) {
A.reason = "its cost change " + costStr(*A.costDelta) +
" is above the budget " + costStr(flags::CompCostBudget);
A.rank = 2;
continue;
}
A.reason = "not on the Pareto frontier of the site";
A.rank = A.accDelta && *A.accDelta < 0 ? 2 : 3;
}
}
std::string Explainer::leftReason(const Unit &U, const Subgraph *sg) {
if (U.applied)
return "";
bool any = llvm::any_of(U.alts, [](const Alt &A) { return A.item; });
if (!any) {
SmallVector<std::string, 2> why;
if (sg) {
bool hadCO = false;
for (const CandidateOutput &CO : st.COs)
if (CO.subgraph == sg) {
hadCO = true;
if (!CO.unpricedReason.empty())
why.push_back("its candidates could not be priced: " +
CO.unpricedReason);
else if (!CO.herbieStatus.empty() && CO.candidates.empty())
why.push_back("Herbie returned no alternative (" + CO.herbieStatus +
")");
}
if (!hadCO) {
if (!flags::EnableHerbie)
why.push_back("the algebraic search is disabled");
else
for (Instruction *O : sg->outputs) {
auto it = st.valueToNodeMap.find(O);
if (it != st.valueToNodeMap.end() && it->second &&
it->second->grad == 0.0) {
why.push_back("the profiled gradient of its output is zero");
break;
}
}
}
}
if (why.empty())
why.push_back("no candidate was generated");
std::sort(why.begin(), why.end());
why.erase(std::unique(why.begin(), why.end()), why.end());
return joinWords(why);
}
if (tau > 0.0) {
const Alt *best = nullptr;
for (const Alt &A : U.alts)
if (A.item && A.err && (!best || *A.err < *best->err))
best = &A;
bool anyMeets = llvm::any_of(
U.alts, [&](const Alt &A) { return A.item && A.err && *A.err <= tau; });
if (!anyMeets && best)
return "every alternative's modelled error is above the target " +
num(tau) + " (best: " + best->id + ", " + num(*best->err) + ")";
bool cheaperMeets = llvm::any_of(U.alts, [&](const Alt &A) {
return A.item && A.err && *A.err <= tau && A.costDelta &&
*A.costDelta < 0;
});
if (!cheaperMeets)
return "no alternative that meets the target " + num(tau) +
" is cheaper than as written";
if (!rec.selected && steps.empty())
return "no solution on the site's frontier meets the target " + num(tau);
return "its alternatives do not fit the target together with the "
"selected rewrites";
}
return "no alternative lowers the modelled error within the cost budget " +
costStr(flags::CompCostBudget);
}
void Explainer::buildSubgraph(size_t s) {
Subgraph &sg = st.subgraphs[s];
Unit U;
U.id = formatv("S{0}", s).str();
U.kind = "subgraph";
U.sg = &sg;
U.root = sg.outputs.empty() ? nullptr : sg.outputs[0];
U.loc = locOf(U.root);
CRendering how = rendering(sg);
DenseMap<const Value *, unsigned> opIdx, inIdx;
for (Instruction &I : instructions(F)) {
if (!sg.operations.contains(&I))
continue;
opIdx[&I] = U.ops.size();
json::Object op;
auto it = st.valueToNodeMap.find(&I);
FPNode *N = it == st.valueToNodeMap.end() ? nullptr : it->second.get();
if (N)
op["opcode"] = N->op;
op["type"] = typeName(I.getType());
putLoc(op, "loc", locOf(&I));
if (N) {
op["executions"] = (int64_t)N->executions;
putNum(op, "sensitivity", N->sens);
putNum(op, "gradient", N->grad);
double lo = N->getLowerBound(), hi = N->getUpperBound();
if (std::isfinite(lo) && std::isfinite(hi) && lo <= hi) {
op["min"] = lo;
op["max"] = hi;
}
}
U.ops.push_back(std::move(op));
}
for (Value *V : sg.inputs) {
inIdx[V] = U.inputs.size();
json::Object in;
auto it = st.valueToNodeMap.find(V);
FPNode *N = it == st.valueToNodeMap.end() ? nullptr : it->second.get();
if (N && !N->symbol.empty())
in["var"] = N->symbol;
std::string name = namer.name(V);
if (!name.empty())
in["name"] = name;
putLoc(in, "loc", locOf(V));
in["type"] = typeName(V->getType());
if (N) {
double lo = N->getLowerBound(), hi = N->getUpperBound();
if (std::isfinite(lo) && std::isfinite(hi) && lo <= hi) {
in["min"] = lo;
in["max"] = hi;
std::string label = leafLabel(V);
if (!label.empty())
U.inputRanges.push_back(rangeOf(V, label));
}
}
U.inputs.push_back(std::move(in));
}
for (Instruction *O : sg.outputs) {
json::Object out;
std::string name = namer.outputName(O);
if (!name.empty())
out["name"] = name;
putLoc(out, "loc", locOf(O));
out["type"] = typeName(O->getType());
for (const CandidateOutput &CO : st.COs)
if (CO.subgraph == &sg && !CO.isArray() && CO.oldOutput == O) {
out["fpcore"] = CO.expr;
break;
}
FPNode *ON = nodeOf(O);
SmallVector<FPNode *, 16> todo;
SmallPtrSet<FPNode *, 16> seen;
std::set<unsigned> coneOps, coneIns;
if (ON)
todo.push_back(ON);
while (!todo.empty()) {
FPNode *N = todo.pop_back_val();
if (!seen.insert(N).second)
continue;
auto *LL = dyn_cast<FPLLValue>(N);
if (!LL)
continue;
if (auto in = inIdx.find(LL->value); in != inIdx.end()) {
coneIns.insert(in->second);
continue;
}
auto op = opIdx.find(LL->value);
if (op == opIdx.end())
continue;
coneOps.insert(op->second);
for (const auto &operand : N->operands)
todo.push_back(operand.get());
}
json::Array cops, cins;
for (unsigned k : coneOps)
cops.push_back((int64_t)k);
for (unsigned k : coneIns)
cins.push_back((int64_t)k);
out["ops"] = std::move(cops);
out["inputs"] = std::move(cins);
std::string c = ON ? renderC(*ON, how, dtypeOfType(O->getType())) : "";
if (!c.empty())
out["c"] = c;
U.outC.push_back(c);
auto it = st.valueToNodeMap.find(O);
if (it != st.valueToNodeMap.end() && it->second) {
U.executions = std::max(U.executions, it->second->executions);
if (std::isfinite(it->second->sens))
U.sensitivity = std::isfinite(U.sensitivity)
? U.sensitivity + it->second->sens
: it->second->sens;
}
U.outputs.push_back(std::move(out));
}
if (!sg.outputs.empty()) {
SmallVector<Value *> outs(sg.outputs.begin(), sg.outputs.end());
double base = getCompCost(outs, sg.inputs);
if (FPNode *N = nodeOf(sg.outputs[0]))
U.baseCost = (int64_t)std::llround(base * (double)N->executions);
}
addCOAlternatives(U, sg);
addCSAlternatives(U, sg);
judge(U);
U.left = leftReason(U, &sg);
units.push_back(std::move(U));
}
void Explainer::buildMatmul(size_t m) {
CandidateMatmul &CM = st.CMs[m];
Unit U;
U.id = formatv("M{0}", m).str();
U.kind = "matmul";
const AbstractMatmul *mm = CM.matmul;
if (mm) {
U.root = dyn_cast_or_null<Instruction>(mm->outputValue);
if (!U.root && !mm->footprint.empty())
U.root = mm->footprint[0];
auto dim = [](unsigned d) {
return d == ~0u || d == 0 ? std::string("?") : std::to_string(d);
};
unsigned M = mm->globalM ? mm->globalM : mm->M;
unsigned N = mm->globalN ? mm->globalN : mm->N;
unsigned K = mm->globalK ? mm->globalK : mm->K;
U.shape = dim(M) + "x" + dim(N) + "x" + dim(K);
}
U.loc = locOf(U.root);
U.executions = (unsigned)std::min<uint64_t>(CM.executions, ~0u);
if (CM.initialCompCost >= 0.0)
U.baseCost =
(int64_t)std::llround(CM.initialCompCost * (double)CM.executions);
for (size_t j = 0; j < CM.candidates.size(); ++j) {
const CandidateMatmul::Option &o = CM.candidates[j];
Alt A;
A.id = formatv("M{0}_{1}", m, j).str();
A.kind = "matmul";
A.summary = matmulOptionLabel(o);
A.item = &CM;
A.index = j;
A.at = U.root;
A.costDelta = GET_COST(CM.getCompCostDelta(j));
double acc = CM.getAccCostDelta(j);
if (std::isfinite(acc)) {
A.accDelta = acc;
if (rec.selector != "error-budget" && std::isfinite(relErr(acc)))
A.err = relErr(acc);
}
if (std::isfinite(o.domainError))
A.domainError = o.domainError;
A.baseCost = U.baseCost;
U.alts.push_back(std::move(A));
}
judge(U);
U.left = leftReason(U, nullptr);
units.push_back(std::move(U));
}
std::optional<int64_t> Explainer::appliedDelta() const {
int64_t total = 0;
bool complete = true;
for (const Unit &U : units)
for (const Alt &A : U.alts)
if (A.status == "applied") {
if (A.costDelta)
total += *A.costDelta;
else
complete = false;
}
if (complete)
return total;
if (rec.selector == "dp" && rec.selected)
return *rec.selected;
return std::nullopt;
}
void Explainer::build() {
markSteps();
for (size_t s = 0; s < st.subgraphs.size(); ++s)
buildSubgraph(s);
for (size_t m = 0; m < st.CMs.size(); ++m)
buildMatmul(m);
}
json::Object Explainer::toJSON() {
json::Object root;
root["schema"] = "poseidon-explain/1";
json::Object fn;
const Function *origin = siteOriginOf(F);
StringRef symbol = origin ? origin->getName() : F.getName();
symbol.consume_back("_poseidon_body");
symbol = symbol.split(".poseidon.").first;
fn["name"] = llvm::demangle(symbol.str());
fn["symbol"] = symbol.str();
fn["ir"] = F.getName().str();
auto site = siteCalls().find(&F);
if (site != siteCalls().end())
putLoc(fn, "site", locOf(site->second));
const DISubprogram *SP = origin ? origin->getSubprogram() : F.getSubprogram();
if (SP)
putLoc(fn, "loc", SourceLoc{SP->getFilename().str(), SP->getLine(), 0});
root["function"] = std::move(fn);
json::Object target;
if (tau > 0.0) {
target["tau"] = tau;
target["tau_from"] = rec.siteTarget ? "site" : "flag";
} else {
target["budget"] = (int64_t)flags::CompCostBudget;
}
target["cost_model"] = costModelPath();
target["selector"] = rec.selector;
if (rec.fromCache)
target["from_cache"] = true;
root["target"] = std::move(target);
json::Object result;
std::optional<int64_t> before;
int64_t sum = 0;
bool known = true;
for (const Unit &U : units) {
if (U.baseCost)
sum += *U.baseCost;
else
known = false;
}
if (known)
before = sum;
if (before)
result["cost_before"] = *before;
std::optional<int64_t> delta = appliedDelta();
if (before && delta)
result["cost_after"] = *before + *delta;
putNum(result, "modelled_rel_error_as_written", relErr(0.0));
bool matmulApplied = false;
for (const Unit &U : units)
for (const Alt &A : U.alts)
matmulApplied |= A.status == "applied" && A.kind == "matmul";
if (!matmulApplied || rec.selector != "error-budget") {
double acc = steps.empty() ? 0.0 : std::numeric_limits<double>::quiet_NaN();
if (rec.selected) {
auto it = rec.table.find(InstructionCost(*rec.selected));
if (it != rec.table.end())
acc = it->second;
}
putNum(result, "modelled_rel_error", relErr(acc));
}
json::Array appliedIds;
for (const SolutionStep &step : steps)
if (!skipped.count(keyOf(step)))
appliedIds.push_back(stepId(step));
result["applied"] = std::move(appliedIds);
root["result"] = std::move(result);
json::Array subgraphs;
for (Unit &U : units) {
json::Object u;
u["id"] = U.id;
u["kind"] = U.kind;
putLoc(u, "loc", U.loc);
if (!U.shape.empty())
u["shape"] = U.shape;
if (U.baseCost)
u["cost"] = *U.baseCost;
u["executions"] = (int64_t)U.executions;
if (U.kind == "subgraph") {
u["ops"] = std::move(U.ops);
u["inputs"] = std::move(U.inputs);
u["outputs"] = json::Array(U.outputs);
json::Object original;
if (U.outC.size() == 1) {
if (auto *o = U.outputs[0].getAsObject())
if (auto f = o->getString("fpcore"))
original["fpcore"] = f->str();
if (!U.outC[0].empty())
original["c"] = U.outC[0];
} else {
json::Array cs, fs;
bool allF = true;
for (size_t k = 0; k < U.outputs.size(); ++k) {
cs.push_back(U.outC[k]);
auto *o = U.outputs[k].getAsObject();
auto f = o ? o->getString("fpcore") : std::nullopt;
if (f)
fs.push_back(f->str());
else
allF = false;
}
original["c"] = std::move(cs);
if (allF)
original["fpcore"] = std::move(fs);
}
u["original"] = std::move(original);
if (!U.herbie.empty())
u["herbie"] = std::move(U.herbie);
}
if (!U.left.empty())
u["left_as_written"] = U.left;
json::Array alts;
for (Alt &A : U.alts) {
json::Object a;
if (!A.id.empty())
a["id"] = A.id;
a["kind"] = A.kind;
a["summary"] = A.summary;
if (!A.precision.empty())
a["precision"] = A.precision;
if (!A.outputs.empty() && U.kind == "subgraph") {
json::Array outs;
for (unsigned o : A.outputs)
outs.push_back((int64_t)o);
a["outputs"] = std::move(outs);
}
if (!A.fpcore.empty())
a["fpcore"] = A.fpcore;
if (A.c.size() == 1 && !A.c[0].empty())
a["c"] = A.c[0];
else if (A.c.size() > 1) {
json::Array cs;
for (const std::string &c : A.c)
cs.push_back(c);
a["c"] = std::move(cs);
}
if (A.kind == "algebraic") {
a["approx"] = A.approx;
if (!A.approxNote.empty())
a["approx_note"] = A.approxNote;
}
if (!A.changes.empty())
a["changes"] = std::move(A.changes);
if (A.costDelta)
a["cost_delta"] = *A.costDelta;
if (A.err)
a["modelled_error"] = *A.err;
if (A.domainError)
a["modelled_domain_error"] = *A.domainError;
if (A.bitsBefore || A.bitsAfter) {
json::Object bits;
if (A.bitsBefore)
bits["before"] = *A.bitsBefore;
if (A.bitsAfter)
bits["after"] = *A.bitsAfter;
a["herbie_error_bits"] = std::move(bits);
}
a["status"] = A.status;
if (!A.reason.empty())
a["reason"] = A.reason;
alts.push_back(std::move(a));
}
u["alternatives"] = std::move(alts);
subgraphs.push_back(std::move(u));
}
root["subgraphs"] = std::move(subgraphs);
if (!rec.table.empty()) {
json::Array pareto;
for (const auto &[cost, acc] : rec.table) {
json::Object p;
int64_t b = GET_COST(cost);
p["budget"] = b;
if (before)
p["cost"] = *before + b;
putNum(p, "modelled_rel_error", relErr(acc));
json::Array ids;
auto it = rec.solutions.find(cost);
if (it != rec.solutions.end())
for (const SolutionStep &step : it->second) {
std::string id = stepId(step);
if (!id.empty())
ids.push_back(id);
}
p["applied"] = std::move(ids);
if (rec.selected && *rec.selected == b)
p["selected"] = true;
pareto.push_back(std::move(p));
}
root["pareto"] = std::move(pareto);
}
return root;
}
void Explainer::emitRemarks() {
OptimizationRemarkEmitter ORE(&F);
auto siteIt = siteCalls().find(&F);
DiagnosticLocation siteLoc = siteIt != siteCalls().end() && siteIt->second
? DiagnosticLocation(siteIt->second)
: DiagnosticLocation(F.getSubprogram());
const Function *origin = siteOriginOf(F);
StringRef symbol = origin ? origin->getName() : F.getName();
symbol.consume_back("_poseidon_body");
symbol = symbol.split(".poseidon.").first;
std::string siteName = llvm::demangle(symbol.str());
if (auto paren = siteName.find('('); paren != std::string::npos)
siteName = siteName.substr(0, paren);
int64_t before = 0;
bool known = true;
for (const Unit &U : units) {
if (U.baseCost)
before += *U.baseCost;
else
known = false;
}
std::optional<int64_t> delta = appliedDelta();
unsigned nApplied = 0;
for (const Unit &U : units)
for (const Alt &A : U.alts)
nApplied += A.status == "applied";
ORE.emit([&]() {
OptimizationRemarkAnalysis R(kPass, "Site", siteLoc, &F.getEntryBlock());
R << "Poseidon site " << ore::NV("Site", siteName) << ": ";
if (tau > 0.0)
R << "target " << ore::NV("Tau", num(tau));
else
R << "cost budget " << ore::NV("Budget", costStr(flags::CompCostBudget));
double asWritten = relErr(0.0);
if (std::isfinite(asWritten))
R << "; modelled error as written "
<< ore::NV("ErrorBefore", num(asWritten));
if (nApplied == 0) {
R << "; left as written";
} else {
double acc = std::numeric_limits<double>::quiet_NaN();
if (rec.selected) {
auto it = rec.table.find(InstructionCost(*rec.selected));
if (it != rec.table.end())
acc = it->second;
}
if (std::isfinite(relErr(acc)))
R << ", after " << ore::NV("Error", num(relErr(acc)));
R << "; " << ore::NV("Rewrites", (unsigned)nApplied)
<< (nApplied == 1 ? " rewrite" : " rewrites");
if (known && delta)
R << ", cost "
<< ore::NV("Cost", costStr(before) + " -> " +
costStr(before + *delta) +
pctStr(before, before + *delta));
}
if (!rec.table.empty())
R << "; " << ore::NV("ParetoPoints", (unsigned)rec.table.size())
<< (rec.table.size() == 1 ? " Pareto point" : " Pareto points");
return R;
});
for (const Unit &U : units) {
if (!U.root)
continue;
ORE.emit([&]() {
OptimizationRemarkAnalysis R(kPass, "Profile", U.root);
if (U.kind == "matmul") {
R << "Poseidon profile of the " << ore::NV("Shape", U.shape)
<< " matrix product: " << ore::NV("Executions", U.executions)
<< " multiply-adds";
return R;
}
R << "Poseidon profile of " << ore::NV("Expr", exprWords(U));
if (U.outC.size() > 1)
R << " and " << ore::NV("MoreOutputs", (unsigned)U.outC.size() - 1)
<< " more output" << (U.outC.size() > 2 ? "s" : "");
R << ": ";
if (!U.inputRanges.empty())
R << ore::NV("Ranges", joinWords(U.inputRanges)) << "; ";
R << ore::NV("Executions", U.executions) << " executions";
if (std::isfinite(U.sensitivity))
R << ", sensitivity " << ore::NV("Sensitivity", num(U.sensitivity));
return R;
});
}
for (const Unit &U : units)
for (const Alt &A : U.alts) {
if (A.status != "applied" || !A.at)
continue;
std::string costChange;
if (A.baseCost && A.costDelta)
costChange = costStr(*A.baseCost) + " -> " +
costStr(*A.baseCost + *A.costDelta) +
pctStr(*A.baseCost, *A.baseCost + *A.costDelta);
std::string errChange;
if (A.accDelta && std::isfinite(share(A.baseAcc)))
errChange = num(share(A.baseAcc)) + " -> " +
num(share(A.baseAcc + *A.accDelta));
if (A.kind == "algebraic") {
for (size_t k = 0; k < A.outputs.size(); ++k) {
unsigned o = A.outputs[k];
const Instruction *at = A.at;
if (U.sg && o < U.sg->outputs.size())
at = U.sg->outputs[o];
std::string orig = o < U.outC.size() ? U.outC[o] : "";
std::string now = k < A.c.size() ? A.c[k] : A.fpcore;
ORE.emit([&]() {
OptimizationRemark R(kPass, "Rewritten", at);
R << "Poseidon rewrote " << ore::NV("Expr", clip(orig)) << " as "
<< ore::NV("Rewritten", clip(now));
std::string how = A.approxNote;
if (!A.approxRanges.empty())
how +=
", valid on the profiled range " + joinWords(A.approxRanges);
if (!how.empty())
R << " (" << ore::NV("Technique", how) << ")";
if (!errChange.empty())
R << "; modelled error " << ore::NV("Error", errChange);
if (!costChange.empty())
R << ", cost " << ore::NV("Cost", costChange);
R << " [" << ore::NV("Id", A.id) << "]";
return R;
});
}
} else if (A.kind == "precision" || A.kind == "expansion") {
ORE.emit([&]() {
OptimizationRemark R(kPass, "PrecisionChanged", A.at);
R << "Poseidon runs " << ore::NV("Ops", joinWords(A.changedOps))
<< " in " << ore::NV("Type", A.changedTo);
if (A.c.size() == 1 && !A.c[0].empty())
R << ": " << ore::NV("Rewritten", clip(A.c[0]));
if (!errChange.empty())
R << "; modelled error " << ore::NV("Error", errChange);
if (!costChange.empty())
R << ", cost " << ore::NV("Cost", costChange);
R << " [" << ore::NV("Id", A.id) << "]";
return R;
});
} else if (A.kind == "matmul") {
ORE.emit([&]() {
OptimizationRemark R(kPass, "MatmulRaised", A.at);
R << "Poseidon raised the " << ore::NV("Shape", U.shape)
<< " matrix product to " << ore::NV("Technique", A.summary);
if (A.domainError)
R << "; modelled domain error "
<< ore::NV("Error", num(*A.domainError));
if (!costChange.empty())
R << ", cost " << ore::NV("Cost", costChange);
R << " [" << ore::NV("Id", A.id) << "]";
return R;
});
}
}
for (const Unit &U : units) {
if (!U.root)
continue;
if (!U.applied)
ORE.emit([&]() {
OptimizationRemarkMissed R(kPass, "LeftAsWritten", U.root);
R << "Poseidon left "
<< ore::NV("Expr", U.kind == "matmul"
? "the " + U.shape + " matrix product"
: exprWords(U))
<< " as written: " << ore::NV("Reason", U.left);
return R;
});
SmallVector<const Alt *, 8> rejected;
for (const Alt &A : U.alts)
if (A.status != "applied")
rejected.push_back(&A);
std::stable_sort(rejected.begin(), rejected.end(),
[](const Alt *a, const Alt *b) {
if (a->rank != b->rank)
return a->rank < b->rank;
if (a->err && b->err)
return *a->err < *b->err;
return false;
});
for (size_t k = 0; k < rejected.size() && k < kMaxRejectedRemarks; ++k) {
const Alt &A = *rejected[k];
const Instruction *at = A.at ? A.at : U.root;
ORE.emit([&]() {
OptimizationRemarkMissed R(kPass, "Rejected", at);
R << "Poseidon "
<< (A.status == "pareto" ? "did not select " : "rejected ")
<< ore::NV("Kind", kindWords(A.kind));
if (!A.id.empty())
R << " " << ore::NV("Id", A.id);
if (A.c.size() == 1 && !A.c[0].empty())
R << " to " << ore::NV("Rewritten", clip(A.c[0]));
else if (A.kind != "algebraic")
R << " (" << ore::NV("Rewritten", A.summary) << ")";
else if (!A.fpcore.empty())
R << " to " << ore::NV("Rewritten", clip(A.fpcore));
R << ": " << ore::NV("Reason", A.reason);
SmallVector<std::string, 2> facts;
if (A.err && A.reason.find("modelled error") == std::string::npos)
facts.push_back("modelled error " + num(*A.err));
if (A.costDelta)
facts.push_back("cost change " + costStr(*A.costDelta));
if (!facts.empty())
R << " (" << ore::NV("Facts", join(facts, ", ")) << ")";
return R;
});
}
}
}
} // namespace
bool explainWanted(const Function &F) {
return !flags::Explain.empty() || !flags::ReportPath.empty() ||
OptimizationRemarkEmitter::allowExtraAnalysis(F, kPass);
}
void noteSiteCall(const Function *body, const DebugLoc &loc) {
siteCalls()[body] = loc;
}
void explainNothingToOptimize(Function &F) {
OptimizationRemarkEmitter ORE(&F);
ORE.emit([&]() {
auto site = siteCalls().find(&F);
DiagnosticLocation loc = site != siteCalls().end() && site->second
? DiagnosticLocation(site->second)
: DiagnosticLocation(F.getSubprogram());
OptimizationRemarkMissed R(kPass, "NothingToOptimize", loc,
&F.getEntryBlock());
R << "Poseidon found no floating-point subgraph of two or more "
"operations and no matrix product to optimize";
return R;
});
}
void explainSolve(Function &F, FunctionFPState &st,
ArrayRef<SolutionStep> steps, const SolveRecord &rec) {
const bool remarks = OptimizationRemarkEmitter::allowExtraAnalysis(F, kPass);
const std::string dir =
!flags::Explain.empty() ? flags::Explain.getValue() : flags::ReportPath;
if (!remarks && dir.empty())
return;
Explainer E(F, st, steps, rec);
E.build();
if (remarks)
E.emitRemarks();
if (dir.empty())
return;
if (auto EC = sys::fs::create_directories(dir)) {
errs() << "Error creating explanation directory: " << EC.message() << "\n";
return;
}
SmallString<128> path(dir);
sys::path::append(path, F.getName() + ".explain.json");
std::error_code EC;
raw_fd_ostream out(path, EC, sys::fs::OF_Text);
if (EC) {
errs() << "Error writing explanation: " << EC.message() << "\n";
return;
}
out << formatv("{0:2}", json::Value(E.toJSON())) << "\n";
errs() << "Poseidon explanation written to " << path << "\n";
}
} // namespace poseidon