Rework global hoisting to be policy based. (#7950)
* Adds a policy/decision layer on top of the analysis.
* Reworks the transformation to be based purely on the policy with no inline decisions.
* Tightens up the policy a bit to better handle:
* Never hoisting a standalone init_tensor
* Working properly with a chain of non-hoistable leaves (i.e. several broadcast or metadata ops which should not hoist)
diff --git a/iree/compiler/Dialect/Util/Analysis/Constant/ConstExpr.cpp b/iree/compiler/Dialect/Util/Analysis/Constant/ConstExpr.cpp
index d681ef6..9de6344 100644
--- a/iree/compiler/Dialect/Util/Analysis/Constant/ConstExpr.cpp
+++ b/iree/compiler/Dialect/Util/Analysis/Constant/ConstExpr.cpp
@@ -21,6 +21,31 @@
namespace IREE {
namespace Util {
+//===----------------------------------------------------------------------===//
+// ConstExprAnalysis
+//===----------------------------------------------------------------------===//
+
+namespace {
+OpOperand *findOperandFor(Operation *op, Value input) {
+ for (OpOperand &operand : op->getOpOperands()) {
+ if (operand.get() == input) return &operand;
+ }
+ return nullptr;
+}
+
+} // namespace
+
+bool ConstExprAnalysis::ConstValueInfo::hasNonAnalyzedConsumer() const {
+ // The analysis cannot represent zero-result operations, so detect that
+ // and return.
+ for (Operation *user : getOperation()->getUsers()) {
+ if (user->getNumResults() == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
ConstExprAnalysis::ConstExprAnalysis(Operation *rootOp) {
Explorer explorer(rootOp, TraversalAction::SHALLOW);
explorer.initialize();
@@ -116,6 +141,14 @@
}
}
}
+
+ // Go through and populate all consumer sets now that producers are known.
+ for (auto it : constInfoMap) {
+ ConstValueInfo *consumer = it.second;
+ for (ConstValueInfo *producer : consumer->producers) {
+ producer->consumers.insert(consumer);
+ }
+ }
}
ConstExprAnalysis::ConstValueInfo *ConstExprAnalysis::addInfo(
@@ -182,6 +215,136 @@
void ConstExprAnalysis::dump() const { print(llvm::errs()); }
+//===----------------------------------------------------------------------===//
+// ConstExprHoistingPolicy
+//===----------------------------------------------------------------------===//
+
+ConstExprHoistingPolicy::ConstExprHoistingPolicy(
+ const ConstExprAnalysis &analysis)
+ : analysis(analysis), decisions(analysis.allocedConstInfos.size()) {
+ for (auto &it : analysis.allocedConstInfos) {
+ decisions[it.get()] = {};
+ }
+}
+
+void ConstExprHoistingPolicy::initialize() {
+ // Bootstrap the worklist in analysis order, which is topological (def, use)
+ // order.
+ // TODO: Do a secondary sort?
+ Worklist worklist;
+ worklist.reserve(analysis.allocedConstInfos.size());
+ for (auto &it : analysis.allocedConstInfos) {
+ worklist.push_back(it.get());
+ }
+
+ // Since just initializing invariants, which are local, iteration order
+ // doesn't matter.
+ for (auto *info : worklist) {
+ Decision *decision = getDecision(info);
+ makeInvariantDecision(info, decision);
+ Outcome outcome = decision->getOutcome();
+ if (outcome != UNDECIDED) {
+ LLVM_DEBUG(dbgs() << "ConstExprHoistPolicy(INVARIANT, ");
+ if (outcome == ENABLE_HOIST) {
+ LLVM_DEBUG(dbgs() << "ENABLE_HOIST");
+ } else if (outcome == DISABLE_HOIST) {
+ LLVM_DEBUG(dbgs() << "DISABLE_HOIST");
+ }
+ LLVM_DEBUG(dbgs() << "): " << info->constValue << "\n");
+ }
+ }
+
+ // Work iteratively until converged.
+ for (int i = 0;; ++i) {
+ bool madeChange = false;
+ for (auto *info : worklist) {
+ Decision *decision = getDecision(info);
+ if (decision->getOutcome() != UNDECIDED) continue;
+ makeDecision(info, decision);
+
+ if (decision->getOutcome() != UNDECIDED) {
+ madeChange = true;
+ LLVM_DEBUG(dbgs() << "ConstExprHoistPolicy(" << i << ", ");
+ if (decision->getOutcome() == ENABLE_HOIST) {
+ LLVM_DEBUG(dbgs() << "ENABLE_HOIST");
+ } else if (decision->getOutcome() == DISABLE_HOIST) {
+ LLVM_DEBUG(dbgs() << "DISABLE_HOIST");
+ }
+ LLVM_DEBUG(dbgs() << "): " << info->constValue << "\n");
+ }
+ }
+
+ if (!madeChange) {
+ LLVM_DEBUG(dbgs() << "ConstExprHoistPolicy(" << i << ", CONVERGED)\n");
+ break;
+ }
+ }
+
+ for (auto *info : worklist) {
+ Decision *decision = getDecision(info);
+ if (decision->getOutcome() == UNDECIDED) {
+ LLVM_DEBUG(dbgs() << "ConstExprHoistPolicy: Value did not converge: "
+ << info->constValue << "\n");
+ }
+ }
+}
+
+void ConstExprHoistingPolicy::makeInvariantDecision(
+ const ConstExprAnalysis::ConstValueInfo *info, Decision *decision) {
+ // Check 1: Is it not const-expr.
+ if (!info->isConstExpr()) {
+ return decision->disableHoist();
+ }
+
+ // Check 2: Is it a root (these are already hoisted).
+ if (info->isRoot) {
+ decision->disableHoist();
+ }
+
+ // Check 3: Is the op itself a valid "leaf" that can become a global.
+ if (!isHoistableConstExprLeaf(info)) {
+ return decision->disableHoist();
+ }
+}
+
+void ConstExprHoistingPolicy::makeDecision(
+ const ConstExprAnalysis::ConstValueInfo *info, Decision *decision) {
+ // A const-expr value has a legal escape if:
+ // - Has a non analyzed consumer
+ // - It has an anlyzed consumer that:
+ // - Has been marked as DISABLE_HOIST (must feed into something that is
+ // not being hoisted).
+ // - Is consumed by a hoistable operand or no operand (signals implicit
+ // capture).
+ bool hasLegalEscape = info->hasNonAnalyzedConsumer();
+ if (!hasLegalEscape) {
+ for (auto *consumerInfo : info->consumers) {
+ Decision *consumerDecision = getDecision(consumerInfo);
+ if (consumerDecision->getOutcome() != DISABLE_HOIST) continue;
+
+ Operation *consumerOp = consumerInfo->getOperation();
+ OpOperand *consumerOperand = findOperandFor(consumerOp, info->constValue);
+ if (!consumerOperand) {
+ // Must be an implicit capture.
+ hasLegalEscape = true;
+ break;
+ } else if (isHoistableConstExprConsumingOperand(consumerOperand)) {
+ hasLegalEscape = true;
+ }
+ }
+ }
+
+ // If there is no legal escape, we can concretely disable.
+ if (!hasLegalEscape) {
+ decision->disableHoist();
+ return;
+ }
+
+ // Otherwise, we can conditionally enable hoisting (based on cost model, etc).
+ // TODO: Implement further conditions.
+ decision->enableHoist();
+}
+
} // namespace Util
} // namespace IREE
} // namespace iree_compiler
diff --git a/iree/compiler/Dialect/Util/Analysis/Constant/ConstExpr.h b/iree/compiler/Dialect/Util/Analysis/Constant/ConstExpr.h
index c59bed0..91ff33b 100644
--- a/iree/compiler/Dialect/Util/Analysis/Constant/ConstExpr.h
+++ b/iree/compiler/Dialect/Util/Analysis/Constant/ConstExpr.h
@@ -7,6 +7,8 @@
#ifndef IREE_COMPILER_DIALECT_IREE_UTIL_ANALYSIS_CONSTANT_CONST_EXPR_H_
#define IREE_COMPILER_DIALECT_IREE_UTIL_ANALYSIS_CONSTANT_CONST_EXPR_H_
+#include <vector>
+
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallPtrSet.h"
#include "llvm/ADT/SmallVector.h"
@@ -101,12 +103,20 @@
// Direct producers that feed into this constant value.
SmallPtrSet<ConstValueInfo *, 8> producers;
+ // Direct consumers (constant and non-constant) of this value.
+ SmallPtrSet<ConstValueInfo *, 8> consumers;
+
// Whether this is a root.
bool isRoot = false;
// Whether this is a const-expr value.
bool isConstExpr() const { return state == CONSTANT; }
+ // If the value is consumed by an operation that was not analyzed, returns
+ // true. This can be considered a non-constexpr escape.
+ bool hasNonAnalyzedConsumer() const;
+
+ // Gets the defining operation.
Operation *getOperation() const {
Operation *ret = constValue.getDefiningOp();
assert(ret && "const-expr must have a defining op");
@@ -139,6 +149,66 @@
// Worklist of const value info structs which need more resolution.
using ConstValueWorklist = llvm::SmallVector<ConstValueInfo *>;
ConstValueWorklist worklist;
+ friend class ConstExprHoistingPolicy;
+};
+
+// Mutable base class for implementing policies that make decisions on
+// which expressions to hoist. This wraps a read-only ConstExprAnalysis,
+// overlaying it with cost and decisions about which specific expressions to
+// hoist.
+//
+// The default base class will hoist everything that is eligible.
+class ConstExprHoistingPolicy {
+ public:
+ using Worklist = llvm::SmallVector<const ConstExprAnalysis::ConstValueInfo *>;
+ enum Outcome {
+ UNDECIDED = 0,
+ ENABLE_HOIST = 1,
+ DISABLE_HOIST = 2,
+ };
+ class Decision {
+ public:
+ void disableHoist() {
+ assert(outcome == UNDECIDED &&
+ "can only disable hoisting of an undecided decision");
+ outcome = DISABLE_HOIST;
+ }
+ void enableHoist() {
+ assert(outcome == UNDECIDED &&
+ "can only disable hoisting of an undecided decision");
+ outcome = ENABLE_HOIST;
+ }
+
+ Outcome getOutcome() const { return outcome; }
+
+ private:
+ Outcome outcome = UNDECIDED;
+ };
+
+ ConstExprHoistingPolicy(const ConstExprAnalysis &analysis);
+ void initialize();
+ Decision *getDecision(const ConstExprAnalysis::ConstValueInfo *info) {
+ return &decisions[info];
+ }
+
+ private:
+ // At initialization time, makes any fixed decisions. This hook can only
+ // make decisions that do not depend on any const-exprs outside of what is
+ // passed.
+ void makeInvariantDecision(const ConstExprAnalysis::ConstValueInfo *info,
+ Decision *decision);
+ // Makes a decision that depends on producers and consumers of a value. This
+ // may be called repeatedly until convergence. The implementation should call
+ // decision.disableHoist() or decision.enableHoist() if it can reach a
+ // decision.
+ void makeDecision(const ConstExprAnalysis::ConstValueInfo *info,
+ Decision *decision);
+
+ const ConstExprAnalysis &analysis;
+
+ // Map of ConstValueInfo * to decision structs. All are allocated at
+ // initialization and then the structure is not changed.
+ llvm::DenseMap<const ConstExprAnalysis::ConstValueInfo *, Decision> decisions;
};
inline raw_ostream &operator<<(raw_ostream &os,
diff --git a/iree/compiler/Dialect/Util/Analysis/Constant/OpOracle.cpp b/iree/compiler/Dialect/Util/Analysis/Constant/OpOracle.cpp
index 15bbc9f..6ea7f8e 100644
--- a/iree/compiler/Dialect/Util/Analysis/Constant/OpOracle.cpp
+++ b/iree/compiler/Dialect/Util/Analysis/Constant/OpOracle.cpp
@@ -95,19 +95,25 @@
return {};
}
+ // Forbid if part of a parent that should be treated atomically.
+ if (op->getParentOfType<linalg::LinalgOp>()) {
+ return {};
+ }
+
return getInfoForDefaultConstExprOp(op);
}
bool isHoistableConstExprLeaf(const ConstExprAnalysis::ConstValueInfo *info) {
- if (!info->getOperation()->isRegistered()) {
- if (info->getOperation()->getName().getStringRef() ==
+ Operation *op = info->getOperation();
+ if (!op->isRegistered()) {
+ if (op->getName().getStringRef() ==
"iree_unregistered.non_leaf_const_expr") {
return false;
}
}
// Generally, we prefer to not hoist broadcasts.
- if (auto genericOp = dyn_cast<linalg::GenericOp>(info->getOperation())) {
+ if (auto genericOp = dyn_cast<linalg::GenericOp>(op)) {
// Detect op that only broadcast input as fusing them makes the new
// op cheaper.
if (genericOp.getNumParallelLoops() == genericOp.getNumLoops() &&
@@ -122,6 +128,12 @@
}
}
+ // Never hoist init_tensor. These are sometimes used for pure shape metadata
+ // and must not be separated from their consumers.
+ if (isa<linalg::InitTensorOp>(op)) {
+ return false;
+ }
+
return true;
}
diff --git a/iree/compiler/Dialect/Util/Transforms/HoistIntoGlobals.cpp b/iree/compiler/Dialect/Util/Transforms/HoistIntoGlobals.cpp
index 5b1ac52..21f08ca 100644
--- a/iree/compiler/Dialect/Util/Transforms/HoistIntoGlobals.cpp
+++ b/iree/compiler/Dialect/Util/Transforms/HoistIntoGlobals.cpp
@@ -53,6 +53,8 @@
const auto &constExprs = getAnalysis<ConstExprAnalysis>();
LLVM_DEBUG(dbgs() << constExprs);
LLVM_DEBUG(dbgs() << "\n\n");
+ ConstExprHoistingPolicy policy(constExprs);
+ policy.initialize();
// Maps original values to newly materialized values.
HoistedValueMap hoistedMap;
@@ -64,66 +66,43 @@
// yet.
getOperation().walk<WalkOrder::PreOrder>([&](Operation *iterOp) {
// We only want to look at const-expr ops (non roots) since they may
- // have interesting escapes.
+ // have interesting escapes. Early exit here for efficiency.
auto *iterInfo = constExprs.lookup(iterOp);
- if (!iterInfo || iterInfo->isRoot || !iterInfo->isConstExpr()) {
+ if (!iterInfo) {
return WalkResult::advance();
}
- // Skip if our policy prohibits this being treated as a hoistable leaf.
- if (!isHoistableConstExprLeaf(iterInfo)) return WalkResult::advance();
-
- // This op is hoistable - for each result find eligible escapes and
- // hoist them.
- LLVM_DEBUG(dbgs() << "PROCESSING CONST-EXPR OP: " << *iterOp << "\n");
for (Value constExprResult : iterOp->getResults()) {
- // Need to snapshot the uses since we modify them during iteration.
- SmallVector<OpOperand *> uses;
- for (OpOperand &use : constExprResult.getUses()) {
- uses.push_back(&use);
+ auto *resultInfo = constExprs.lookup(constExprResult);
+ assert(resultInfo && "must have const-expr info");
+
+ if (policy.getDecision(resultInfo)->getOutcome() !=
+ ConstExprHoistingPolicy::ENABLE_HOIST) {
+ continue;
}
- // Iterate over uses snapshots.
- for (OpOperand *operand : uses) {
- auto *targetInfo = constExprs.lookup(operand->getOwner());
-
- // We do not treat is as a const escape if the target is:
- // - Not a valid operand to convert to a constant.
- // - Const-expr and is an allowed leaf by policy
- // Note that we never touch an operand that is part of a const-expr.
- if (targetInfo && targetInfo->isConstExpr() &&
- isHoistableConstExprLeaf(targetInfo)) {
- LLVM_DEBUG(dbgs() << " - SKIP (CONST-EXPR): "
- << *operand->getOwner() << "\n");
- continue;
- }
- if (!isHoistableConstExprConsumingOperand(operand)) {
- LLVM_DEBUG(dbgs() << " - SKIP (INVALID OPERAND): "
- << *operand->getOwner() << "\n");
- continue;
- }
-
- // Bingo.
- LLVM_DEBUG(dbgs() << " + HOIST CONST-EXPR:\n");
- LLVM_DEBUG(dbgs() << " : Operand #" << operand->getOperandNumber()
- << " of " << *operand->getOwner() << "\n");
- LLVM_DEBUG(dbgs() << " : From " << operand->get() << "\n\n");
-
- hoistConstExpr(operand, hoistedMap, moduleSymbols, constExprs);
- }
+ hoistConstExpr(constExprResult, hoistedMap, moduleSymbols, constExprs);
}
-
return WalkResult::advance();
});
+ // Apply any remaining RAUW cleanups. We have to do these at the cleanup
+ // phase since modifying the source program can invalidate the analysis.
+ // Up to this point, we have only been cloning.
+ OpBuilder builder(&getContext());
+ for (auto it : hoistedMap) {
+ Value originalValue = it.first;
+ GlobalOp globalOp = it.second;
+ builder.setInsertionPointAfterValue(originalValue);
+ auto load = builder.create<GlobalLoadOp>(globalOp->getLoc(), globalOp);
+ originalValue.replaceAllUsesWith(load);
+ }
cleanupDeadOps(constExprs);
}
- void hoistConstExpr(OpOperand *operand, HoistedValueMap &hoistedMap,
- SymbolTable &moduleSymbols,
- const ConstExprAnalysis &constExprs) {
- Operation *targetOp = operand->getOwner();
- Value originalValue = operand->get();
+ GlobalOp hoistConstExpr(Value originalValue, HoistedValueMap &hoistedMap,
+ SymbolTable &moduleSymbols,
+ const ConstExprAnalysis &constExprs) {
GlobalOp existingGlobal = hoistedMap.lookup(originalValue);
if (!existingGlobal) {
@@ -142,12 +121,7 @@
assert(existingGlobal &&
"hoisting const-expr should have mapped a global for the requested "
"value");
-
- // Already hoisted - just convert to a load.
- OpBuilder builder(targetOp);
- auto load =
- builder.create<GlobalLoadOp>(targetOp->getLoc(), existingGlobal);
- operand->set(load);
+ return existingGlobal;
}
void cloneProducerTreeInto(
diff --git a/iree/compiler/Dialect/Util/Transforms/test/hoist_into_globals.mlir b/iree/compiler/Dialect/Util/Transforms/test/hoist_into_globals.mlir
index df59eb2..2c1d4d9 100644
--- a/iree/compiler/Dialect/Util/Transforms/test/hoist_into_globals.mlir
+++ b/iree/compiler/Dialect/Util/Transforms/test/hoist_into_globals.mlir
@@ -64,10 +64,9 @@
// CHECK: func @main
builtin.func @main() -> (i32, i32, i32) {
- // CHECK: %[[LOAD_HOISTED_1:.*]] = util.global.load @[[HOISTED_1]] : i32
- // CHECK: %[[RESULT:.*]] = "iree_unregistered.var_expr"(%[[LOAD_HOISTED_1]])
// CHECK-DAG: %[[LOAD_HOISTED_0:.*]] = util.global.load @[[HOISTED_0]] : i32
// CHECK-DAG: %[[LOAD_HOISTED_1:.*]] = util.global.load @[[HOISTED_1]] : i32
+ // CHECK-DAG: %[[RESULT:.*]] = "iree_unregistered.var_expr"(%[[LOAD_HOISTED_1]])
// CHECK: return %[[LOAD_HOISTED_0]], %[[LOAD_HOISTED_1]], %[[RESULT]]
%0 = arith.constant 0 : i32
%1 = arith.constant 1 : i32