[DispatchCreation] Fix GatherFusionPattern on a scalar producer operand (#24803)

GatherFusionPattern builds a tensor.extract for every operand of the
producer generic, so a scalar operand reached cast<TensorType> and
asserted. A scalar is invariant over the iteration space, so there is no
index to permute and nothing to extract: the fused body reads the value
itself.

The producer has to be a bit extend or a transpose to qualify, and that
classification deliberately ignores non-tensor inputs, so a qualifying
producer can have them. A per-tensor dequantize widening i8 to f32 with
its scale passed as an f32 is one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: default <ziereis@roofline.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/compiler/src/iree/compiler/DispatchCreation/ElementwiseOpFusion.cpp b/compiler/src/iree/compiler/DispatchCreation/ElementwiseOpFusion.cpp
index 61eedf0..77a85aa 100644
--- a/compiler/src/iree/compiler/DispatchCreation/ElementwiseOpFusion.cpp
+++ b/compiler/src/iree/compiler/DispatchCreation/ElementwiseOpFusion.cpp
@@ -146,6 +146,14 @@
     auto resultMap = producerOp.getIndexingMapMatchingResult(result);
     SmallVector<Value> extractOps;
     for (OpOperand &operand : producerOp->getOpOperands()) {
+      // A scalar operand is invariant over the iteration space, so there is no
+      // index to permute and nothing to extract: the body sees the value
+      // itself. Bit width classification ignores non-tensor inputs, so a
+      // producer that qualifies here can still have them.
+      if (!isa<RankedTensorType>(operand.get().getType())) {
+        extractOps.push_back(operand.get());
+        continue;
+      }
       auto inputMap = producerOp.getMatchingIndexingMap(&operand);
       auto composedMap = inputMap.compose(inversePermutation(resultMap));
       auto perm = llvm::map_to_vector<4>(
diff --git a/compiler/src/iree/compiler/DispatchCreation/test/elementwise_op_fusion.mlir b/compiler/src/iree/compiler/DispatchCreation/test/elementwise_op_fusion.mlir
index ba198b6..2a79853 100644
--- a/compiler/src/iree/compiler/DispatchCreation/test/elementwise_op_fusion.mlir
+++ b/compiler/src/iree/compiler/DispatchCreation/test/elementwise_op_fusion.mlir
@@ -625,6 +625,42 @@
 
 // -----
 
+util.func public @gather_scalar_producer_operand(%arg0 : tensor<256xi8>, %arg1 : f32, %arg2 : tensor<100xindex>) -> (tensor<100xf32>) {
+  %empty0 = tensor.empty() : tensor<256xf32>
+  %0 = linalg.generic {indexing_maps = [affine_map<(d0) -> (d0)>, affine_map<(d0) -> ()>, affine_map<(d0) -> (d0)>], iterator_types = ["parallel"]} ins(%arg0, %arg1 : tensor<256xi8>, f32) outs(%empty0 : tensor<256xf32>) {
+  ^bb0(%in: i8, %in0 : f32, %out: f32):
+    %0 = arith.extsi %in : i8 to i32
+    %1 = arith.sitofp %0 : i32 to f32
+    %2 = arith.mulf %1, %in0 : f32
+    linalg.yield %2 : f32
+  } -> tensor<256xf32>
+  %empty1 = tensor.empty() : tensor<100xf32>
+  %gather = linalg.generic {indexing_maps = [affine_map<(d0) -> (d0)>, affine_map<(d0) -> (d0)>], iterator_types = ["parallel"]} ins(%arg2: tensor<100xindex>) outs(%empty1 : tensor<100xf32>) {
+  ^bb0(%in: index, %out: f32):
+    %1 = tensor.extract %0[%in] : tensor<256xf32>
+    linalg.yield %1 : f32
+  } -> tensor<100xf32>
+  util.return %gather : tensor<100xf32>
+}
+// A scalar operand is invariant over the iteration space, so it has no index to
+// permute and nothing to extract: the fused body reads the value itself. Bit
+// width classification ignores non-tensor inputs, so a producer that qualifies
+// for this fusion can have them.
+// CHECK-LABEL: util.func public @gather_scalar_producer_operand(
+//  CHECK-SAME:   %[[ARG0:[A-Za-z0-9]+]]: tensor<256xi8>
+//  CHECK-SAME:   %[[ARG1:[A-Za-z0-9]+]]: f32
+//  CHECK-SAME:   %[[ARG2:[A-Za-z0-9]+]]: tensor<100xindex>
+//       CHECK:   %[[GATHER:.+]] = linalg.generic
+//  CHECK-SAME:     ins(%[[ARG2]] : tensor<100xindex>
+//  CHECK-NEXT:     ^bb0(%[[IN:.+]]: index
+//       CHECK:     %[[EXTRACT:.+]] = tensor.extract %[[ARG0]][%[[IN]]]
+//       CHECK:     %[[EXT:.+]] = arith.extsi %[[EXTRACT]]
+//       CHECK:     %[[FP:.+]] = arith.sitofp %[[EXT]]
+//       CHECK:     arith.mulf %[[FP]], %[[ARG1]]
+//       CHECK:   return %[[GATHER]]
+
+// -----
+
 util.func public @gather_replace_linalg_index(%arg0 : tensor<256x256xf16>, %arg1 : tensor<100xindex>) -> (tensor<100xf32>) {
   %empty0 = tensor.empty() : tensor<256x256xf32>
   %0 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0, d1)>], iterator_types = ["parallel", "parallel"]} ins(%arg0 : tensor<256x256xf16>) outs(%empty0 : tensor<256x256xf32>) {