[LinalgExt] Implement CustomOp::getStaticLoopRanges (#24734)

CustomOp implements LinalgFusionInterface but does not provide static
loop ranges, so dispatch formation has to exclude it from several fusion
checks.

Compute the ranges from the indexing maps and operand shapes, matching
the existing iteration-domain calculation. Remove the CustomOp-specific
exclusions and cover both static and dynamic shapes.

Fixes #23345

Signed-off-by: zhangpan <98025960+zhangpan2001@users.noreply.github.com>
Co-authored-by: OpenAI <noreply@openai.com>
diff --git a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtInterfaces.td b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtInterfaces.td
index e6a91dd..fb748f0 100644
--- a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtInterfaces.td
+++ b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtInterfaces.td
@@ -133,7 +133,8 @@
     >,
     InterfaceMethod<
       /*desc=*/[{
-        Return the static loop ranges.
+        Return one range per logical loop. Returns `ShapedType::kDynamic` for
+        ranges that cannot be statically recovered.
       }],
       /*retTy=*/"SmallVector<int64_t>",
       /*methodName=*/"getStaticLoopRanges",
diff --git a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.cpp b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.cpp
index 0016d82..0a9b7d0 100644
--- a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.cpp
+++ b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.cpp
@@ -3311,6 +3311,39 @@
       [](Attribute attr) { return cast<AffineMapAttr>(attr).getValue(); });
 }
 
+SmallVector<int64_t> CustomOp::getStaticLoopRanges() {
+  SmallVector<AffineMap> maps = getIndexingMapsArray();
+  SmallVector<int64_t> loopRanges(getNumLoops(), ShapedType::kDynamic);
+  SmallVector<bool> conflictingRanges(getNumLoops(), false);
+
+  for (auto [operand, map] :
+       llvm::zip_equal(getOperation()->getOpOperands(), maps)) {
+    if (map.isEmpty()) {
+      continue;
+    }
+
+    for (auto [size, expr] :
+         llvm::zip_equal(getShape(&operand), map.getResults())) {
+      auto dimExpr = dyn_cast<AffineDimExpr>(expr);
+      if (!dimExpr || ShapedType::isDynamic(size)) {
+        continue;
+      }
+
+      unsigned position = dimExpr.getPosition();
+      if (conflictingRanges[position]) {
+        continue;
+      }
+      if (ShapedType::isDynamic(loopRanges[position])) {
+        loopRanges[position] = size;
+      } else if (loopRanges[position] != size) {
+        loopRanges[position] = ShapedType::kDynamic;
+        conflictingRanges[position] = true;
+      }
+    }
+  }
+  return loopRanges;
+}
+
 /// End `LinalgFusionInterface` implementation
 
 /// Start `ReifyRankedShapedTypeOpInterface` implementation
diff --git a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.td b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.td
index c0fc2db..7f6ec1c 100644
--- a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.td
+++ b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.td
@@ -1994,7 +1994,7 @@
   AttrSizedOperandSegments,
   DeclareOpInterfaceMethods<AggregatedOpInterface, [
     "decomposeOperation"]>,
