[GPU] Add thread tile size inference for scatter (#19694)

This adds basic logic for picking thread distribution tile sizes for
scatter ops. This allows fallback distribution to kick in for scatter.

Additionally splits derived_thread_config tests into their own file.
The existing test for GPUApplyTilingLevel has grown rather large. The
tests for derived_thread_config within that file aren't really testing
the pass but the logic in how we pick tile sizes. As a result it makes
more sense to split out those tests into a separate file. In the future
the logic for derived_thread_configs will probably need to move
elsewhere but this is good cleanup to start.
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel b/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel
index a43b450..473ca95 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel
@@ -18,6 +18,7 @@
     name = "lit",
     srcs = enforce_glob(
         [
+            "gpu_apply_derived_thread_config.mlir",
             "gpu_apply_tiling_level.mlir",
             "gpu_check_resource_usage.mlir",
             "gpu_create_fast_slow_path.mlir",
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt
index 8efb734..cf1cf03 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt
@@ -14,6 +14,7 @@
   NAME
     lit
   SRCS
+    "gpu_apply_derived_thread_config.mlir"
     "gpu_apply_tiling_level.mlir"
     "gpu_check_resource_usage.mlir"
     "gpu_combine_value_barriers.mlir"
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_apply_derived_thread_config.mlir b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_apply_derived_thread_config.mlir
new file mode 100644
index 0000000..dbaa3c2
--- /dev/null
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_apply_derived_thread_config.mlir
@@ -0,0 +1,214 @@
+// RUN: iree-opt --split-input-file --mlir-print-local-scope --pass-pipeline="builtin.module(func.func(iree-codegen-gpu-apply-tiling-level{tiling-level=thread}, canonicalize, cse))" %s | FileCheck %s
+
+#config = #iree_gpu.derived_thread_config
+#map = affine_map<(d0, d1) -> (d0, d1)>
+module {
+  func.func @inferred_add_tensor(%3: tensor<64x256xf32>, %4: tensor<64x256xf32>, %5: tensor<64x256xf32>) -> tensor<64x256xf32>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [16, 32, 1] subgroup_size = 64, {}>
+      } {
+    %6 = linalg.generic {
+      indexing_maps = [#map, #map, #map],
+      iterator_types = ["parallel", "parallel"]
+      } ins(%3, %4 : tensor<64x256xf32>, tensor<64x256xf32>) outs(%5 : tensor<64x256xf32>) attrs =  {lowering_config = #config} {
+    ^bb0(%in: f32, %in_0: f32, %out: f32):
+      %7 = arith.addf %in, %in_0 : f32
+      linalg.yield %7 : f32
+    } -> tensor<64x256xf32>
+    return %6 : tensor<64x256xf32>
+  }
+}
+
+// CHECK-LABEL: func.func @inferred_add_tensor
+//       CHECK:   scf.forall ({{.*}}) = (0, 0) to (64, 256) step (1, 4)
+//       CHECK:     linalg.generic {{.*}} ins(%{{.*}}: tensor<1x4xf32>, tensor<1x4xf32>)
+//       CHECK:     scf.forall.in_parallel
+//       CHECK:   mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
+
+// -----
+
+#config = #iree_gpu.derived_thread_config
+#map = affine_map<(d0, d1) -> (d0, d1)>
+module {
+  func.func @inferred_dynamic(%3: tensor<?x?xf32>, %4: tensor<?x?xf32>, %5: tensor<?x?xf32>) -> tensor<?x?xf32>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [16, 32, 1] subgroup_size = 64, {}>
+      } {
+    %6 = linalg.generic {
+      indexing_maps = [#map, #map, #map],
+      iterator_types = ["parallel", "parallel"]
+      } ins(%3, %4 : tensor<?x?xf32>, tensor<?x?xf32>) outs(%5 : tensor<?x?xf32>) attrs =  {lowering_config = #config} {
+    ^bb0(%in: f32, %in_0: f32, %out: f32):
+      %7 = arith.addf %in, %in_0 : f32
+      linalg.yield %7 : f32
+    } -> tensor<?x?xf32>
+    return %6 : tensor<?x?xf32>
+  }
+}
+
+// CHECK-LABEL: func.func @inferred_dynamic
+//  CHECK-SAME:   %[[A:[A-Za-z0-9]+]]: tensor<?x?xf32>
+//   CHECK-DAG:   %[[DIM0:.+]] = tensor.dim %[[A]], %c0 : tensor<?x?xf32>
+//   CHECK-DAG:   %[[DIM1:.+]] = tensor.dim %[[A]], %c1 : tensor<?x?xf32>
+//       CHECK:   scf.forall ({{.*}}) = (0, 0) to (%[[DIM0]], %[[DIM1]]) step (1, 4)
+//       CHECK:     linalg.generic {{.*}} ins(%{{.*}}: tensor<1x?xf32>, tensor<1x?xf32>)
+//       CHECK:     scf.forall.in_parallel
+//       CHECK:   mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
+
+// -----
+
+#config = #iree_gpu.derived_thread_config
+#map = affine_map<(d0, d1) -> (d0, d1)>
+module {
+  func.func @inferred_small_inner_dim(%3: tensor<8x2xf32>, %4: tensor<8x2xf32>, %5: tensor<8x2xf32>) -> tensor<8x2xf32>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [16, 32, 1] subgroup_size = 64, {}>
+      } {
+    %6 = linalg.generic {
+      indexing_maps = [#map, #map, #map],
+      iterator_types = ["parallel", "parallel"]
+      } ins(%3, %4 : tensor<8x2xf32>, tensor<8x2xf32>) outs(%5 : tensor<8x2xf32>) attrs =  {lowering_config = #config} {
+    ^bb0(%in: f32, %in_0: f32, %out: f32):
+      %7 = arith.addf %in, %in_0 : f32
+      linalg.yield %7 : f32
+    } -> tensor<8x2xf32>
+    return %6 : tensor<8x2xf32>
+  }
+}
+
+// CHECK-LABEL: func.func @inferred_small_inner_dim
+//       CHECK:   scf.forall ({{.*}}) = (0, 0) to (8, 2) step (1, 2)
+//       CHECK:     linalg.generic {{.*}} ins(%{{.*}}: tensor<1x2xf32>, tensor<1x2xf32>)
+//       CHECK:     scf.forall.in_parallel
+//       CHECK:   mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
+
+// -----
+
+#map = affine_map<(d0, d1) -> (d0, d1)>
+module {
+  func.func @inferred_small_inner_dim_fill_vector_sizes(%0: tensor<4x16x8x4x16x2x4xf16>, %1: tensor<4x16x8x4x16x2x4xf16>) -> tensor<4x16x8x4x16x2x4xf16>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [256, 1, 1] subgroup_size = 64, {}>
+      } {
+    %2 = linalg.copy {lowering_config = #iree_gpu.derived_thread_config}
+        ins(%0 : tensor<4x16x8x4x16x2x4xf16>)
+        outs(%1 : tensor<4x16x8x4x16x2x4xf16>) -> tensor<4x16x8x4x16x2x4xf16>
+    return %2 : tensor<4x16x8x4x16x2x4xf16>
+  }
+}
+
+// CHECK-LABEL: func.func @inferred_small_inner_dim_fill_vector_sizes
+//       CHECK:   scf.forall ({{.*}}) = (0, 0, 0, 0, 0, 0, 0) to (4, 16, 8, 4, 16, 2, 4) step (1, 1, 1, 1, 1, 2, 4)
+//       CHECK:     linalg.copy{{.*}}ins({{.*}} : tensor<1x1x1x1x1x2x4xf16>) outs({{.*}} : tensor<1x1x1x1x1x2x4xf16>)
+//       CHECK:     scf.forall.in_parallel
+//       CHECK:   mapping = [#gpu.thread<linear_dim_6>, {{.*}}, #gpu.thread<linear_dim_0>]
+
+// -----
+
+#map = affine_map<(d0, d1) -> (d0, d1)>
+module {
+  func.func @inferred_small_inner_dim_dont_fill_non_contiguous(
+    %0: tensor<4x16x4x4xf16>, %1: tensor<4x16x4x4xf16>) -> tensor<4x16x4x4xf16>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [64, 1, 1] subgroup_size = 64, {}>
+      } {
+    %2 = linalg.copy {lowering_config = #iree_gpu.derived_thread_config}
+        ins(%0 : tensor<4x16x4x4xf16>)
+        outs(%1 : tensor<4x16x4x4xf16>) -> tensor<4x16x4x4xf16>
+    return %2 : tensor<4x16x4x4xf16>
+  }
+}
+
+// CHECK-LABEL: func.func @inferred_small_inner_dim_dont_fill_non_contiguous
+//       CHECK:   scf.forall ({{.*}}) = (0, 0, 0, 0) to (4, 16, 4, 4) step (1, 1, 1, 4)
+//       CHECK:     linalg.copy{{.*}}ins({{.*}} : tensor<1x1x1x4xf16>) outs({{.*}} : tensor<1x1x1x4xf16>)
+//       CHECK:     scf.forall.in_parallel
+//       CHECK:   mapping = [#gpu.thread<linear_dim_3>, {{.*}}, #gpu.thread<linear_dim_0>]
+
+// -----
+
+#map = affine_map<(d0, d1) -> (d0, d1)>
+module {
+  func.func @inferred_unaligned(%0: tensor<70xf16>, %1: tensor<70xf16>) -> tensor<70xf16>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [64, 1, 1] subgroup_size = 64, {}>
+      } {
+    %2 = linalg.copy {lowering_config = #iree_gpu.derived_thread_config}
+        ins(%0 : tensor<70xf16>)
+        outs(%1 : tensor<70xf16>) -> tensor<70xf16>
+    return %2 : tensor<70xf16>
+  }
+}
+
+// CHECK-LABEL: func.func @inferred_unaligned
+//       CHECK:   scf.forall ({{.*}}) = (0) to (70) step (8)
+//       CHECK:     linalg.copy{{.*}}ins({{.*}} : tensor<?xf16>) outs({{.*}} : tensor<?xf16>)
+//       CHECK:     scf.forall.in_parallel
+//       CHECK:   mapping = [#gpu.thread<linear_dim_0>]
+
+// -----
+
+#map = affine_map<(d0, d1) -> (d0, d1)>
+module {
+  func.func @inferred_smaller_load(%0: tensor<128xf16>, %1: tensor<128xf16>) -> tensor<128xf16>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [64, 1, 1] subgroup_size = 64, {}>
+      } {
+    %2 = linalg.copy {lowering_config = #iree_gpu.derived_thread_config}
+        ins(%0 : tensor<128xf16>)
+        outs(%1 : tensor<128xf16>) -> tensor<128xf16>
+    return %2 : tensor<128xf16>
+  }
+}
+
+// CHECK-LABEL: func.func @inferred_smaller_load
+//       CHECK:   scf.forall ({{.*}}) = (0) to (128) step (2)
+//       CHECK:     linalg.copy{{.*}}ins({{.*}} : tensor<2xf16>) outs({{.*}} : tensor<2xf16>)
+//       CHECK:     scf.forall.in_parallel
+//       CHECK:   mapping = [#gpu.thread<linear_dim_0>]
+
+// -----
+
+#config = #iree_gpu.derived_thread_config
+module {
+  func.func @inferred_im2col(%2: tensor<2x34x34x128xf16>, %3: tensor<2x128x8xf16>) -> tensor<2x128x8xf16>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [16, 32, 1] subgroup_size = 64, {}>
+      } {
+    %4 = iree_linalg_ext.im2col {lowering_config = #config}
+      strides = [1, 1] dilations = [1, 1] kernel_size = [3, 3]
+      m_offset = [0] * [1] k_offset = [0] * [1]
+      batch_pos = [0] m_pos = [2, 3] k_pos = [1]
+      ins(%2 : tensor<2x34x34x128xf16>)
+      outs(%3 : tensor<2x128x8xf16>) -> tensor<2x128x8xf16>
+    return %4 : tensor<2x128x8xf16>
+  }
+}
+
+// CHECK-LABEL: func.func @inferred_im2col
+//       CHECK:   scf.forall ({{.*}}) = (0, 0, 0) to (2, 128, 8) step (1, 1, 4)
+//       CHECK:     iree_linalg_ext.im2col {{.*}} ins(%{{.*}}: tensor<1x34x34x128xf16>) outs({{.*}}: tensor<1x1x4xf16>)
+//       CHECK:     scf.forall.in_parallel
+//       CHECK:   mapping = [#gpu.thread<linear_dim_2>, #gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
+
+// -----
+
+#config = #iree_gpu.derived_thread_config
+func.func @scatter(%arg0: tensor<3x32x16xf32>, %arg1: tensor<3x1xi32>) -> tensor<3x32x16xf32>
+      attributes {
+        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [64, 1, 1] subgroup_size = 64, {}>
+      } {
+  %cst = arith.constant 0.000000e+00 : f32
+  %0 = tensor.empty() : tensor<3x32x16xf32>
+  %1 = iree_linalg_ext.scatter {lowering_config = #config} dimension_map = [0] unique_indices(true)
+    ins(%arg0, %arg1 : tensor<3x32x16xf32>, tensor<3x1xi32>) outs(%0 : tensor<3x32x16xf32>) {
+  ^bb0(%arg2: f32, %arg3: f32):
+    iree_linalg_ext.yield %arg2 : f32
+  } -> tensor<3x32x16xf32>
+  return %1 : tensor<3x32x16xf32>
+}
+
+// CHECK-LABEL: func.func @scatter
+//       CHECK:   scf.forall ({{.*}}) = (0, 0, 0) to (3, 32, 16) step (1, 1, 4)
+//       CHECK:     linalg_ext.scatter
+//       CHECK:     scf.forall.in_parallel
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_apply_tiling_level.mlir b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_apply_tiling_level.mlir
index ee0c9a2..99a46be 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_apply_tiling_level.mlir
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_apply_tiling_level.mlir
@@ -206,211 +206,6 @@
 
 // -----
 
-#config = #iree_gpu.derived_thread_config
-#map = affine_map<(d0, d1) -> (d0, d1)>
-module {
-  func.func @inferred_add_tensor(%3: tensor<64x256xf32>, %4: tensor<64x256xf32>, %5: tensor<64x256xf32>) -> tensor<64x256xf32>
-      attributes {
-        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [16, 32, 1] subgroup_size = 64, {}>
-      } {
-    %6 = linalg.generic {
-      indexing_maps = [#map, #map, #map],
-      iterator_types = ["parallel", "parallel"]
-      } ins(%3, %4 : tensor<64x256xf32>, tensor<64x256xf32>) outs(%5 : tensor<64x256xf32>) attrs =  {lowering_config = #config} {
-    ^bb0(%in: f32, %in_0: f32, %out: f32):
-      %7 = arith.addf %in, %in_0 : f32
-      linalg.yield %7 : f32
-    } -> tensor<64x256xf32>
-    return %6 : tensor<64x256xf32>
-  }
-}
-
-// Verify that no loops are generated without a reduction configuration.
-// CHECK-LABEL: func.func @inferred_add_tensor
-//   CHECK-NOT:   scf.for
-
-// THREAD-LABEL: func.func @inferred_add_tensor
-//       THREAD:   scf.forall ({{.*}}) = (0, 0) to (64, 256) step (1, 4)
-//       THREAD:     linalg.generic {{.*}} ins(%{{.*}}: tensor<1x4xf32>, tensor<1x4xf32>)
-//       THREAD:     scf.forall.in_parallel
-//       THREAD:   mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
-
-// -----
-
-#config = #iree_gpu.derived_thread_config
-#map = affine_map<(d0, d1) -> (d0, d1)>
-module {
-  func.func @inferred_dynamic(%3: tensor<?x?xf32>, %4: tensor<?x?xf32>, %5: tensor<?x?xf32>) -> tensor<?x?xf32>
-      attributes {
-        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [16, 32, 1] subgroup_size = 64, {}>
-      } {
-    %6 = linalg.generic {
-      indexing_maps = [#map, #map, #map],
-      iterator_types = ["parallel", "parallel"]
-      } ins(%3, %4 : tensor<?x?xf32>, tensor<?x?xf32>) outs(%5 : tensor<?x?xf32>) attrs =  {lowering_config = #config} {
-    ^bb0(%in: f32, %in_0: f32, %out: f32):
-      %7 = arith.addf %in, %in_0 : f32
-      linalg.yield %7 : f32
-    } -> tensor<?x?xf32>
-    return %6 : tensor<?x?xf32>
-  }
-}
-
-// THREAD-LABEL: func.func @inferred_dynamic
-//  THREAD-SAME:   %[[A:[A-Za-z0-9]+]]: tensor<?x?xf32>
-//   THREAD-DAG:   %[[DIM0:.+]] = tensor.dim %[[A]], %c0 : tensor<?x?xf32>
-//   THREAD-DAG:   %[[DIM1:.+]] = tensor.dim %[[A]], %c1 : tensor<?x?xf32>
-//       THREAD:   scf.forall ({{.*}}) = (0, 0) to (%[[DIM0]], %[[DIM1]]) step (1, 4)
-//       THREAD:     linalg.generic {{.*}} ins(%{{.*}}: tensor<1x?xf32>, tensor<1x?xf32>)
-//       THREAD:     scf.forall.in_parallel
-//       THREAD:   mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
-
-// -----
-
-#config = #iree_gpu.derived_thread_config
-#map = affine_map<(d0, d1) -> (d0, d1)>
-module {
-  func.func @inferred_small_inner_dim(%3: tensor<8x2xf32>, %4: tensor<8x2xf32>, %5: tensor<8x2xf32>) -> tensor<8x2xf32>
-      attributes {
-        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [16, 32, 1] subgroup_size = 64, {}>
-      } {
-    %6 = linalg.generic {
-      indexing_maps = [#map, #map, #map],
-      iterator_types = ["parallel", "parallel"]
-      } ins(%3, %4 : tensor<8x2xf32>, tensor<8x2xf32>) outs(%5 : tensor<8x2xf32>) attrs =  {lowering_config = #config} {
-    ^bb0(%in: f32, %in_0: f32, %out: f32):
-      %7 = arith.addf %in, %in_0 : f32
-      linalg.yield %7 : f32
-    } -> tensor<8x2xf32>
-    return %6 : tensor<8x2xf32>
-  }
-}
-
-// THREAD-LABEL: func.func @inferred_small_inner_dim
-//       THREAD:   scf.forall ({{.*}}) = (0, 0) to (8, 2) step (1, 2)
-//       THREAD:     linalg.generic {{.*}} ins(%{{.*}}: tensor<1x2xf32>, tensor<1x2xf32>)
-//       THREAD:     scf.forall.in_parallel
-//       THREAD:   mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
-
-// -----
-
-#config = #iree_gpu.derived_thread_config
-#map = affine_map<(d0, d1) -> (d0, d1)>
-module {
-  func.func @inferred_small_inner_dim_fill_vector_sizes(%0: tensor<4x16x8x4x16x2x4xf16>, %1: tensor<4x16x8x4x16x2x4xf16>) -> tensor<4x16x8x4x16x2x4xf16>
-      attributes {
-        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [256, 1, 1] subgroup_size = 64, {}>
-      } {
-    %2 = linalg.copy {lowering_config = #iree_gpu.derived_thread_config}
-        ins(%0 : tensor<4x16x8x4x16x2x4xf16>)
-        outs(%1 : tensor<4x16x8x4x16x2x4xf16>) -> tensor<4x16x8x4x16x2x4xf16>
-    return %2 : tensor<4x16x8x4x16x2x4xf16>
-  }
-}
-
-// THREAD-LABEL: func.func @inferred_small_inner_dim_fill_vector_sizes
-//       THREAD:   scf.forall ({{.*}}) = (0, 0, 0, 0, 0, 0, 0) to (4, 16, 8, 4, 16, 2, 4) step (1, 1, 1, 1, 1, 2, 4)
-//       THREAD:     linalg.copy{{.*}}ins({{.*}} : tensor<1x1x1x1x1x2x4xf16>) outs({{.*}} : tensor<1x1x1x1x1x2x4xf16>)
-//       THREAD:     scf.forall.in_parallel
-//       THREAD:   mapping = [#gpu.thread<linear_dim_6>, {{.*}}, #gpu.thread<linear_dim_0>]
-
-// -----
-
-#config = #iree_gpu.derived_thread_config
-#map = affine_map<(d0, d1) -> (d0, d1)>
-module {
-  func.func @inferred_small_inner_dim_dont_fill_non_contiguous(
-    %0: tensor<4x16x4x4xf16>, %1: tensor<4x16x4x4xf16>) -> tensor<4x16x4x4xf16>
-      attributes {
-        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [64, 1, 1] subgroup_size = 64, {}>
-      } {
-    %2 = linalg.copy {lowering_config = #iree_gpu.derived_thread_config}
-        ins(%0 : tensor<4x16x4x4xf16>)
-        outs(%1 : tensor<4x16x4x4xf16>) -> tensor<4x16x4x4xf16>
-    return %2 : tensor<4x16x4x4xf16>
-  }
-}
-
-// THREAD-LABEL: func.func @inferred_small_inner_dim_dont_fill_non_contiguous
-//       THREAD:   scf.forall ({{.*}}) = (0, 0, 0, 0) to (4, 16, 4, 4) step (1, 1, 1, 4)
-//       THREAD:     linalg.copy{{.*}}ins({{.*}} : tensor<1x1x1x4xf16>) outs({{.*}} : tensor<1x1x1x4xf16>)
-//       THREAD:     scf.forall.in_parallel
-//       THREAD:   mapping = [#gpu.thread<linear_dim_3>, {{.*}}, #gpu.thread<linear_dim_0>]
-
-// -----
-
-#config = #iree_gpu.derived_thread_config
-#map = affine_map<(d0, d1) -> (d0, d1)>
-module {
-  func.func @inferred_unaligned(%0: tensor<70xf16>, %1: tensor<70xf16>) -> tensor<70xf16>
-      attributes {
-        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [64, 1, 1] subgroup_size = 64, {}>
-      } {
-    %2 = linalg.copy {lowering_config = #iree_gpu.derived_thread_config}
-        ins(%0 : tensor<70xf16>)
-        outs(%1 : tensor<70xf16>) -> tensor<70xf16>
-    return %2 : tensor<70xf16>
-  }
-}
-
-// THREAD-LABEL: func.func @inferred_unaligned
-//       THREAD:   scf.forall ({{.*}}) = (0) to (70) step (8)
-//       THREAD:     linalg.copy{{.*}}ins({{.*}} : tensor<?xf16>) outs({{.*}} : tensor<?xf16>)
-//       THREAD:     scf.forall.in_parallel
-//       THREAD:   mapping = [#gpu.thread<linear_dim_0>]
-
-// -----
-
-#config = #iree_gpu.derived_thread_config
-#map = affine_map<(d0, d1) -> (d0, d1)>
-module {
-  func.func @inferred_smaller_load(%0: tensor<128xf16>, %1: tensor<128xf16>) -> tensor<128xf16>
-      attributes {
-        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [64, 1, 1] subgroup_size = 64, {}>
-      } {
-    %2 = linalg.copy {lowering_config = #iree_gpu.derived_thread_config}
-        ins(%0 : tensor<128xf16>)
-        outs(%1 : tensor<128xf16>) -> tensor<128xf16>
-    return %2 : tensor<128xf16>
-  }
-}
-
-// THREAD-LABEL: func.func @inferred_smaller_load
-//       THREAD:   scf.forall ({{.*}}) = (0) to (128) step (2)
-//       THREAD:     linalg.copy{{.*}}ins({{.*}} : tensor<2xf16>) outs({{.*}} : tensor<2xf16>)
-//       THREAD:     scf.forall.in_parallel
-//       THREAD:   mapping = [#gpu.thread<linear_dim_0>]
-
-// -----
-
-#config = #iree_gpu.derived_thread_config
-module {
-  func.func @inferred_im2col(%2: tensor<2x34x34x128xf16>, %3: tensor<2x128x8xf16>) -> tensor<2x128x8xf16>
-      attributes {
-        translation_info = #iree_codegen.translation_info<pipeline = LLVMGPUTileAndFuse workgroup_size = [16, 32, 1] subgroup_size = 64, {}>
-      } {
-    %4 = iree_linalg_ext.im2col {lowering_config = #config}
-      strides = [1, 1] dilations = [1, 1] kernel_size = [3, 3]
-      m_offset = [0] * [1] k_offset = [0] * [1]
-      batch_pos = [0] m_pos = [2, 3] k_pos = [1]
-      ins(%2 : tensor<2x34x34x128xf16>)
-      outs(%3 : tensor<2x128x8xf16>) -> tensor<2x128x8xf16>
-    return %4 : tensor<2x128x8xf16>
-  }
-}
-
-// Verify that no loops are generated without a reduction configuration.
-// CHECK-LABEL: func.func @inferred_im2col
-//   CHECK-NOT:   scf.for
-
-// THREAD-LABEL: func.func @inferred_im2col
-//       THREAD:   scf.forall ({{.*}}) = (0, 0, 0) to (2, 128, 8) step (1, 1, 4)
-//       THREAD:     iree_linalg_ext.im2col {{.*}} ins(%{{.*}}: tensor<1x34x34x128xf16>) outs({{.*}}: tensor<1x1x4xf16>)
-//       THREAD:     scf.forall.in_parallel
-//       THREAD:   mapping = [#gpu.thread<linear_dim_2>, #gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
-
-// -----
-
 #config = #iree_gpu.lowering_config<{reduction = [0, 0, 8], subgroup = [2, 4]}>
 #contraction_accesses = [
  affine_map<(i, j, k) -> (i, k)>,
diff --git a/compiler/src/iree/compiler/Codegen/Dialect/GPU/IR/DerivedConfigUtils.cpp b/compiler/src/iree/compiler/Codegen/Dialect/GPU/IR/DerivedConfigUtils.cpp
index 6f7b75f..249ba7a 100644
--- a/compiler/src/iree/compiler/Codegen/Dialect/GPU/IR/DerivedConfigUtils.cpp
+++ b/compiler/src/iree/compiler/Codegen/Dialect/GPU/IR/DerivedConfigUtils.cpp
@@ -153,7 +153,17 @@
       .Case([&](IREE::LinalgExt::Im2colOp im2colOp) -> SmallVector<int64_t> {
         return deriveIm2colOpThreadTileSizes(im2colOp, numThreads);
       })
-      .Default([](Operation *op) -> SmallVector<int64_t> { return {}; });
+      .Case([&](IREE::LinalgExt::ScatterOp scatterOp) -> SmallVector<int64_t> {
+        int64_t loopDepth = scatterOp.getLoopIteratorTypes().size();
+        SmallVector<int64_t> loopBounds =
+            scatterOp.getStaticLoopRanges().value_or(
+                SmallVector<int64_t>(loopDepth, ShapedType::kDynamic));
+        int64_t elemBits = scatterOp.getOriginalType().getElementTypeBitWidth();
+        int64_t vectorSize = kPreferredCopyNumBits / elemBits;
+        return getVectorTileSizesFromLoopRanges(loopBounds, numThreads,
+                                                vectorSize);
+      })
+      .Default([&](Operation *op) -> SmallVector<int64_t> { return {}; });
 }
 
 } // namespace mlir::iree_compiler::IREE::GPU