[DispatchCreation] hoist entire chains of set_encoding ops (#24904)

Capture the entire producer chains of `set_encoding` ops, and hoist them
if there is no unhoistable op in the source backward slice. The metadata
backward slice, i.e. the backward slice from the `encoding_dims` can
always be hoisted.

Fixes #24876.

Assisted-by: Claude Code

---------

Signed-off-by: Ege Beysel <beyselege@gmail.com>
diff --git a/compiler/src/iree/compiler/DispatchCreation/HoistEncodingOps.cpp b/compiler/src/iree/compiler/DispatchCreation/HoistEncodingOps.cpp
index 9c3b674..81312b9 100644
--- a/compiler/src/iree/compiler/DispatchCreation/HoistEncodingOps.cpp
+++ b/compiler/src/iree/compiler/DispatchCreation/HoistEncodingOps.cpp
@@ -5,7 +5,6 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include <cassert>
-#include <queue>
 
 #include "iree/compiler/Dialect/Encoding/IR/EncodingDialect.h"
 #include "iree/compiler/Dialect/Encoding/IR/EncodingOps.h"
@@ -22,6 +21,7 @@
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/DebugLog.h"
+#include "mlir/Analysis/SliceAnalysis.h"
 #include "mlir/Dialect/Linalg/IR/Linalg.h"
 #include "mlir/Dialect/Linalg/Utils/Utils.h"
 #include "mlir/Dialect/MemRef/Transforms/Transforms.h"
@@ -329,36 +329,38 @@
       return;
     }
 
+    // Every op within the dispatch that the SetEncodingOp transitively depends
+    // on (data and metadata) has to be hoisted along with it. While computing
+    // the slice, record the ops on the data path, i.e. the transitive defs of
+    // the source. The source operand is traversed before the encoding dims.
+    Operation *sourceOp = setEncodingOp.getSource().getDefiningOp();
+    DenseSet<Operation *> dataPathOps;
+    BackwardSliceOptions sliceOptions;
     bool isHoistable = true;
-    SetVector<Operation *> opsWithinDispatch, seen;
-    std::queue<Operation *> worklist;
-    worklist.push(setEncodingOp);
-    seen.insert(setEncodingOp);
-    while (!worklist.empty()) {
-      Operation *op = worklist.front();
-      worklist.pop();
-      opsWithinDispatch.insert(op);
-      // For SetEncodingOp, the encoding_dims operands are metadata and should
-      // not block hoistability. We still add their defining ops to the worklist
-      // so they get hoisted, but only the source operand affects hoistability.
-      auto setEnc = dyn_cast<IREE::Encoding::SetEncodingOp>(op);
-      for (Value input : op->getOperands()) {
-        auto inputOp = input.getDefiningOp();
-        if (inputOp &&
-            inputOp->getParentOfType<IREE::Flow::DispatchRegionOp>() &&
-            !seen.contains(inputOp)) {
-          // For SetEncodingOp, only the source operand affects hoistability.
-          bool affectsHoistability = !setEnc || input == setEnc.getSource();
-          if (affectsHoistability && !isHoistableOp(inputOp)) {
-            isHoistable = false;
-          }
-          worklist.push(inputOp);
-          seen.insert(inputOp);
-        }
+    sliceOptions.inclusive = true;
+    sliceOptions.omitBlockArguments = true;
+    sliceOptions.filter = [&dataPathOps, &isHoistable,
+                           &sourceOp](Operation *op) {
+      if (!op->getParentOfType<IREE::Flow::DispatchRegionOp>()) {
+        return false;
       }
-    }
+      if (op == sourceOp || llvm::any_of(op->getUsers(), [&](Operation *user) {
+            return dataPathOps.contains(user);
+          })) {
+        dataPathOps.insert(op);
+        // Only the ops on the data path gate hoistability. Ops that are only
+        // reachable through `encoding_dims` are metadata and come along for
+        // free.
+        isHoistable &= isHoistableOp(op);
+      }
+      return true;
+    };
+    SetVector<Operation *> opsWithinDispatch;
+    (void)getBackwardSlice(setEncodingOp.getOperation(), &opsWithinDispatch,
+                           sliceOptions);
+
     if (isHoistable) {
-      candidates.push_back(llvm::to_vector(llvm::reverse(opsWithinDispatch)));
+      candidates.push_back(llvm::to_vector(opsWithinDispatch));
       return;
     }
 
@@ -371,7 +373,7 @@
     }
     if (policy.getDecision(constInfo)->getOutcome() ==
         IREE::Util::ConstExprHoistingPolicy::ENABLE_HOIST) {
-      candidates.push_back(llvm::to_vector(llvm::reverse(opsWithinDispatch)));
+      candidates.push_back(llvm::to_vector(opsWithinDispatch));
       return;
     }
     LDBG() << "Non-hoistable op: " << setEncodingOp;