-  DeclareOpInterfaceMethods<LinalgFusionInterface>,
+  DeclareOpInterfaceMethods<LinalgFusionInterface, ["getStaticLoopRanges"]>,
   DeclareOpInterfaceMethods<ReifyRankedShapedTypeOpInterface, ["reifyResultShapes"]>,
   DeclareOpInterfaceMethods<TilingInterface,
     ["getIterationDomain",
diff --git a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/BUILD.bazel b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/BUILD.bazel
new file mode 100644
index 0000000..7fd48fe
--- /dev/null
+++ b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/BUILD.bazel
@@ -0,0 +1,26 @@
+# Copyright 2026 The IREE Authors
+#
+# Licensed 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
+
+load("//build_tools/bazel:build_defs.oss.bzl", "iree_compiler_cc_test")
+
+package(
+    features = ["layering_check"],
+    licenses = ["notice"],  # Apache 2.0
+)
+
+iree_compiler_cc_test(
+    name = "LinalgExtOpsTest",
+    testonly = True,
+    srcs = ["LinalgExtOpsTest.cpp"],
+    deps = [
+        "//compiler/src/iree/compiler/Dialect/LinalgExt/IR",
+        "//compiler/src/iree/testing:gtest_main",
+        "@com_google_googletest//:gtest",
+        "@llvm-project//mlir:FuncDialect",
+        "@llvm-project//mlir:IR",
+        "@llvm-project//mlir:Parser",
+    ],
+)
diff --git a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/CMakeLists.txt b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/CMakeLists.txt
new file mode 100644
index 0000000..f258a6f
--- /dev/null
+++ b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/CMakeLists.txt
@@ -0,0 +1,28 @@
+################################################################################
+# Autogenerated by build_tools/bazel_to_cmake/bazel_to_cmake.py from           #
+# compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/BUILD.bazel        #
+#                                                                              #
+# Use iree_cmake_extra_content from iree/build_defs.oss.bzl to add arbitrary   #
+# CMake-only content.                                                          #
+#                                                                              #
+# To disable autogeneration for this file entirely, delete this header.        #
+################################################################################
+
+iree_add_all_subdirs()
+
+iree_cc_test(
+  NAME
+    LinalgExtOpsTest
+  SRCS
+    "LinalgExtOpsTest.cpp"
+  DEPS
+    MLIRFuncDialect
+    MLIRIR
+    MLIRParser
+    gmock
+    gtest
+    iree::compiler::Dialect::LinalgExt::IR
+    iree::testing::gtest_main
+)
+
+### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_BELOW_THIS_LINE ###
diff --git a/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/LinalgExtOpsTest.cpp b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/LinalgExtOpsTest.cpp
new file mode 100644
index 0000000..8889af5
--- /dev/null
+++ b/compiler/src/iree/compiler/Dialect/LinalgExt/IR/unittests/LinalgExtOpsTest.cpp
@@ -0,0 +1,143 @@
+// Copyright 2026 The IREE Authors
+//
+// Licensed 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 <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtDialect.h"
+#include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.h"
+#include "mlir/Dialect/Func/IR/FuncOps.h"
+#include "mlir/IR/BuiltinOps.h"
+#include "mlir/IR/Verifier.h"
+#include "mlir/Parser/Parser.h"
+
+namespace mlir::iree_compiler::IREE::LinalgExt {
+namespace {
+
+using ::testing::ElementsAre;
+
+class LinalgExtOpsTest : public ::testing::Test {
+protected:
+  LinalgExtOpsTest() {
+    registry.insert<IREELinalgExtDialect, func::FuncDialect>();
+    context.appendDialectRegistry(registry);
+    context.loadAllAvailableDialects();
+  }
+
+  MLIRContext *getContext() { return &context; }
+
+private:
+  MLIRContext context;
+  DialectRegistry registry;
+};
+
+static CustomOp findCustomOp(ModuleOp module, StringRef functionName) {
+  func::FuncOp function = module.lookupSymbol<func::FuncOp>(functionName);
+  if (!function) {
+    return {};
+  }
+  auto customOps = function.getOps<CustomOp>();
+  return customOps.empty() ? CustomOp{} : *customOps.begin();
+}
+
+TEST_F(LinalgExtOpsTest, GetStaticLoopRanges) {
+  OwningOpRef<ModuleOp> module = parseSourceString<ModuleOp>(
+      R"mlir(
+module {
+  func.func @symbol_dimensions(
+      %input : tensor<4x16xf32>, %output : tensor<4x16xf32>)
+      -> tensor<4x16xf32> {
+    %0 = iree_linalg_ext.custom_op {
+        indexing_maps = [affine_map<(d0)[s0] -> (d0, s0)>,
+                         affine_map<(d0)[s0] -> (d0, s0)>],
+        iterator_types = [#iree_linalg_ext.iterator_type<parallel>]}
+        ins(%input : tensor<4x16xf32>)
+        outs(%output : tensor<4x16xf32>) {
+      ^bb0(%input_slice : tensor<?x?xf32>,
+           %output_slice : tensor<?x?xf32>):
+        iree_linalg_ext.yield %output_slice : tensor<?x?xf32>
+    } -> tensor<4x16xf32>
+    return %0 : tensor<4x16xf32>
+  }
+
+  func.func @partial_static_loop_ranges(
+      %input : tensor<7xf32>, %output : tensor<4xf32>) -> tensor<4xf32> {
+    %0 = iree_linalg_ext.custom_op {
+        indexing_maps = [affine_map<(d0, d1) -> (d0 + d1)>,
+                         affine_map<(d0, d1) -> (d0)>],
+        iterator_types = [#iree_linalg_ext.iterator_type<parallel>,
+                          #iree_linalg_ext.iterator_type<reduction>]}
+        ins(%input : tensor<7xf32>) outs(%output : tensor<4xf32>) {
+      ^bb0(%input_slice : tensor<?xf32>, %output_slice : tensor<?xf32>):
+        iree_linalg_ext.yield %output_slice : tensor<?xf32>
+    } -> tensor<4xf32>
+    return %0 : tensor<4xf32>
+  }
+
+  func.func @dynamic_and_static(
+      %input : tensor<?xf32>, %output : tensor<4xf32>) -> tensor<4xf32> {
+    %0 = iree_linalg_ext.custom_op {
+        indexing_maps = [affine_map<(d0) -> (d0)>,
+                         affine_map<(d0) -> (d0)>],
+        iterator_types = [#iree_linalg_ext.iterator_type<parallel>]}
+        ins(%input : tensor<?xf32>) outs(%output : tensor<4xf32>) {
+      ^bb0(%input_slice : tensor<?xf32>, %output_slice : tensor<?xf32>):
+        iree_linalg_ext.yield %output_slice : tensor<?xf32>
+    } -> tensor<4xf32>
+    return %0 : tensor<4xf32>
+  }
+
+  func.func @conflicting_static(
+      %input : tensor<4xf32>, %output : tensor<8xf32>) -> tensor<8xf32> {
+    %0 = iree_linalg_ext.custom_op {
+        indexing_maps = [affine_map<(d0) -> (d0)>,
+                         affine_map<(d0) -> (d0)>],
+        iterator_types = [#iree_linalg_ext.iterator_type<parallel>]}
+        ins(%input : tensor<4xf32>) outs(%output : tensor<8xf32>) {
+      ^bb0(%input_slice : tensor<?xf32>, %output_slice : tensor<?xf32>):
+        iree_linalg_ext.yield %output_slice : tensor<?xf32>
+    } -> tensor<8xf32>
+    return %0 : tensor<8xf32>
+  }
+
+  func.func @empty_map(%input : tensor<4xf32>,
+      %ignored : tensor<5x6xf32>, %output : tensor<4xf32>)
+      -> tensor<4xf32> {
+    %0 = iree_linalg_ext.custom_op {
+        indexing_maps = [affine_map<(d0) -> (d0)>, affine_map<() -> ()>,
+                         affine_map<(d0) -> (d0)>],
+        iterator_types = [#iree_linalg_ext.iterator_type<parallel>]}
+        ins(%input, %ignored : tensor<4xf32>, tensor<5x6xf32>)
+        outs(%output : tensor<4xf32>) {
+      ^bb0(%input_slice : tensor<?xf32>,
+           %ignored_slice : tensor<?x?xf32>,
+           %output_slice : tensor<?xf32>):
+        iree_linalg_ext.yield %output_slice : tensor<?xf32>
+    } -> tensor<4xf32>
+    return %0 : tensor<4xf32>
+  }
+}
+)mlir",
+      getContext());
+
+  ASSERT_TRUE(module);
+  ASSERT_TRUE(succeeded(verify(*module)));
+
+  EXPECT_THAT(findCustomOp(*module, "symbol_dimensions").getStaticLoopRanges(),
+              ElementsAre(4));
+  EXPECT_THAT(
+      findCustomOp(*module, "partial_static_loop_ranges").getStaticLoopRanges(),
+      ElementsAre(4, ShapedType::kDynamic));
+  EXPECT_THAT(findCustomOp(*module, "dynamic_and_static").getStaticLoopRanges(),
+              ElementsAre(4));
+  EXPECT_THAT(findCustomOp(*module, "conflicting_static").getStaticLoopRanges(),
+              ElementsAre(ShapedType::kDynamic));
+  EXPECT_THAT(findCustomOp(*module, "empty_map").getStaticLoopRanges(),
+              ElementsAre(4));
+}
+
+} // namespace
+} // namespace mlir::iree_compiler::IREE::LinalgExt
diff --git a/compiler/src/iree/compiler/DispatchCreation/FormDispatchRegions.cpp b/compiler/src/iree/compiler/DispatchCreation/FormDispatchRegions.cpp
index cf4f535..20d3a06 100644
--- a/compiler/src/iree/compiler/DispatchCreation/FormDispatchRegions.cpp
+++ b/compiler/src/iree/compiler/DispatchCreation/FormDispatchRegions.cpp
@@ -580,11 +580,6 @@
   // TODO(#12664): This is unnecessary requirement, but we need a better config
   // to tile the consumer with a larger iteration space.
   if (!options.aggressiveFusion) {
-    // FIXME: Implement getStaticLoopRanges for LinalgExt::CustomOp.
-    if (isa<IREE::LinalgExt::CustomOp>(producer)) {
-      return false;
-    }
-
     SmallVector<int64_t> producerIterationSpace =
         producerFusionOp.getStaticLoopRanges();
     SmallVector<int64_t> consumerIterationSpace =
@@ -601,7 +596,7 @@
   Operation *rootOp = tracker.getFusionGroup(producer).getRoot();
   if (auto rootFusionOp =
           dyn_cast<IREE::LinalgExt::LinalgFusionOpInterface>(rootOp);
-      rootFusionOp && !isa<IREE::LinalgExt::CustomOp>(rootOp)) {
+      rootFusionOp) {
     SmallVector<int64_t> rootLoopRanges = rootFusionOp.getStaticLoopRanges();
     SmallVector<int64_t> consumerLoopRanges =
         consumerFusionOp.getStaticLoopRanges();
diff --git a/compiler/src/iree/compiler/DispatchCreation/test/form_dispatch_regions.mlir b/compiler/src/iree/compiler/DispatchCreation/test/form_dispatch_regions.mlir
index ee358f0..8e4eb51 100644
--- a/compiler/src/iree/compiler/DispatchCreation/test/form_dispatch_regions.mlir
+++ b/compiler/src/iree/compiler/DispatchCreation/test/form_dispatch_regions.mlir
@@ -807,6 +807,147 @@
 //  CHECK-SAME:         ins(%[[CUSTOM_OP]] :
 //       CHECK:     flow.return %[[GENERIC]]
 //       CHECK:   util.return %[[DISPATCH]]
+// DEFAULT-LABEL: func public @custom_op_consumer_fusion
+//       DEFAULT:   %[[DISPATCH:.+]] = flow.dispatch.region
+//       DEFAULT:     %[[CUSTOM_OP:.+]] = iree_linalg_ext.custom_op
+//       DEFAULT:       linalg.generic
+//       DEFAULT:     %[[GENERIC:.+]] = linalg.generic
+//  DEFAULT-SAME:         ins(%[[CUSTOM_OP]] :
+//       DEFAULT:     flow.return %[[GENERIC]]
+//   DEFAULT-NOT:   flow.dispatch.region
+//       DEFAULT:   util.return %[[DISPATCH]]
+
+// -----
+
+util.func @custom_op_static_consumer_fusion(%arg0 : tensor<4xf32>,
+    %arg1 : tensor<4xf32>, %arg2 : tensor<4xf32>) -> tensor<4xf32> {
+  %0 = iree_linalg_ext.custom_op {
+      indexing_maps = [affine_map<(d0) -> (d0)>,
+                       affine_map<(d0) -> (d0)>],
+      iterator_types = [#iree_linalg_ext.iterator_type<parallel>]}
+      ins(%arg0 : tensor<4xf32>) outs(%arg1 : tensor<4xf32>) {
+    ^bb0(%b0 : tensor<?xf32>, %b1 : tensor<?xf32>):
+      %1 = linalg.generic {
+          indexing_maps = [affine_map<(d0) -> (d0)>,
+                           affine_map<(d0) -> (d0)>],
+          iterator_types = ["parallel"]}
+          ins(%b0 : tensor<?xf32>) outs(%b1 : tensor<?xf32>) {
+        ^bb1(%bb0 : f32, %bb1 : f32):
+          %2 = arith.addf %bb0, %bb1 : f32
+          linalg.yield %2 : f32
+      } -> tensor<?xf32>
+      iree_linalg_ext.yield %1 : tensor<?xf32>
+  } -> tensor<4xf32>
+  %3 = linalg.generic {
+      indexing_maps = [affine_map<(d0) -> (d0)>, affine_map<(d0) -> (d0)>],
+      iterator_types = ["parallel"]}
+      ins(%0 : tensor<4xf32>) outs(%arg2 : tensor<4xf32>) {
+    ^bb0(%b0 : f32, %b1 : f32):
+      %4 = arith.mulf %b0, %b0 : f32
+      linalg.yield %4 : f32
+  } -> tensor<4xf32>
+  util.return %3 : tensor<4xf32>
+}
+// CHECK-LABEL: func public @custom_op_static_consumer_fusion
+//       CHECK:   %[[DISPATCH:.+]] = flow.dispatch.region
+//       CHECK:     %[[CUSTOM_OP:.+]] = iree_linalg_ext.custom_op
+//       CHECK:       linalg.generic
+//       CHECK:     %[[GENERIC:.+]] = linalg.generic
+//  CHECK-SAME:         ins(%[[CUSTOM_OP]] :
+//       CHECK:     flow.return %[[GENERIC]]
+//       CHECK:   util.return %[[DISPATCH]]
+// DEFAULT-LABEL: func public @custom_op_static_consumer_fusion
+//       DEFAULT:   %[[DISPATCH:.+]] = flow.dispatch.region
+//       DEFAULT:     %[[CUSTOM_OP:.+]] = iree_linalg_ext.custom_op
+//       DEFAULT:       linalg.generic
+//       DEFAULT:     %[[GENERIC:.+]] = linalg.generic
+//  DEFAULT-SAME:         ins(%[[CUSTOM_OP]] :
+//       DEFAULT:     flow.return %[[GENERIC]]
+//   DEFAULT-NOT:   flow.dispatch.region
+//       DEFAULT:   util.return %[[DISPATCH]]
+
+// -----
+
+util.func @custom_op_dynamic_loop_range_with_static_consumer_fusion(
+    %arg0 : tensor<7xf32>, %arg1 : tensor<4xf32>,
+    %arg2 : tensor<4xf32>) -> tensor<4xf32> {
+  %0 = iree_linalg_ext.custom_op {
+      indexing_maps = [affine_map<(d0, d1) -> (d0 + d1)>,
+                       affine_map<(d0, d1) -> (d0)>],
+      iterator_types = [#iree_linalg_ext.iterator_type<parallel>,
+                        #iree_linalg_ext.iterator_type<reduction>]}
+      ins(%arg0 : tensor<7xf32>) outs(%arg1 : tensor<4xf32>) {
+    ^bb0(%b0 : tensor<?xf32>, %b1 : tensor<?xf32>):
+      iree_linalg_ext.yield %b1 : tensor<?xf32>
+  } -> tensor<4xf32>
+  %1 = linalg.generic {
+      indexing_maps = [affine_map<(d0) -> (d0)>,
+                       affine_map<(d0) -> (d0)>],
+      iterator_types = ["parallel"]}
+      ins(%0 : tensor<4xf32>) outs(%arg2 : tensor<4xf32>) {
+    ^bb0(%b0 : f32, %b1 : f32):
+      %2 = arith.mulf %b0, %b0 : f32
+      linalg.yield %2 : f32
+  } -> tensor<4xf32>
+  util.return %1 : tensor<4xf32>
+}
+// CHECK-LABEL: func public @custom_op_dynamic_loop_range_with_static_consumer_fusion
+//       CHECK:   %[[DISPATCH:.+]] = flow.dispatch.region
+//       CHECK:     %[[CUSTOM_OP:.+]] = iree_linalg_ext.custom_op
+//       CHECK:     %[[GENERIC:.+]] = linalg.generic
+//  CHECK-SAME:         ins(%[[CUSTOM_OP]] :
+//       CHECK:     flow.return %[[GENERIC]]
+//       CHECK:   util.return %[[DISPATCH]]
+// DEFAULT-LABEL: func public @custom_op_dynamic_loop_range_with_static_consumer_fusion
+//       DEFAULT:   %[[DISPATCH:.+]] = flow.dispatch.region
+//       DEFAULT:     %[[CUSTOM_OP:.+]] = iree_linalg_ext.custom_op
+//       DEFAULT:     %[[GENERIC:.+]] = linalg.generic
+//  DEFAULT-SAME:         ins(%[[CUSTOM_OP]] :
+//       DEFAULT:     flow.return %[[GENERIC]]
+//   DEFAULT-NOT:   flow.dispatch.region
+//       DEFAULT:   util.return %[[DISPATCH]]
+
+// -----
+
+util.func @custom_op_consumer_fusion_larger_consumer_loop_count(
+    %arg0 : tensor<4xf32>, %arg1 : tensor<4xf32>,
+    %arg2 : tensor<4x1xf32>) -> tensor<4x1xf32> {
+  %0 = iree_linalg_ext.custom_op {
+      indexing_maps = [affine_map<(d0) -> (d0)>,
+                       affine_map<(d0) -> (d0)>],
+      iterator_types = [#iree_linalg_ext.iterator_type<parallel>]}
+      ins(%arg0 : tensor<4xf32>) outs(%arg1 : tensor<4xf32>) {
+    ^bb0(%b0 : tensor<?xf32>, %b1 : tensor<?xf32>):
+      iree_linalg_ext.yield %b1 : tensor<?xf32>
+  } -> tensor<4xf32>
+  %1 = linalg.generic {
+      indexing_maps = [affine_map<(d0, d1) -> (d0)>,
+                       affine_map<(d0, d1) -> (d0, d1)>],
+      iterator_types = ["parallel", "parallel"]}
+      ins(%0 : tensor<4xf32>) outs(%arg2 : tensor<4x1xf32>) {
+    ^bb0(%b0 : f32, %b1 : f32):
+      %2 = arith.mulf %b0, %b0 : f32
+      linalg.yield %2 : f32
+  } -> tensor<4x1xf32>
+  util.return %1 : tensor<4x1xf32>
+}
+// CHECK-LABEL: func public @custom_op_consumer_fusion_larger_consumer_loop_count
+//       CHECK:   %[[DISPATCH:.+]] = flow.dispatch.region
+//       CHECK:     %[[CUSTOM_OP:.+]] = iree_linalg_ext.custom_op
+//       CHECK:     %[[GENERIC:.+]] = linalg.generic
+//  CHECK-SAME:         ins(%[[CUSTOM_OP]] :
+//       CHECK:     flow.return %[[GENERIC]]
+//   CHECK-NOT:   flow.dispatch.region
+//       CHECK:   util.return %[[DISPATCH]]
+// DEFAULT-LABEL: func public @custom_op_consumer_fusion_larger_consumer_loop_count
+//       DEFAULT:   %[[CUSTOM_DISPATCH:.+]] = flow.dispatch.region
+//       DEFAULT:     %[[CUSTOM_OP:.+]] = iree_linalg_ext.custom_op
+//       DEFAULT:     flow.return %[[CUSTOM_OP]]
+//       DEFAULT:   %[[GENERIC_DISPATCH:.+]] = flow.dispatch.region
+//       DEFAULT:     %[[GENERIC:.+]] = linalg.generic
+//  DEFAULT-SAME:         ins(%[[CUSTOM_DISPATCH]] :
+//       DEFAULT:     flow.return %[[GENERIC]]
+//       DEFAULT:   util.return %[[GENERIC_DISPATCH]]
 
 // -----