[Codegen][DispatchCreation] Separate dispatch for iree_linalg_ext.scan producers (#24651)

This fix is necessitated by lack of fusion support for LinalgExt Scan -
since the default LLVMCPU tiling does not respect the fusability of
producer ops, nor the limit for stack-bound allocations, we force this
non-fusable "cumulative reduction"-style operation to be dispatched
separately from non-trivial linalg operations.

---------

Co-authored-by: Artem Gindinson <gindinson@roofline.ai>
diff --git a/compiler/src/iree/compiler/Dialect/Flow/Transforms/RegionOpUtils.cpp b/compiler/src/iree/compiler/Dialect/Flow/Transforms/RegionOpUtils.cpp
index be39b20..3ab05b9 100644
--- a/compiler/src/iree/compiler/Dialect/Flow/Transforms/RegionOpUtils.cpp
+++ b/compiler/src/iree/compiler/Dialect/Flow/Transforms/RegionOpUtils.cpp
@@ -779,6 +779,10 @@
   return false;
 }
 
+static bool hasExplicitNonFusableUsers(Operation *op) {
+  return llvm::any_of(op->getUsers(), llvm::IsaPred<IREE::LinalgExt::ScanOp>);
+}
+
 /// Operations that are cloned into dispatch regions formed with other
 /// operations as roots.
 bool isCloneableIntoDispatchOp(Operation *op,
@@ -795,6 +799,11 @@
           complex::CreateOp, IREE::Encoding::UnsetEncodingOp>(op)) {
     return true;
   }
+  // TODO: Tune the cases excluded through hasExplicitNonFusableUsers
+  // condition in a more targeted manner, then remove the condition.
+  if (isa<linalg::LinalgOp>(op) && hasExplicitNonFusableUsers(op)) {
+    return false;
+  }
   if (LinalgExt::isBitExtendOp(op)) {
     return true;
   }
diff --git a/compiler/src/iree/compiler/DispatchCreation/FormDispatchRegions.cpp b/compiler/src/iree/compiler/DispatchCreation/FormDispatchRegions.cpp
index 91f4b9c..cf4f535 100644
--- a/compiler/src/iree/compiler/DispatchCreation/FormDispatchRegions.cpp
+++ b/compiler/src/iree/compiler/DispatchCreation/FormDispatchRegions.cpp
@@ -888,6 +888,13 @@
 
   // Once all root linalg ops have been tagged, put all remaining generic ops
   // into their own dispatches.