diff --git a/compiler/src/iree/compiler/DispatchCreation/test/hoist_encoding_ops.mlir b/compiler/src/iree/compiler/DispatchCreation/test/hoist_encoding_ops.mlir
index 88d2381..e205928 100644
--- a/compiler/src/iree/compiler/DispatchCreation/test/hoist_encoding_ops.mlir
+++ b/compiler/src/iree/compiler/DispatchCreation/test/hoist_encoding_ops.mlir
@@ -1,4 +1,4 @@
-// RUN: iree-opt --pass-pipeline="builtin.module(iree-dispatch-creation-hoist-encoding-ops,cse)" --split-input-file %s | FileCheck %s
+// RUN: iree-opt --pass-pipeline="builtin.module(iree-dispatch-creation-hoist-encoding-ops,cse)" --split-input-file --allow-unregistered-dialect %s | FileCheck %s
 
 #map1 = affine_map<(d0, d1, d2, d3) -> (d0, d3, d2)>
 #map2 = affine_map<(d0, d1, d2, d3) -> (d0, d1, d3)>
@@ -785,7 +785,7 @@
 // CHECK-DAG:   %[[C0:.+]] = arith.constant 0 : index
 // CHECK-DAG:   %[[C128:.+]] = arith.constant 128 : index
 // CHECK-DAG:   %[[C11008:.+]] = arith.constant 11008 : index
-// CHECK:       %[[DIM:.+]] = tensor.dim %{{.*}}, %[[C0]]
+// CHECK-DAG:   %[[DIM:.+]] = tensor.dim %{{.*}}, %[[C0]]
 // CHECK-DAG:   iree_encoding.set_encoding %{{.*}} encoding_dims{%[[DIM]], %[[C11008]], %[[C128]]}
 // CHECK-DAG:   iree_encoding.set_encoding %{{.*}} encoding_dims{%[[DIM]], %[[C11008]], %[[C128]]}
 // CHECK-DAG:   iree_encoding.set_encoding %{{.*}} encoding_dims{%[[DIM]], %[[C11008]], %[[C128]]}
@@ -1115,3 +1115,81 @@
 // CHECK:         flow.return %[[DEQUANT]]
 // CHECK:       }
 // CHECK:       util.return %[[DISPATCH]]
