[DispatchCreation] Hoist scalar tensor.extract and tensor.extract_slice (#24552)
This PR aims to :
- Enable hoisting tensor.extract ops that read from a scalar tensor
already outside the dispatch.
- Loosen the requirement to hoist tensor.extract_slice ops.
### Context
I encountered this error when working on a modified version of LFM2.5. I
attach a reproducer which captures the idea.
A causal mask path stayed fused inside a QK matmul dispatch, which
produced large vectors due to tile size propagation in an mmt4d ukernel.
```
mask_slice_qk_repro.mlir:45:18: error: One or more operations with large vector sizes (32768 bytes) were found:
%scores_3d = torch.aten.bmm %q, %k : !torch.vtensor<[16,?,64],f32>, !torch.vtensor<[16,64,256],f32> -> !torch.vtensor<[16,?,256],f32>
^
<unknown>:0: note: %cst = arith.constant dense<0xFF800000> : vector<16x16x16x16xf32>
<unknown>:0: note: %cst_0 = arith.constant dense<0.000000e+00> : vector<16x16x16x16xf32>
<unknown>:0: note: %cst_1 = arith.constant dense<0> : vector<16x16x16x16xi8>
```
This originated due to the mask's slicing offset coming from a
tensor.extract and subsequently producing a scalar metadata chain that
remained inside the dispatch.
### Proposed fix
- `HoistUniformScalarComputePass` can accept more "candidate ops" than
just arith ops. I just included tensor.extract since its what I ran
into. `isUniformScalarForDispatch` is still in charge to verify that the
candidate op is hoistable, so i added the logic to check the
tensor.extract ops.
- `IREE::Flow::isOffsetSizeAndStrideMappableToFlow` got split into two:
`isOffsetSizeAndStrideStructurallyMappableToFlow` just checks if the
slice can be represented as one flat contiguous byte range, and
`isOffsetSizeAndStrideMappableToFlow` checks for that and the additional
tensor.extract provenance.
- `isHoistableOp` in HoistEncodingOps.cpp was rejecting extract slice
ops whose offset, size, and stride where produced by an extract op due
to calling `isOffsetSizeAndStrideMappableToFlow` on them. Now it calls
`isOffsetSizeAndStrideStructurallyMappableToFlow`.
### Additional Notes:
- I tried to not interfere with the codebase's original intentions.
- Since the extract and extract_slice make it out of the dispatch, the
large vectors never occur. I thought this was the right way to address
the root cause of the problem.
- Inspecting the mmt4d ukernel tile size propagation, it seems that the
problematic large vectors originated due to propagating a pack op tiling
config to the outer dims of an accumulator, which should not happen
afaiu. I could work on that separate issue if it is of interest.
<details>
<summary>mlir reproducer</summary>
```mlir
module @module {
func.func @forward(
%query: !torch.vtensor<[1,16,?,64],f32>,
%key: !torch.vtensor<[1,16,64,256],f32>,
%mask: !torch.vtensor<[256,256],ui8>,
%positions: !torch.vtensor<[1],si64>)
-> !torch.vtensor<[1,16,?,256],f32>
attributes {torch.assume_strict_symbolic_shapes} {
%s = torch.symbolic_int "s" {min_val = 1, max_val = 256} : !torch.int
torch.bind_symbolic_shape %query, [%s], affine_map<()[s0] -> (1, 16, s0, 64)> : !torch.vtensor<[1,16,?,64],f32>
%int0 = torch.constant.int 0
%int1 = torch.constant.int 1
%int2 = torch.constant.int 2
%int16 = torch.constant.int 16
%int64 = torch.constant.int 64
%int256 = torch.constant.int 256
%int-1 = torch.constant.int -1
%seq_len = torch.aten.size.int %query, %int2 : !torch.vtensor<[1,16,?,64],f32>, !torch.int -> !torch.int
%pos_tensor = torch.aten.select.int %positions, %int0, %int-1 : !torch.vtensor<[1],si64>, !torch.int, !torch.int -> !torch.vtensor<[],si64>
%pos = torch.aten.item %pos_tensor : !torch.vtensor<[],si64> -> !torch.int
%end = torch.aten.add.int %pos, %seq_len : !torch.int, !torch.int -> !torch.int
%bool_dtype = torch.constant.int 11
%mask_bool = torch.prims.convert_element_type %mask, %bool_dtype : !torch.vtensor<[256,256],ui8>, !torch.int -> !torch.vtensor<[256,256],i1>
%mask_slice = torch.aten.slice.Tensor %mask_bool, %int0, %pos, %end, %int1 : !torch.vtensor<[256,256],i1>, !torch.int, !torch.int, !torch.int, !torch.int -> !torch.vtensor<[?,256],i1>
torch.bind_symbolic_shape %mask_slice, [%s], affine_map<()[s0] -> (s0, 256)> : !torch.vtensor<[?,256],i1>
%float-Inf = torch.constant.float 0xFFF0000000000000
%float0 = torch.constant.float 0.000000e+00
%f32_dtype = torch.constant.int 6
%none = torch.constant.none
%cpu = torch.constant.device "cpu"
%neg_inf = torch.aten.scalar_tensor %float-Inf, %f32_dtype, %none, %cpu, %none : !torch.float, !torch.int, !torch.none, !torch.Device, !torch.none -> !torch.vtensor<[],f32>
%mask_bias = torch.aten.where.ScalarSelf %mask_slice, %float0, %neg_inf : !torch.vtensor<[?,256],i1>, !torch.float, !torch.vtensor<[],f32> -> !torch.vtensor<[?,256],f32>
torch.bind_symbolic_shape %mask_bias, [%s], affine_map<()[s0] -> (s0, 256)> : !torch.vtensor<[?,256],f32>
%q_shape = torch.prim.ListConstruct %int16, %seq_len, %int64 : (!torch.int, !torch.int, !torch.int) -> !torch.list<int>
%q = torch.aten.view %query, %q_shape : !torch.vtensor<[1,16,?,64],f32>, !torch.list<int> -> !torch.vtensor<[16,?,64],f32>
torch.bind_symbolic_shape %q, [%s], affine_map<()[s0] -> (16, s0, 64)> : !torch.vtensor<[16,?,64],f32>
%k_shape = torch.prim.ListConstruct %int16, %int64, %int256 : (!torch.int, !torch.int, !torch.int) -> !torch.list<int>
%k = torch.aten.view %key, %k_shape : !torch.vtensor<[1,16,64,256],f32>, !torch.list<int> -> !torch.vtensor<[16,64,256],f32>
%scores_3d = torch.aten.bmm %q, %k : !torch.vtensor<[16,?,64],f32>, !torch.vtensor<[16,64,256],f32> -> !torch.vtensor<[16,?,256],f32>
torch.bind_symbolic_shape %scores_3d, [%s], affine_map<()[s0] -> (16, s0, 256)> : !torch.vtensor<[16,?,256],f32>
%scores_shape = torch.prim.ListConstruct %int1, %int16, %seq_len, %int256 : (!torch.int, !torch.int, !torch.int, !torch.int) -> !torch.list<int>
%scores = torch.aten.view %scores_3d, %scores_shape : !torch.vtensor<[16,?,256],f32>, !torch.list<int> -> !torch.vtensor<[1,16,?,256],f32>
torch.bind_symbolic_shape %scores, [%s], affine_map<()[s0] -> (1, 16, s0, 256)> : !torch.vtensor<[1,16,?,256],f32>
%result = torch.aten.add.Tensor %scores, %mask_bias, %int1 : !torch.vtensor<[1,16,?,256],f32>, !torch.vtensor<[?,256],f32>, !torch.int -> !torch.vtensor<[1,16,?,256],f32>
torch.bind_symbolic_shape %result, [%s], affine_map<()[s0] -> (1, 16, s0, 256)> : !torch.vtensor<[1,16,?,256],f32>
return %result : !torch.vtensor<[1,16,?,256],f32>
}
}
```
</details>
Compile command:
```
iree-compile \
mask_slice_qk_repro.mlir \
-o mask_slice_qk_repro.vmfb \
--iree-input-type=auto \
--iree-hal-target-device=local \
--iree-opt-data-tiling=true \
--iree-llvmcpu-enable-ukernels=all \
--iree-hal-local-target-device-backends=llvm-cpu \
--iree-hal-local-host-device-backends=llvm-cpu \
--iree-llvmcpu-target-cpu-features=host
```
Assisted by Codex 5.5
---------
Signed-off-by: Juan Ignacio Pisula <pisula@roofline.ai>diff --git a/compiler/src/iree/compiler/Dialect/Flow/Conversion/TensorToFlow/Utils.cpp b/compiler/src/iree/compiler/Dialect/Flow/Conversion/TensorToFlow/Utils.cpp
index aef20cd..5f8520b 100644
--- a/compiler/src/iree/compiler/Dialect/Flow/Conversion/TensorToFlow/Utils.cpp
+++ b/compiler/src/iree/compiler/Dialect/Flow/Conversion/TensorToFlow/Utils.cpp
@@ -8,6 +8,7 @@
#include "iree/compiler/Dialect/Flow/IR/FlowDialect.h"
#include "iree/compiler/Dialect/Flow/IR/FlowOps.h"
+#include "llvm/ADT/STLExtras.h"
#include "mlir/Analysis/SliceAnalysis.h"
#include "mlir/Dialect/Arith/Utils/Utils.h"
#include "mlir/Dialect/Tensor/Utils/Utils.h"
@@ -67,12 +68,11 @@
return hasExtract;
}
-bool isOffsetSizeAndStrideMappableToFlow(ArrayRef<OpFoldResult> offsets,
- ArrayRef<OpFoldResult> sizes,
- ArrayRef<OpFoldResult> strides,
- ArrayRef<int64_t> baseShape) {
+bool isOffsetSizeAndStrideStructurallyMappableToFlow(
+ ArrayRef<OpFoldResult> offsets, ArrayRef<OpFoldResult> sizes,
+ ArrayRef<OpFoldResult> strides, ArrayRef<int64_t> baseShape) {
if (offsets.size() != baseShape.size()) {
- // Unhanded rank-reducing case.
+ // Unhandled rank-reducing case.
return false;
}
auto getVal = [](OpFoldResult valueOrAttr, int64_t dynamicVal) -> int64_t {
@@ -90,16 +90,6 @@
OpFoldResult size = sizes[dim - 1];
OpFoldResult stride = strides[dim - 1];
- // The offsets and sizes dont have to be static for all dimensions. When
- // `fullSlices` is true, the offset and sizes can be dynamic. But in some
- // cases, the dynamic offset/size value is obtained by computing from
- // another tensor which lives on the device. To avoid host-round tripping
- // try to infer when a value is extracted from a tensor.
- if (producedByValueExtract(offset) || producedByValueExtract(stride) ||
- producedByValueExtract(size)) {
- return false;
- }
-
int64_t staticOffset = getVal(offset, ShapedType::kDynamic);
int64_t staticSize = getVal(size, ShapedType::kDynamic);
int64_t staticStride = getVal(stride, ShapedType::kDynamic);
@@ -125,6 +115,29 @@
return true;
}
+bool isOffsetSizeAndStrideMappableToFlow(ArrayRef<OpFoldResult> offsets,
+ ArrayRef<OpFoldResult> sizes,
+ ArrayRef<OpFoldResult> strides,
+ ArrayRef<int64_t> baseShape) {
+ if (!isOffsetSizeAndStrideStructurallyMappableToFlow(offsets, sizes, strides,
+ baseShape)) {
+ return false;
+ }
+
+ // The offsets and sizes don't have to be static for all dimensions. When
+ // `fullSlices` is true, the offset and sizes can be dynamic. But in some
+ // cases, the dynamic offset/size value is obtained by computing from another
+ // tensor which lives on the device. To avoid host-round tripping try to infer
+ // when a value is extracted from a tensor.
+ for (auto [offset, size, stride] : llvm::zip_equal(offsets, sizes, strides)) {
+ if (producedByValueExtract(offset) || producedByValueExtract(size) ||
+ producedByValueExtract(stride)) {
+ return false;
+ }
+ }
+ return true;
+}
+
LogicalResult
convertInsertSliceOpToFlowUpdateOp(RewriterBase &rewriter,
tensor::InsertSliceOp insertOp) {
diff --git a/compiler/src/iree/compiler/Dialect/Flow/Conversion/TensorToFlow/Utils.h b/compiler/src/iree/compiler/Dialect/Flow/Conversion/TensorToFlow/Utils.h
index 0713698..592277e 100644
--- a/compiler/src/iree/compiler/Dialect/Flow/Conversion/TensorToFlow/Utils.h
+++ b/compiler/src/iree/compiler/Dialect/Flow/Conversion/TensorToFlow/Utils.h
@@ -14,7 +14,14 @@
namespace mlir::iree_compiler::IREE::Flow {
/// Indicates whether the given offsets/sizes/strides representing a slice from
-/// baseShape is a contiguous slice, and this is mappable to Flow ops.
+/// baseShape is a contiguous slice structurally representable by Flow ops.
+bool isOffsetSizeAndStrideStructurallyMappableToFlow(
+ llvm::ArrayRef<OpFoldResult> offsets, llvm::ArrayRef<OpFoldResult> sizes,
+ llvm::ArrayRef<OpFoldResult> strides, llvm::ArrayRef<int64_t> baseShape);
+
+/// Indicates whether the given offsets/sizes/strides representing a slice from
+/// baseShape is mappable to Flow ops without tensor-load-derived dynamic
+/// offsets/sizes/strides.
bool isOffsetSizeAndStrideMappableToFlow(llvm::ArrayRef<OpFoldResult> offsets,
llvm::ArrayRef<OpFoldResult> sizes,
llvm::ArrayRef<OpFoldResult> strides,
diff --git a/compiler/src/iree/compiler/DispatchCreation/HoistEncodingOps.cpp b/compiler/src/iree/compiler/DispatchCreation/HoistEncodingOps.cpp
index d225a32..4796602 100644
--- a/compiler/src/iree/compiler/DispatchCreation/HoistEncodingOps.cpp
+++ b/compiler/src/iree/compiler/DispatchCreation/HoistEncodingOps.cpp
@@ -159,15 +159,16 @@
}
/// Returns true if the op is hoistable outside dispatches, which indicates that
-/// the ops can be either mappable to Flow ops or get hoisted to globals.
+/// the op can be moved across the dispatch boundary by this pass or hoisted to
+/// globals.
static bool isHoistableOp(Operation *op) {
if (auto sliceOp = dyn_cast<tensor::ExtractSliceOp>(op)) {
SmallVector<OpFoldResult> offsets = sliceOp.getMixedOffsets();
SmallVector<OpFoldResult> sizes = sliceOp.getMixedSizes();
SmallVector<OpFoldResult> strides = sliceOp.getMixedStrides();
ArrayRef<int64_t> srcShape = sliceOp.getSourceType().getShape();
- return IREE::Flow::isOffsetSizeAndStrideMappableToFlow(offsets, sizes,
- strides, srcShape);
+ return IREE::Flow::isOffsetSizeAndStrideStructurallyMappableToFlow(
+ offsets, sizes, strides, srcShape);
}
// ConstExprHoistingPolicy has an assumption that any root op is not hoistable
// because they are already hoisted. This is not the case when the parent op
diff --git a/compiler/src/iree/compiler/DispatchCreation/HoistUniformScalarCompute.cpp b/compiler/src/iree/compiler/DispatchCreation/HoistUniformScalarCompute.cpp
index e0c6096..382627e 100644
--- a/compiler/src/iree/compiler/DispatchCreation/HoistUniformScalarCompute.cpp
+++ b/compiler/src/iree/compiler/DispatchCreation/HoistUniformScalarCompute.cpp
@@ -9,6 +9,7 @@
#include "iree/compiler/DispatchCreation/Passes.h"
#include "llvm/ADT/SmallVectorExtras.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
+#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/IR/OpDefinition.h"
#include "mlir/IR/Visitors.h"
#include "mlir/Interfaces/FunctionInterfaces.h"
@@ -31,11 +32,36 @@
return v.getParentRegion()->getParentOp() != dispatch;
};
- // Check that all operands are defined outside the dispatch and all operand
- // and result types are scalars or vectors.
- return llvm::all_of(op->getOperands(), isOutsideDispatch) &&
- llvm::all_of(op->getOperandTypes(), isIntOrIndex) &&
- llvm::all_of(op->getResultTypes(), isIntOrIndex);
+ // Arith candidates are uniform when all operands are defined outside the
+ // dispatch and all operand/result types are int/index scalars.
+ if (isa<arith::ArithDialect>(op->getDialect())) {
+ return llvm::all_of(op->getOperands(), isOutsideDispatch) &&
+ llvm::all_of(op->getOperandTypes(), isIntOrIndex) &&
+ llvm::all_of(op->getResultTypes(), isIntOrIndex);
+ }
+
+ // A tensor.extract is only uniform scalar metadata when it extracts the sole
+ // int/index element from a tensor and all extraction dependencies are already
+ // outside this dispatch.
+ if (auto extractOp = dyn_cast<tensor::ExtractOp>(op)) {
+ auto tensorType =
+ dyn_cast<RankedTensorType>(extractOp.getTensor().getType());
+ return tensorType && tensorType.hasStaticShape() &&
+ tensorType.getNumElements() == 1 &&
+ extractOp.getResult().getType().isIntOrIndex() &&
+ isOutsideDispatch(extractOp.getTensor()) &&
+ llvm::all_of(extractOp.getIndices(), isOutsideDispatch);
+ }
+
+ return false;
+}
+
+// Restrict to arith ops and scalar extracts from one-element
+// tensors to avoid unexpected hoisting of flow/stream/hal.dispatch
+// workgroups count/id ops.
+static bool isHoistCandidate(Operation *op) {
+ return isa<arith::ArithDialect>(op->getDialect()) ||
+ isa<tensor::ExtractOp>(op);
}
namespace {
@@ -52,11 +78,9 @@
funcOp.walk([&](IREE::Flow::DispatchRegionOp dispatch) {
for (Block &body : dispatch.getBody()) {
SmallVector<Operation *> ops;
- // Restrict to arith ops to avoid unexpected hoisting of
- // flow/stream/hal.dispatch.workgroups.count/id ops.
// TODO: Add an op trait to tie count/id ops to region ops.
for (Operation &op : body.getOperations()) {
- if (isa<arith::ArithDialect>(op.getDialect())) {
+ if (isHoistCandidate(&op)) {
ops.push_back(&op);
}
}
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 2d075f5..95a1a7a 100644
--- a/compiler/src/iree/compiler/DispatchCreation/test/hoist_encoding_ops.mlir
+++ b/compiler/src/iree/compiler/DispatchCreation/test/hoist_encoding_ops.mlir
@@ -239,6 +239,88 @@
// -----
+// Do not hoist a `set_encoding` whose source is a structurally flow-mappable
+// slice if the dynamic offset is still produced by a tensor.extract inside the
+// dispatch. HoistUniformScalarCompute is responsible for moving such scalar
+// metadata out first.
+
+#encoding = #iree_encoding.testing<>
+util.func private @get_tensor(tensor<64x256xi1, #encoding>) -> tensor<64x256xi1>
+util.func public @no_hoist_slice_with_dispatch_local_extract_offset(%input: tensor<256x256xi1>, %offsets: tensor<1xi64>) -> tensor<64x256xi1> {
+ %0 = flow.dispatch.region -> (tensor<64x256xi1>) {
+ %c0 = arith.constant 0 : index
+ %offset_i64 = tensor.extract %offsets[%c0] : tensor<1xi64>
+ %offset = arith.index_cast %offset_i64 : i64 to index
+ %1 = tensor.extract_slice %input[%offset, 0] [64, 256] [1, 1] : tensor<256x256xi1> to tensor<64x256xi1>
+ %2 = iree_encoding.set_encoding %1 : tensor<64x256xi1> -> tensor<64x256xi1, #encoding>
+ %3 = util.call @get_tensor(%2) : (tensor<64x256xi1, #encoding>) -> tensor<64x256xi1>
+ flow.return %3 : tensor<64x256xi1>
+ }
+ util.return %0 : tensor<64x256xi1>
+}
+// CHECK-LABEL: util.func public @no_hoist_slice_with_dispatch_local_extract_offset(
+// CHECK: flow.dispatch.region
+// CHECK: %[[OFFSET_I64:.+]] = tensor.extract
+// CHECK: %[[OFFSET:.+]] = arith.index_cast %[[OFFSET_I64]]
+// CHECK: %[[SRC:.+]] = tensor.extract_slice %{{.*}}[%[[OFFSET]], 0] [64, 256] [1, 1]
+// CHECK: %[[SET_ENCODING:.+]] = iree_encoding.set_encoding %[[SRC]]
+// CHECK: flow.return
+
+// -----
+
+// Hoist a `set_encoding` whose source is a structurally flow-mappable slice,
+// even if a dynamic offset is derived from a scalar tensor.extract that has
+// already been hoisted outside the dispatch.
+
+#encoding = #iree_encoding.testing<>
+util.func private @get_tensor(tensor<64x256xi1, #encoding>) -> tensor<64x256xi1>
+util.func public @hoist_slice_with_external_extract_offset(%input: tensor<256x256xi1>, %offsets: tensor<1xi64>) -> tensor<64x256xi1> {
+ %c0 = arith.constant 0 : index
+ %offset_i64 = tensor.extract %offsets[%c0] : tensor<1xi64>
+ %offset = arith.index_cast %offset_i64 : i64 to index
+ %0 = flow.dispatch.region -> (tensor<64x256xi1>) {
+ %1 = tensor.extract_slice %input[%offset, 0] [64, 256] [1, 1] : tensor<256x256xi1> to tensor<64x256xi1>
+ %2 = iree_encoding.set_encoding %1 : tensor<64x256xi1> -> tensor<64x256xi1, #encoding>
+ %3 = util.call @get_tensor(%2) : (tensor<64x256xi1, #encoding>) -> tensor<64x256xi1>
+ flow.return %3 : tensor<64x256xi1>
+ }
+ util.return %0 : tensor<64x256xi1>
+}
+// CHECK-LABEL: util.func public @hoist_slice_with_external_extract_offset(
+// CHECK-SAME: %[[INPUT:.+]]: tensor<256x256xi1>, %[[OFFSETS:.+]]: tensor<1xi64>
+// CHECK: %[[C0:.+]] = arith.constant 0 : index
+// CHECK: %[[OFFSET_I64:.+]] = tensor.extract %[[OFFSETS]][%[[C0]]] : tensor<1xi64>
+// CHECK: %[[OFFSET:.+]] = arith.index_cast %[[OFFSET_I64]] : i64 to index
+// CHECK: %[[SRC:.+]] = tensor.extract_slice %[[INPUT]][%[[OFFSET]], 0] [64, 256] [1, 1]
+// CHECK: %[[SET_ENCODING:.+]] = iree_encoding.set_encoding %[[SRC]]
+// CHECK: flow.dispatch.region
+
+// -----
+
+// Do not hoist a `set_encoding` whose source is an `extract_slice` that is NOT
+// structurally flow-mappable: dim 1 has size 12 (not 1) while dim 2 is
+// partially sliced, so the slice is not a single contiguous byte range.
+
+#encoding = #iree_encoding.testing<>
+util.func private @get_tensor(tensor<12x100x64xf16, #encoding>) -> tensor<12x100x64xf16>
+util.func public @no_hoist_non_flow_mappable_slice(%input: tensor<1x12x2080x64xf16>) -> tensor<12x100x64xf16> {
+ %0 = flow.dispatch.region -> (tensor<12x100x64xf16>) {
+ %1 = tensor.extract_slice %input[0, 0, 0, 0] [1, 12, 100, 64] [1, 1, 1, 1]
+ : tensor<1x12x2080x64xf16> to tensor<12x100x64xf16>
+ %2 = iree_encoding.set_encoding %1 : tensor<12x100x64xf16> -> tensor<12x100x64xf16, #encoding>
+ %3 = util.call @get_tensor(%2) : (tensor<12x100x64xf16, #encoding>) -> tensor<12x100x64xf16>
+ flow.return %3 : tensor<12x100x64xf16>
+ }
+ util.return %0 : tensor<12x100x64xf16>
+}
+// CHECK-LABEL: util.func public @no_hoist_non_flow_mappable_slice(
+// CHECK: flow.dispatch.region
+// CHECK: %[[SRC:.+]] = tensor.extract_slice
+// CHECK: %[[SET_ENCODING:.+]] = iree_encoding.set_encoding %[[SRC]]
+// CHECK: flow.return
+
+// -----
+
// Avoid hoisting `set_encoding` operations that have pad encodings
#encoding = #iree_encoding.padding<[0, ?]>
diff --git a/compiler/src/iree/compiler/DispatchCreation/test/hoist_uniform_scalar_compute.mlir b/compiler/src/iree/compiler/DispatchCreation/test/hoist_uniform_scalar_compute.mlir
index a29880f..365a1fd 100644
--- a/compiler/src/iree/compiler/DispatchCreation/test/hoist_uniform_scalar_compute.mlir
+++ b/compiler/src/iree/compiler/DispatchCreation/test/hoist_uniform_scalar_compute.mlir
@@ -68,3 +68,48 @@
// CHECK: arith.muli
// CHECK: tensor.empty
// CHECK: flow.return
+
+// -----
+
+util.func public @hoist_one_element_tensor_extract(%arg0: tensor<1xi64>, %m: index) -> tensor<?xf32> {
+ %0 = flow.dispatch.region -> (tensor<?xf32>{%m}) {
+ %c0 = arith.constant 0 : index
+ %extracted = tensor.extract %arg0[%c0] : tensor<1xi64>
+ %idx = arith.index_cast %extracted : i64 to index
+ %sum = arith.addi %idx, %m : index
+ %1 = tensor.empty(%sum) : tensor<?xf32>
+ flow.return %1 : tensor<?xf32>
+ }
+ util.return %0 : tensor<?xf32>
+}
+
+// CHECK-LABEL: @hoist_one_element_tensor_extract
+// CHECK-SAME: %[[ARG0:.+]]: tensor<1xi64>, %[[M:.+]]: index
+// CHECK: %[[C0:.+]] = arith.constant 0 : index
+// CHECK: %[[EXTRACT:.+]] = tensor.extract %[[ARG0]][%[[C0]]] : tensor<1xi64>
+// CHECK: %[[IDX:.+]] = arith.index_cast %[[EXTRACT]] : i64 to index
+// CHECK: %[[SUM:.+]] = arith.addi %[[IDX]], %[[M]] : index
+// CHECK: flow.dispatch.region
+// CHECK: tensor.empty(%[[SUM]])
+// CHECK: flow.return
+
+// -----
+
+util.func public @dont_hoist_extract_from_multi_element_tensor(%arg0: tensor<4xi64>, %i: index, %m: index) -> tensor<?xf32> {
+ %0 = flow.dispatch.region -> (tensor<?xf32>{%m}) {
+ %extracted = tensor.extract %arg0[%i] : tensor<4xi64>
+ %idx = arith.index_cast %extracted : i64 to index
+ %sum = arith.addi %idx, %m : index
+ %1 = tensor.empty(%sum) : tensor<?xf32>
+ flow.return %1 : tensor<?xf32>
+ }
+ util.return %0 : tensor<?xf32>
+}
+
+// CHECK-LABEL: @dont_hoist_extract_from_multi_element_tensor
+// CHECK: flow.dispatch.region
+// CHECK: %[[EXTRACT:.+]] = tensor.extract
+// CHECK: %[[IDX:.+]] = arith.index_cast %[[EXTRACT]]
+// CHECK: %[[SUM:.+]] = arith.addi %[[IDX]]
+// CHECK: tensor.empty(%[[SUM]])
+// CHECK: flow.return
diff --git a/tests/e2e/stablehlo_ops/CMakeLists.txt b/tests/e2e/stablehlo_ops/CMakeLists.txt
index 8c00921..9ff2976 100644
--- a/tests/e2e/stablehlo_ops/CMakeLists.txt
+++ b/tests/e2e/stablehlo_ops/CMakeLists.txt
@@ -693,7 +693,7 @@
"pad.mlir"
# "philox.mlir" # TODO(#12509): WebGPU SPIR-V broken
# "pow.mlir" # TODO(#11321): error: value nan cannot be represented as 'f32'
- "reduce.mlir"
+ # "reduce.mlir" # TODO(#24650): error: use of variable address space 'immediate'
"reduce_window.mlir"
"remainder.mlir"
"reshape.mlir"