+  // TODO: when identyfing root operations, there are multiple
+  // isFusableWith[Producer|Consumer] checks being invoked, however we only
+  // access the positive results of those checks (i.e. explicit assignment to
+  // the fusion group). We should consider assigning *all* operations to fusion
+  // groups during the fuseRootsWith...() phase, creating new groups for more
+  // negative fusion cases, and potentially eliding the second iteration over
+  // the region altogether.
   for (Block &block : region) {
     SmallVector<Operation *> roots;
     for (Operation &op : llvm::reverse(block)) {
diff --git a/compiler/src/iree/compiler/DispatchCreation/test/dispatch_linalg_ext_fusion.mlir b/compiler/src/iree/compiler/DispatchCreation/test/dispatch_linalg_ext_fusion.mlir
index aa25951..0f50ebe 100644
--- a/compiler/src/iree/compiler/DispatchCreation/test/dispatch_linalg_ext_fusion.mlir
+++ b/compiler/src/iree/compiler/DispatchCreation/test/dispatch_linalg_ext_fusion.mlir
@@ -1,4 +1,7 @@
-// RUN: iree-opt --split-input-file --pass-pipeline="builtin.module(util.func(iree-dispatch-creation-form-dispatch-regions{aggressive-fusion=true}, iree-dispatch-creation-clone-producers-into-dispatch-regions{aggressive=true}), cse, canonicalize, cse)" %s | FileCheck %s
+// RUN: iree-opt %s --split-input-file \
+// RUN: --pass-pipeline="builtin.module(util.func(iree-dispatch-creation-form-dispatch-regions{aggressive-fusion=true}, \
+// RUN:                  iree-dispatch-creation-clone-producers-into-dispatch-regions{aggressive=true}), cse, canonicalize, cse)" \
+// RUN: | FileCheck %s
 
 #map = affine_map<(d0, d1) -> (d0, d1)>
 #map1 = affine_map<(d0, d1, d2, d3, d4) -> (d0, d1, d2, d3, d4)>
@@ -175,3 +178,42 @@
 //       CHECK:         %[[GEN2:.+]] = linalg.generic
 //  CHECK-SAME:           ins(%[[ATTN]]
 //       CHECK:         flow.return %[[GEN2]]
+
+// -----
+
+// Check that a simple elementwise bit extend producer is assigned to a separate dispatch
+// (until fusion is supported)
+#map = affine_map<(d0, d1) -> (d0, d1)>
+util.func public @linalgext_scan_inclusive_dispatch_non_fusable(%arg0: tensor<8x32xi32>) -> tensor<8x32xi64> {
+  %c0_i64 = arith.constant 0 : i64
+  %0 = tensor.empty() : tensor<8x32xi64>
+  %1 = linalg.generic {indexing_maps = [#map, #map], iterator_types = ["parallel", "parallel"]} ins(%arg0 : tensor<8x32xi32>) outs(%0 : tensor<8x32xi64>) {
+  ^bb0(%in: i32, %out: i64):
+    %6 = arith.extsi %in : i32 to i64
+    linalg.yield %6 : i64
+  } -> tensor<8x32xi64>
+  %2 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<8x32xi64>) -> tensor<8x32xi64>
+  %3 = tensor.empty() : tensor<8xi64>
+  %4 = linalg.fill ins(%c0_i64 : i64) outs(%3 : tensor<8xi64>) -> tensor<8xi64>
+  %5:2 = iree_linalg_ext.scan dimension(1) inclusive(true) ins(%1 : tensor<8x32xi64>) outs(%2, %4 : tensor<8x32xi64>, tensor<8xi64>) {
+  ^bb0(%arg3: i64, %arg4: i64):
+    %6 = arith.addi %arg3, %arg4 : i64
+    iree_linalg_ext.yield %6 : i64
+  } -> tensor<8x32xi64>, tensor<8xi64>
+  util.return %5#0 : tensor<8x32xi64>
+}
+
+// CHECK-LABEL:     util.func public @linalgext_scan_inclusive_dispatch_non_fusable(
+//  CHECK-SAME:         %[[ARG:.+]]: tensor<8x32xi32>) -> tensor<8x32xi64>
+//       CHECK:       %[[ZERO_CONST:.+]] = arith.constant 0 : i64
+//       CHECK:       %[[PRODUCER_REGION:.+]] = flow.dispatch.region -> (tensor<8x32xi64>)
+//       CHECK:         %[[EMPTY:.+]] = tensor.empty()
+//       CHECK:         %[[PRODUCER:.+]] = linalg.generic {{.+}} ins(%[[ARG]] : tensor<8x32xi32>) outs(%[[EMPTY]] : tensor<8x32xi64>)
+//       CHECK:         flow.return %[[PRODUCER]]
+//       CHECK:       %[[LINALGEXT_REGION:.+]] = flow.dispatch.region -> (tensor<8x32xi64>)
+//       CHECK:         %[[CUMULATIVE_FILL:.+]] = linalg.fill ins(%[[ZERO_CONST]] : i64) outs(%{{.+}} : tensor<8x32xi64>)
+//       CHECK:         %[[REDUCED_FILL:.+]] = linalg.fill ins(%[[ZERO_CONST]] : i64) outs(%{{.+}} : tensor<8xi64>)
+//       CHECK:         %[[SCAN_RESULT:.+]]:2 = iree_linalg_ext.scan dimension(1) inclusive(true)
+//  CHECK-SAME:             ins(%[[PRODUCER_REGION]] : tensor<8x32xi64>) outs(%[[CUMULATIVE_FILL]], %[[REDUCED_FILL]] : {{.+}}) {
+//       CHECK:         flow.return %[[SCAN_RESULT]]#0
+//       CHECK:       util.return %[[LINALGEXT_REGION]] : tensor<8x32xi64>