Fix compile error when hoisting constants. (#24672)
The constant-expression hoisting pass (`HoistIntoGlobalsPass`) could
hoist a const-expr defined *inside* a `flow.dispatch.region`, placing
the resulting `util.global.load` within the region. When the region is
later outlined into an isolated executable module the global is no
longer visible, producing an "undefined global" verification error.
Skip any const-expr whose defining op is strictly nested inside a
`flow.dispatch.region` / `flow.dispatch.workgroups`. Hoisting the
dispatch op itself remains valid and is unaffected.
Fixes issue https://github.com/iree-org/iree/issues/24671
---------
Signed-off-by: Paul Stark <paul.stark@cdprojektred.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
diff --git a/compiler/src/iree/compiler/Dialect/Util/Transforms/HoistIntoGlobals.cpp b/compiler/src/iree/compiler/Dialect/Util/Transforms/HoistIntoGlobals.cpp
index d4360c8..12e7afc 100644
--- a/compiler/src/iree/compiler/Dialect/Util/Transforms/HoistIntoGlobals.cpp
+++ b/compiler/src/iree/compiler/Dialect/Util/Transforms/HoistIntoGlobals.cpp
@@ -37,6 +37,16 @@
// Maps an original value in the program to the symbol name of a global.
using HoistedValueMap = llvm::DenseMap<Value, GlobalOp>;
+// Returns true if |op| is a flow dispatch region/workgroups op, i.e. a region
+// that is later outlined into an isolated executable module.
+// The dialect/op names are checked by string since Flow depends on Util
+// and we don't want to create a circular dependency.
+static bool isDispatchContainer(Operation *op) {
+ StringRef opName = op->getName().getStringRef();
+ return opName == "flow.dispatch.region" ||
+ opName == "flow.dispatch.workgroups";
+}
+
static std::string getHoistedName(Type type) {
std::string str;
llvm::raw_string_ostream os(str);
@@ -119,26 +129,33 @@
auto walkRes = funcOp.walk<WalkOrder::PreOrder>([&](Operation *iterOp) {
// We only want to look at const-expr ops (non roots) since they may
- // have interesting escapes. Early exit here for efficiency.
+ // have interesting escapes. Non const-expr ops are ignored here, but
+ // we still fall through to the dispatch check below so that we stop
+ // descending into dispatch bodies even when the dispatch op itself is
+ // not a const-expr.
auto *iterInfo = constExprs.lookup(iterOp);
- if (!iterInfo) {
- return WalkResult::advance();
+ if (iterInfo) {
+ // Record operation order for deterministic sorting. Since we walk in
+ // PreOrder, producers are visited before their users.
+ opOrder[iterOp] = orderIdx++;
+ for (Value constExprResult : iterOp->getResults()) {
+ auto *resultInfo = constExprs.lookup(constExprResult);
+ assert(resultInfo && "must have const-expr info");
+ if (policy.getDecision(resultInfo)->getOutcome() !=
+ ConstExprHoistingPolicy::ENABLE_HOIST) {
+ continue;
+ }
+ if (failed(hoistConstExpr(constExprResult, hoistedMap,
+ moduleSymbols, constExprs, opOrder))) {
+ return WalkResult::interrupt();
+ }
+ }
}
- // Record operation order for deterministic sorting. Since we walk in
- // PreOrder, producers are visited before their users.
- opOrder[iterOp] = orderIdx++;
- for (Value constExprResult : iterOp->getResults()) {
- auto *resultInfo = constExprs.lookup(constExprResult);
- assert(resultInfo && "must have const-expr info");
- if (policy.getDecision(resultInfo)->getOutcome() !=
- ConstExprHoistingPolicy::ENABLE_HOIST) {
- continue;
- }
- if (failed(hoistConstExpr(constExprResult, hoistedMap, moduleSymbols,
- constExprs, opOrder))) {
- return WalkResult::interrupt();
- }
+ // FIXME: this is a temporary workaround for issue #24671
+ // Never hoist values defined inside a dispatch region
+ if (isDispatchContainer(iterOp)) {
+ return WalkResult::skip();
}
return WalkResult::advance();
});
diff --git a/compiler/src/iree/compiler/Dialect/Util/Transforms/test/hoist_into_globals.mlir b/compiler/src/iree/compiler/Dialect/Util/Transforms/test/hoist_into_globals.mlir
index e2502b1..2fd972e 100644
--- a/compiler/src/iree/compiler/Dialect/Util/Transforms/test/hoist_into_globals.mlir
+++ b/compiler/src/iree/compiler/Dialect/Util/Transforms/test/hoist_into_globals.mlir
@@ -391,6 +391,93 @@
// -----
+// A const-expr that is nested *inside* a dispatch region must not be hoisted:
+// doing so would place a `util.global.load` inside the dispatch region, which
+// later gets outlined into an isolated executable where the global is not
+// visible (producing an "undefined global" error). Here the constants are
+// defined inside the dispatch and the region result is captured into a second
+// dispatch, so the inner const-expr (not the whole region) would otherwise be
+// the hoisting candidate.
+
+// CHECK-LABEL: @do_not_hoist_const_expr_nested_in_dispatch
+module @do_not_hoist_const_expr_nested_in_dispatch {
+ // CHECK-NOT: util.global
+ // CHECK-NOT: util.initializer
+ util.func public @main(%arg0: tensor<2x2xf32>) -> tensor<2x2xf32> {
+ // CHECK: flow.dispatch.region
+ // The inner const-expr stays inside the dispatch; no global load is placed
+ // inside the region.
+ // CHECK-NOT: util.global.load
+ // CHECK: linalg.generic
+ // CHECK: arith.mulf
+ %0 = flow.dispatch.region -> (tensor<2x2xf32>) {
+ %cst0 = arith.constant dense<[[1.0, 2.0], [3.0, 4.0]]> : tensor<2x2xf32>
+ %cst1 = arith.constant dense<[[5.0, 6.0], [7.0, 8.0]]> : tensor<2x2xf32>
+ %empty = tensor.empty() : tensor<2x2xf32>
+ %g = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0, d1)>], iterator_types = ["parallel", "parallel"]} ins(%cst0, %cst1 : tensor<2x2xf32>, tensor<2x2xf32>) outs(%empty : tensor<2x2xf32>) {
+ ^bb0(%a: f32, %b: f32, %o: f32):
+ %m = arith.mulf %a, %b : f32
+ linalg.yield %m : f32
+ } -> tensor<2x2xf32>
+ flow.return %g : tensor<2x2xf32>
+ }
+ %1 = flow.dispatch.region -> (tensor<2x2xf32>) {
+ %empty = tensor.empty() : tensor<2x2xf32>
+ %r = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0, d1)>], iterator_types = ["parallel", "parallel"]} ins(%0, %arg0 : tensor<2x2xf32>, tensor<2x2xf32>) outs(%empty : tensor<2x2xf32>) {
+ ^bb0(%a: f32, %b: f32, %o: f32):
+ %s = arith.addf %a, %b : f32
+ linalg.yield %s : f32
+ } -> tensor<2x2xf32>
+ flow.return %r : tensor<2x2xf32>
+ }
+ util.return %1 : tensor<2x2xf32>
+ }
+}
+
+// -----
+
+// A whole flow.dispatch.region that is itself a const-expr (all of its captured
+// operands are constant) is still hoisted as a unit: the entire region moves
+// into the initializer and a single util.global.load replaces it in the
+// function body. The hoisting walk performs this hoist *before* it stops
+// descending into the region body, so whole-region hoisting keeps working; and
+// because the body is then skipped, no extra util.global.load is stranded
+// inside the region for the nested const-expr (which would become an undefined
+// global once the region is outlined). This complements
+// @do_not_hoist_const_expr_nested_in_dispatch above.
+
+// CHECK-LABEL: @hoist_whole_dispatch_region_skips_body
+module @hoist_whole_dispatch_region_skips_body {
+ // CHECK: util.global private @[[SYM:.+]] : tensor<2x2xi32>
+ // CHECK: util.initializer {
+ // CHECK: %[[DISPATCH:.+]] = flow.dispatch.region -> (tensor<2x2xi32>) {
+ // CHECK: linalg.generic
+ // CHECK-NOT: util.global.load
+ // CHECK: flow.return
+ // CHECK: }
+ // CHECK: util.global.store %[[DISPATCH]], @[[SYM]] : tensor<2x2xi32>
+ // CHECK: util.return
+ // CHECK: }
+ // CHECK: util.func public @main
+ util.func public @main() -> tensor<2x2xi32> {
+ %cst = arith.constant dense<[[1, 2], [3, 4]]> : tensor<2x2xi32>
+ %empty = tensor.empty() : tensor<2x2xi32>
+ // CHECK: %[[VAL:.+]] = util.global.load immutable @[[SYM]] : tensor<2x2xi32>
+ // CHECK: util.return %[[VAL]]
+ %0 = flow.dispatch.region -> (tensor<2x2xi32>) {
+ %g = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0, d1)>], iterator_types = ["parallel", "parallel"]} ins(%cst : tensor<2x2xi32>) outs(%empty : tensor<2x2xi32>) {
+ ^bb0(%a: i32, %o: i32):
+ %m = arith.addi %a, %a : i32
+ linalg.yield %m : i32
+ } -> tensor<2x2xi32>
+ flow.return %g : tensor<2x2xi32>
+ }
+ util.return %0 : tensor<2x2xi32>
+ }
+}
+
+// -----
+
// The --iree-util-const-expr-max-size-increase-threshold flag controls the
// maximum size increase (vs sum of size of it's roots) allowed for hoisting a
// constant expression. The threshold is set to 64 bytes in this test suite.