+
+// -----
+
+// `encoding_dims` are produced inside the dispatch by tensor.dim / affine.apply
+// of values defined outside the dispatch. These ops only feed metadata, so they
+// must not block hoisting of the set_encoding ops.
+
+#map_md = affine_map<(d0, d1, d2) -> (d0, d2)>
+#map_md1 = affine_map<(d0, d1, d2) -> (d2, d1)>
+#map_md2 = affine_map<(d0, d1, d2) -> (d0, d1)>
+#map_md3 = affine_map<()[s0] -> (s0 * 8)>
+#enc_md_lhs = #iree_encoding.encoding<operand_index = 0 : index, op_type =  matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map_md, #map_md1, #map_md2], iteration_sizes = [?, 256, ?]>
+#enc_md_rhs = #iree_encoding.encoding<operand_index = 1 : index, op_type =  matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map_md, #map_md1, #map_md2], iteration_sizes = [?, 256, ?]>
+#enc_md_out = #iree_encoding.encoding<operand_index = 2 : index, op_type =  matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map_md, #map_md1, #map_md2], iteration_sizes = [?, 256, ?]>
+util.func public @hoist_matmul_with_metadata_encoding_dims(
+    %reshape: tensor<8x?x?xf32>, %collapsed: tensor<?x?xf32>,
+    %rhs: tensor<?x256xf32>, %out_sz: index) -> tensor<?x256xf32> {
+  %0 = flow.dispatch.region -> (tensor<?x256xf32>{%out_sz}) {
+    %cst = arith.constant 0.000000e+00 : f32
+    %c1 = arith.constant 1 : index
+    %c2 = arith.constant 2 : index
+    %dim1 = tensor.dim %reshape, %c1 : tensor<8x?x?xf32>
+    %m = affine.apply #map_md3()[%dim1]
+    %k = tensor.dim %reshape, %c2 : tensor<8x?x?xf32>
+    %lhs_enc = iree_encoding.set_encoding %collapsed encoding_dims{%m, %k} : tensor<?x?xf32> -> tensor<?x?xf32, #enc_md_lhs>
+    %rhs_enc = iree_encoding.set_encoding %rhs encoding_dims{%m, %k} : tensor<?x256xf32> -> tensor<?x256xf32, #enc_md_rhs>
+    %init = tensor.empty(%out_sz) : tensor<?x256xf32, #enc_md_out>
+    %fill = linalg.fill ins(%cst : f32) outs(%init : tensor<?x256xf32, #enc_md_out>) -> tensor<?x256xf32, #enc_md_out>
+    %mm = linalg.matmul ins(%lhs_enc, %rhs_enc : tensor<?x?xf32, #enc_md_lhs>, tensor<?x256xf32, #enc_md_rhs>) outs(%fill : tensor<?x256xf32, #enc_md_out>) -> tensor<?x256xf32, #enc_md_out>
+    %unset = iree_encoding.unset_encoding %mm encoding_dims{%m, %k} : tensor<?x256xf32, #enc_md_out> -> tensor<?x256xf32>{%out_sz}
+    flow.return %unset : tensor<?x256xf32>
+  }
+  util.return %0 : tensor<?x256xf32>
+}
+// CHECK-LABEL: @hoist_matmul_with_metadata_encoding_dims
+// CHECK-SAME:    %[[RESHAPE:.+]]: tensor<8x?x?xf32>, %[[COLLAPSED:.+]]: tensor<?x?xf32>, %[[RHS:.+]]: tensor<?x256xf32>
+// The tensor.dim / affine.apply that produce the encoding_dims hoist out
+// alongside the set_encoding ops.
+// CHECK-DAG:   %[[DIM1:.+]] = tensor.dim %[[RESHAPE]], %c1
+// CHECK-DAG:   %[[DIM2:.+]] = tensor.dim %[[RESHAPE]], %c2
+// CHECK-DAG:   %[[M:.+]] = affine.apply {{.*}}[%[[DIM1]]]
+// CHECK-DAG:   iree_encoding.set_encoding %[[COLLAPSED]] encoding_dims{%[[M]], %[[DIM2]]}
+// CHECK-DAG:   iree_encoding.set_encoding %[[RHS]] encoding_dims{%[[M]], %[[DIM2]]}
+// CHECK:       flow.dispatch.region
+// CHECK:         linalg.matmul
+// CHECK:         iree_encoding.unset_encoding
+// CHECK:         flow.return
+
+// -----
+
+// Mixed-chain rejection: `some_unhoistable_op` is shared between the metadata path
+// (it is an encoding_dim of the SetEncodingOp) and the data path (it is a
+// dynamic size of the tensor.extract_slice that becomes the SetEncodingOp's
+// source).
+
+#map_rej = affine_map<(d0, d1, d2) -> (d0, d2)>
+#map_rej1 = affine_map<(d0, d1, d2) -> (d2, d1)>
+#map_rej2 = affine_map<(d0, d1, d2) -> (d0, d1)>
+#enc_rej = #iree_encoding.encoding<operand_index = 0 : index, op_type =  matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map_rej, #map_rej1, #map_rej2], iteration_sizes = [?, 256, 256]>
+util.func public @no_hoist_when_data_path_shares_metadata_op(
+    %src: tensor<?x256xf32>, %m: index) -> tensor<?x256xf32> {
+  %0 = flow.dispatch.region -> (tensor<?x256xf32>{%m}) {
+    %c0 = arith.constant 0 : index
+    %d = "some_unhoistable_op"(%src) : (tensor<?x256xf32>) -> index
+    %slice = tensor.extract_slice %src[0, 0] [%d, 256] [1, 1] : tensor<?x256xf32> to tensor<?x256xf32>
+    %enc = iree_encoding.set_encoding %slice encoding_dims{%d} : tensor<?x256xf32> -> tensor<?x256xf32, #enc_rej>
+    %unset = iree_encoding.unset_encoding %enc encoding_dims{%d} : tensor<?x256xf32, #enc_rej> -> tensor<?x256xf32>{%m}
+    flow.return %unset : tensor<?x256xf32>
+  }
+  util.return %0 : tensor<?x256xf32>
+}
+// CHECK-LABEL: @no_hoist_when_data_path_shares_metadata_op
+// CHECK:       %[[DISPATCH:.+]] = flow.dispatch.region
+// CHECK:         %[[DIM:.+]] = "some_unhoistable_op"
+// CHECK:         %[[SLICE:.+]] = tensor.extract_slice
+// CHECK:         %[[SET:.+]] = iree_encoding.set_encoding %[[SLICE]] encoding_dims{%[[DIM]]}
+// CHECK:         iree_encoding.unset_encoding %[[SET]] encoding_dims{%[[DIM]]}
+// CHECK:         flow.return