e2e microkernel pipeline + argmax ukernel on ROCM backend. (#15943)

This commit presents is an end-to-end ukernel pipeline on ROCm with argmax as the first ukernel. It is
end-to-end in a because we automatically generate and link the bitcode, which means end users do not need to manually link bitcodes or hand-modify the kernel to have ukernel.generics.

This work is based on Ukernel lowerings on CPU written by Benoit Jacob
as well as a ukernel on CUDA sample by Mahesh Ravishankar.
diff --git a/compiler/plugins/target/ROCM/BUILD.bazel b/compiler/plugins/target/ROCM/BUILD.bazel
index 3186984..ce37323 100644
--- a/compiler/plugins/target/ROCM/BUILD.bazel
+++ b/compiler/plugins/target/ROCM/BUILD.bazel
@@ -29,12 +29,15 @@
     deps = [
         "//compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR:IREECodegenDialect",
         "//compiler/src/iree/compiler/Codegen/LLVMGPU",
+        "//compiler/src/iree/compiler/Codegen/Utils",
         "//compiler/src/iree/compiler/Dialect/HAL/Target",
+        "//compiler/src/iree/compiler/Dialect/HAL/Target:LLVMLinkerUtils",
         "//compiler/src/iree/compiler/PluginAPI",
         "//compiler/src/iree/compiler/Utils",
         "//runtime/src/iree/schemas:rocm_executable_def_c_fbs",
         "@llvm-project//llvm:AMDGPUCodeGen",
         "@llvm-project//llvm:Analysis",
+        "@llvm-project//llvm:BitWriter",
         "@llvm-project//llvm:Core",
         "@llvm-project//llvm:IPO",
         "@llvm-project//llvm:IRReader",
diff --git a/compiler/plugins/target/ROCM/CMakeLists.txt b/compiler/plugins/target/ROCM/CMakeLists.txt
index c5cc9f4..400a07f 100644
--- a/compiler/plugins/target/ROCM/CMakeLists.txt
+++ b/compiler/plugins/target/ROCM/CMakeLists.txt
@@ -28,6 +28,7 @@
   DEPS
     LLVMAMDGPUCodeGen
     LLVMAnalysis
+    LLVMBitWriter
     LLVMCore
     LLVMIRReader
     LLVMLinker
@@ -52,7 +53,9 @@
     MLIRTargetLLVMIRExport
     iree::compiler::Codegen::Dialect::Codegen::IR::IREECodegenDialect
     iree::compiler::Codegen::LLVMGPU
+    iree::compiler::Codegen::Utils
     iree::compiler::Dialect::HAL::Target
+    iree::compiler::Dialect::HAL::Target::LLVMLinkerUtils
     iree::compiler::PluginAPI
     iree::compiler::Utils
     iree::schemas::rocm_executable_def_c_fbs
diff --git a/compiler/plugins/target/ROCM/ROCMTarget.cpp b/compiler/plugins/target/ROCM/ROCMTarget.cpp
index 1b57579..19700a5 100644
--- a/compiler/plugins/target/ROCM/ROCMTarget.cpp
+++ b/compiler/plugins/target/ROCM/ROCMTarget.cpp
@@ -11,12 +11,14 @@
 
 #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.h"
 #include "iree/compiler/Codegen/LLVMGPU/Passes.h"
+#include "iree/compiler/Dialect/HAL/Target/LLVMLinkerUtils.h"
 #include "iree/compiler/Dialect/HAL/Target/TargetRegistry.h"
 #include "iree/compiler/PluginAPI/Client.h"
 #include "iree/compiler/Utils/FlatbufferUtils.h"
 #include "iree/compiler/Utils/ToolUtils.h"
 #include "iree/schemas/rocm_executable_def_builder.h"
 #include "llvm/Analysis/TargetTransformInfo.h"
+#include "llvm/Bitcode/BitcodeWriter.h"
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/LegacyPassManager.h"
 #include "llvm/IR/Module.h"
@@ -47,6 +49,7 @@
   bool linkBitcode = false;
   std::string bitcodeDirectory;
   int wavesPerEu = 0;
+  std::string enableROCMUkernels = "none";
 
   void bindOptions(OptionsBinder &binder) {
     static llvm::cl::OptionCategory category("ROCM HAL Target");
@@ -62,10 +65,27 @@
                     llvm::cl::cat(category),
                     llvm::cl::desc("Optimization hint specifying minimum "
                                    "number of waves per execution unit"));
+    binder.opt<std::string>(
+        "iree-rocm-enable-ukernels", enableROCMUkernels,
+        llvm::cl::cat(category),
+        llvm::cl::desc(
+            "Enables microkernels in the llvmcpu backend. May be "
+            "`default`, `none`, `all`, or a comma-separated list of "
+            "specific unprefixed microkernels to enable, e.g. `mmt4d`."));
   }
 };
 } // namespace
 
+static void dumpModuleToPath(StringRef path, StringRef baseName,
+                             StringRef suffix, StringRef extension,
+                             llvm::Module &module) {
+  llvm::SmallVector<char, 0> data;
+  llvm::raw_svector_ostream ostream(data);
+  module.print(ostream, nullptr);
+  dumpDataToPath(path, baseName, suffix, extension,
+                 StringRef(data.data(), data.size()));
+}
+
 static std::string translateModuleToObj(llvm::Module &module,
                                         llvm::TargetMachine &targetMachine) {
   std::string targetObj;
@@ -309,6 +329,23 @@
     iree_compiler::FlatbufferBuilder builder;
     iree_hal_rocm_ExecutableDef_start_as_root(builder);
 
+    // Link user modules and libdevice (if required).
+    // Note that linking order matters:
+    llvm::Linker linker(*llvmModule);
+    if (failed(linkCmdlineBitcodeFiles(
+            variantOp.getLoc(), linker, llvm::Linker::OverrideFromSrc,
+            *targetMachine, llvmModule->getContext()))) {
+      return failure();
+    }
+
+    if (!options.enableROCMUkernels.empty() ||
+        options.enableROCMUkernels != "none") {
+      auto enabledUkernelsStr = StringRef(options.enableROCMUkernels);
+      linkUkernelBCFiles(llvmModule.get(), variantOp.getLoc(),
+                         enabledUkernelsStr, options.targetChip,
+                         options.bitcodeDirectory,
+                         llvm::Linker::OverrideFromSrc, *targetMachine);
+    }
     // Link module to Device Library
     if (options.linkBitcode) {
       if (options.bitcodeDirectory.empty()) {
@@ -320,9 +357,19 @@
       linkROCDLIfNecessary(llvmModule.get(), options.targetChip,
                            options.bitcodeDirectory);
     }
+    if (!serOptions.dumpIntermediatesPath.empty()) {
+      dumpModuleToPath(serOptions.dumpIntermediatesPath,
+                       serOptions.dumpBaseName, variantOp.getName(),
+                       ".linked.ll", *llvmModule);
+    }
     // Add Optimize module
     optimizeModule(*llvmModule, *targetMachine);
-
+    // Store optimized ll.
+    if (!serOptions.dumpIntermediatesPath.empty()) {
+      dumpModuleToPath(serOptions.dumpIntermediatesPath,
+                       serOptions.dumpBaseName, variantOp.getName(),
+                       ".optimized.ll", *llvmModule);
+    }
     // Serialize hsaco kernel into the binary that we will embed in the
     // final FlatBuffer.
     std::unique_ptr<llvm::Module> moduleCopy;
@@ -406,6 +453,8 @@
     // Set target arch
     addConfig("target_arch", StringAttr::get(context, options.targetChip));
 
+    addConfig("ukernels", StringAttr::get(context, options.enableROCMUkernels));
+
     auto configAttr = b.getDictionaryAttr(configItems);
     return IREE::HAL::ExecutableTargetAttr::get(
         context, b.getStringAttr("rocm"), b.getStringAttr("rocm-hsaco-fb"),
diff --git a/compiler/plugins/target/ROCM/ROCMTargetUtils.cpp b/compiler/plugins/target/ROCM/ROCMTargetUtils.cpp
index 51b1e2f..7798693 100644
--- a/compiler/plugins/target/ROCM/ROCMTargetUtils.cpp
+++ b/compiler/plugins/target/ROCM/ROCMTargetUtils.cpp
@@ -6,6 +6,8 @@
 
 #include "./ROCMTargetUtils.h"
 
+#include "iree/compiler/Codegen/Utils/GPUUtils.h"
+#include "iree/compiler/Dialect/HAL/Target/LLVMLinkerUtils.h"
 #include "iree/compiler/Utils/ToolUtils.h"
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/Module.h"
@@ -92,6 +94,48 @@
   return success();
 }
 
+LogicalResult linkPathBitcodeFiles(Location loc, llvm::Linker &linker,
+                                   unsigned linkerFlags, StringRef path,
+                                   llvm::TargetMachine &targetMachine,
+                                   llvm::LLVMContext &context) {
+  auto bitcodeBufferRef = llvm::MemoryBuffer::getFile(path);
+  if (auto ec = bitcodeBufferRef.getError()) {
+    return mlir::emitError(loc) << "failed reading user bitcode file `" << path
+                                << "`: " << ec.message();
+  }
+  auto setAlwaysInline = [&](llvm::Module &module) {
+    if (targetMachine.getTargetCPU().contains("gfx10") ||
+        targetMachine.getTargetCPU().contains("gfx11")) {
+      // some ROCM/HIP functions for gfx10 or gfx11 has accuracy issue if
+      // inlined.
+      return;
+    }
+    for (auto &func : module.getFunctionList()) {
+      // Some ROCM/HIP builtin functions have Optnone and NoInline for default.
+      if (targetMachine.getTargetTriple().isAMDGCN()) {
+        if (func.hasFnAttribute(llvm::Attribute::OptimizeNone)) {
+          func.removeFnAttr(llvm::Attribute::OptimizeNone);
+        }
+        if (targetMachine.getTargetTriple().isAMDGCN() &&
+            func.hasFnAttribute(llvm::Attribute::NoInline)) {
+          func.removeFnAttr(llvm::Attribute::NoInline);
+        }
+      }
+      func.addFnAttr(llvm::Attribute::AlwaysInline);
+    }
+  };
+  if (failed(linkBitcodeModule(
+          loc, linker, linkerFlags, targetMachine, path,
+          llvm::parseBitcodeFile(*bitcodeBufferRef->get(), context),
+          setAlwaysInline))) {
+    return mlir::emitError(loc) << "failed linking in user bitcode file `"
+                                << path << "` for target triple '"
+                                << targetMachine.getTargetTriple().str() << "'";
+  }
+
+  return success();
+}
+
 static std::vector<std::string> getROCDLPaths(std::string targetChip,
                                               std::string bitCodeDir) {
   // AMDGPU bitcodes.
@@ -107,6 +151,36 @@
   return result;
 }
 
+static std::vector<std::string> getUkernelPaths(StringRef enabledUkernelsStr,
+                                                StringRef targetChip,
+                                                StringRef bitCodeDir) {
+  // AMD bitcodes.
+  std::vector<std::string> selectedUkernelNames;
+  if (enabledUkernelsStr == "all") {
+    const char *allUkernelNames[] = {"argmax"};
+    size_t numUkernels = sizeof(allUkernelNames) / sizeof(allUkernelNames[0]);
+    for (int i = 0; i < numUkernels; i++) {
+      selectedUkernelNames.push_back(allUkernelNames[i]);
+    }
+  } else {
+    while (!enabledUkernelsStr.empty()) {
+      auto split = enabledUkernelsStr.split(',');
+      selectedUkernelNames.push_back(split.first.str());
+      enabledUkernelsStr = split.second;
+    }
+  }
+
+  // Construct full path to ROCDL bitcode libraries.
+  std::vector<std::string> result;
+  std::string app = "/";
+  for (auto &kernelName : selectedUkernelNames) {
+    std::string filename =
+        "rocm_" + kernelName + "_ukernel_" + targetChip.str();
+    result.push_back(bitCodeDir.str() + app + filename + ".bc");
+  }
+  return result;
+}
+
 static void overridePlatformGlobal(llvm::Module *module, StringRef globalName,
                                    uint32_t newValue, llvm::Type *globalTy) {
   // NOTE: the global will not be defined if it is not used in the module.
@@ -180,6 +254,26 @@
   };
 }
 
+// Links optimized Ukernel bitcodes into the given module if the module needs
+// it.
+void linkUkernelBCFiles(llvm::Module *module, Location loc,
+                        StringRef enabledUkernelsStr, StringRef targetChip,
+                        StringRef bitCodeDir, unsigned linkerFlags,
+                        llvm::TargetMachine &targetMachine) {
+  // Early exit if Ukernel not supported on target chip.
+  if (!iree_compiler::hasUkernelSupportedRocmArch(targetChip))
+    return;
+  std::vector<std::string> ukernelPaths =
+      getUkernelPaths(enabledUkernelsStr, targetChip, bitCodeDir);
+  llvm::Linker linker(*module);
+  for (auto &path : ukernelPaths) {
+    if (failed(linkPathBitcodeFiles(loc, linker, linkerFlags, StringRef(path),
+                                    targetMachine, module->getContext()))) {
+      llvm::WithColor::error(llvm::errs()) << "Fail to Link Ukernel.\n";
+    }
+  }
+}
+
 //===========Link LLVM Module to ROCDL End===================/
 
 //=====================Create HSACO Begin=============//
diff --git a/compiler/plugins/target/ROCM/ROCMTargetUtils.h b/compiler/plugins/target/ROCM/ROCMTargetUtils.h
index 39bb89f..171fded 100644
--- a/compiler/plugins/target/ROCM/ROCMTargetUtils.h
+++ b/compiler/plugins/target/ROCM/ROCMTargetUtils.h
@@ -9,6 +9,7 @@
 
 #include "iree/compiler/Dialect/HAL/Target/TargetBackend.h"
 #include "llvm/IR/Module.h"
+#include "llvm/Target/TargetMachine.h"
 
 namespace mlir::iree_compiler::IREE::HAL {
 
@@ -16,9 +17,17 @@
 void linkROCDLIfNecessary(llvm::Module *module, std::string targetChip,
                           std::string bitCodeDir);
 
+// Links optimized Ukernel module.
+void linkUkernelBCFiles(llvm::Module *module, Location loc,
+                        StringRef enabledUkernelsStr, StringRef targetChip,
+                        StringRef bitCodeDir, unsigned linkerFlags,
+                        llvm::TargetMachine &targetMachine);
 // Compiles ISAToHsaco Code
 std::string createHsaco(Location loc, const std::string isa, StringRef name);
 
+// Returns true if the rocm archtecture target is supported for ukernels.
+bool hasUkernelSupportedRocmArch(IREE::HAL::ExecutableTargetAttr targetAttr);
+
 } // namespace mlir::iree_compiler::IREE::HAL
 
 #endif // IREE_COMPILER_PLUGINS_TARGET_ROCM_ROCMTARGETUTILS_H_
diff --git a/compiler/plugins/target/ROCM/builtins/CMakeLists.txt b/compiler/plugins/target/ROCM/builtins/CMakeLists.txt
new file mode 100644
index 0000000..02d4284
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/CMakeLists.txt
@@ -0,0 +1,16 @@
+# Copyright 2023 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
+
+iree_cc_library(
+  NAME
+    defs
+  INCLUDES
+    ${CMAKE_CURRENT_SOURCE_DIR}/../..
+    ${CMAKE_CURRENT_BINARY_DIR}/../..
+  PUBLIC
+)
+
+iree_add_all_subdirs()
diff --git a/compiler/plugins/target/ROCM/builtins/ukernel/CMakeLists.txt b/compiler/plugins/target/ROCM/builtins/ukernel/CMakeLists.txt
new file mode 100644
index 0000000..da3a8d9
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/ukernel/CMakeLists.txt
@@ -0,0 +1,174 @@
+# Copyright 2023 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
+if (NOT IREE_TARGET_BACKEND_ROCM)
+  return()
+endif()
+
+# Check if HIP is installed on system.
+# HIP is required to compile ukernels.
+if (NOT IREE_ROCM_PATH)
+  set(IREE_ROCM_PATH "/opt/rocm")
+endif()
+set (IREE_ROCM_VERSION "${IREE_ROCM_PATH}/.info/version")
+if (NOT EXISTS ${IREE_ROCM_VERSION})
+  message(STATUS
+          "hip runtime cannot be found in ${IREE_ROCM_PATH}.
+          Please try setting IREE_ROCM_PATH to rocm directory.
+          Ukernel will not be compiled.")
+  return()
+endif()
+
+
+iree_add_all_subdirs()
+
+set(_platform_lib_reldir "iree_platform_libs/rocm")
+set(_device_bc_path "${IREE_COMPILER_DYLIB_DIR}/iree_platform_libs/rocm")
+set (_amd_ukernel_libs)
+set (_amd_ukernel_targets)
+function(iree_rocm_bitcode_library)
+  cmake_parse_arguments(
+    _RULE
+    ""
+    "NAME;OUT;ROCM_ARCH"
+    "SRCS;COPTS"
+    ${ARGN}
+  )
+
+  if(DEFINED _RULE_OUT)
+    set(_OUT "${_RULE_OUT}")
+  else()
+    set(_OUT "${_RULE_NAME}_${_RULE_ROCM_ARCH}.bc")
+  endif()
+
+  set(_ROCM_ARCH "${_RULE_ROCM_ARCH}")
+  set (OPT_FLAG "-O0")
+  if (_ROCM_ARCH MATCHES "GFX9")
+    set (OPT_FLAG "-O3")
+  endif()
+  set(_COPTS
+    "-x" "hip"
+    # Target architecture.
+    "--offload-arch=${_ROCM_ARCH}"
+
+    # Suppress warnings about missing path to rocm lib,
+    # and benign warning about ROCM version.
+    "-nogpulib"
+    "-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH"
+    # Avoid error caused by clang bundling.
+    "--offload-device-only"
+    # Only enable necessary optimizations S.T we can use -O3.
+    "-Xclang" "-disable-llvm-optzns"
+    "${OPT_FLAG}"
+
+    # Object file only in bitcode format:
+    "-c"
+    "-emit-llvm"
+  )
+
+  set(_BITCODE_FILES)
+  foreach(_SRC ${_RULE_SRCS})
+    get_filename_component(_BITCODE_SRC_PATH "${_SRC}" REALPATH)
+    set(_BITCODE_FILE "${_RULE_NAME}_${_SRC}_${_ROCM_ARCH}.bc")
+    list(APPEND _BITCODE_FILES ${_BITCODE_FILE})
+    add_custom_command(
+      OUTPUT
+        "${_BITCODE_FILE}"
+      COMMAND
+        "${IREE_CLANG_BINARY}"
+        ${_COPTS}
+        "${_BITCODE_SRC_PATH}"
+        "-o"
+        "${_BITCODE_FILE}"
+      DEPENDS
+        "${IREE_CLANG_BINARY}"
+        "${_SRC}"
+      COMMENT
+        "Compiling ${_SRC} to ${_BITCODE_FILE}"
+      VERBATIM
+    )
+  endforeach()
+
+  add_custom_command(
+    OUTPUT
+      "${_OUT}"
+    COMMAND
+      ${IREE_LLVM_LINK_BINARY}
+      ${_BITCODE_FILES}
+      "-o"
+      "${_OUT}"
+    DEPENDS
+      ${IREE_LLVM_LINK_BINARY}
+      ${_BITCODE_FILES}
+    COMMENT
+      "Linking bitcode to ${_OUT}"
+    VERBATIM
+  )
+  # Only add iree_${NAME} as custom target doesn't support aliasing to
+  # iree::${NAME}.
+  iree_package_name(_PACKAGE_NAME)
+  add_custom_target("${_PACKAGE_NAME}_${_RULE_NAME}_${_ROCM_ARCH}"
+    DEPENDS "${_OUT}"
+  )
+  set(_amd_ukernel_libs ${_amd_ukernel_libs} ${_OUT} PARENT_SCOPE)
+  set(_amd_ukernel_targets ${_amd_ukernel_targets} "${_PACKAGE_NAME}_${_RULE_NAME}_${_ROCM_ARCH}" PARENT_SCOPE)
+endfunction()
+
+# TODO: Decide what to build by default. No real constaints here
+#       except compile-time cost, so just picked out the popular ones.
+set(_ukernel_supported_chips "gfx90a" "gfx940" "gfx1030" "gfx1100")
+foreach(_amd_chip ${_ukernel_supported_chips})
+  iree_rocm_bitcode_library(
+    NAME
+      rocm_argmax_ukernel
+    ROCM_ARCH
+      ${_amd_chip}
+    SRCS
+      "argmax_ukernel.c"
+  )
+endforeach()
+
+# Copy UKernel into platform dir.
+set(_all_ukernel_bc_copy_commands)
+set(_all_ukernel_bc_files)
+set(_ukernel_lib_srcdir ${CMAKE_CURRENT_BINARY_DIR})
+foreach(_amd_ukernel_name ${_amd_ukernel_libs})
+  # Copy to lib/ tree.
+  set(_ukernel_bc_srcpath "${_ukernel_lib_srcdir}/${_amd_ukernel_name}")
+  set(_ukernel_bc_relpath "${_platform_lib_reldir}/${_amd_ukernel_name}")
+  list(APPEND _all_ukernel_bc_files "${IREE_COMPILER_DYLIB_DIR}/${_ukernel_bc_relpath}")
+  list(APPEND _all_ukernel_bc_deps "${_ukernel_bc_path}")
+  list(APPEND _all_ukernel_bc_copy_commands
+    COMMAND ${CMAKE_COMMAND} -E copy
+      "${_ukernel_bc_srcpath}"
+      "${IREE_COMPILER_DYLIB_DIR}/${_ukernel_bc_relpath}"
+  )
+  # Note this bc file as being part of the bundle that must be included with
+  # the compiler dylib.
+  set_property(GLOBAL APPEND PROPERTY IREE_COMPILER_DYLIB_RELPATHS "${_ukernel_bc_relpath}")
+endforeach()
+
+# Generate a custom target with all file level dependencies and commands to
+# copy to our build tree locations.
+# Our GenDeviceLibs target depends on all of the defined device lib targets.
+add_custom_command(
+  OUTPUT ${_all_ukernel_bc_files}
+  DEPENDS ${_amd_ukernel_targets}
+  POST_BUILD
+    ${_all_ukernel_bc_copy_commands}
+)
+
+add_custom_target(iree_builtin_ROCM_UkernelDeviceLibs
+  DEPENDS
+    ${_all_ukernel_bc_files}
+)
+
+# Ensure that the device libs are built when the compiler dylib is built.
+set_property(GLOBAL APPEND PROPERTY IREE_COMPILER_DYLIB_DEPENDS
+  iree_builtin_ROCM_UkernelDeviceLibs)
+
+# Install.
+install(FILES ${_all_ukernel_bc_files}
+  DESTINATION "${IREE_COMPILER_DYLIB_INSTALL_PREFIX}/${_platform_lib_reldir}")
diff --git a/compiler/plugins/target/ROCM/builtins/ukernel/argmax_ukernel.c b/compiler/plugins/target/ROCM/builtins/ukernel/argmax_ukernel.c
new file mode 100644
index 0000000..9c2caf3
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/ukernel/argmax_ukernel.c
@@ -0,0 +1,198 @@
+// Copyright 2023 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 <float.h>
+#include <hip/hip_fp16.h>
+#include <hip/hip_runtime.h>
+
+extern "C" __device__ __attribute__((const)) half __ockl_wfred_max_f16(half);
+extern "C" __device__ __attribute__((const))
+int64_t __ockl_wfred_min_i64(int64_t);
+extern "C" __device__ __attribute__((const))
+int32_t __ockl_wfred_min_i32(int32_t);
+
+/*
+Constraint/Tiling note:
+For simplicity, we distribute all parallel dim across different workgroup, and
+only use single subgroup/warp per workgroup. This constraint is also set during
+tiling phase in KernelConfig.
+*/
+
+extern "C" __device__ void __iree_uk_rocm_argmax_F32I32(float *inputBuffer,
+                                                        size_t input_offset,
+                                                        int32_t *outputBuffer,
+                                                        size_t output_offset,
+                                                        size_t reductionSize) {
+  uint laneID = __builtin_amdgcn_workitem_id_x();
+  // Set identity value to handle problem non divisible by subgroupSize.
+  float laneMax =
+      laneID >= reductionSize ? -FLT_MAX : inputBuffer[input_offset + laneID];
+  int32_t laneResult = laneID;
+
+  // NOTE: On F32 kernels with clang, reductionSize/blockDim.x has numerical
+  // inaccuracy.
+  uint numBatches = (reductionSize + warpSize - 1) / warpSize;
+  for (int i = 1; i < numBatches; ++i) {
+    uint idx = warpSize * i + laneID;
+    float newIn =
+        idx >= reductionSize ? -FLT_MAX : inputBuffer[input_offset + idx];
+    if (newIn == laneMax)
+      continue;
+    laneMax = __ocml_fmax_f32(newIn, laneMax);
+    laneResult = newIn == laneMax ? idx : laneResult;
+  }
+
+  // Final reduction with one subgroup
+  // NOTE: __ockl_wfred_max_f32 has correctness issue on gfx1100 documented on
+  // https://github.com/openxla/iree/issues/16112.
+  float wgMax = laneMax;
+  for (int i = 1; i < warpSize; i *= 2) {
+    wgMax = __ocml_fmax_f32(__shfl_xor(wgMax, i), wgMax);
+  }
+  // Check if there are multiple max value holders.
+  uint64_t laneHasMaxValmask = __ballot(wgMax == laneMax);
+  // if there is only one max value holder, write and exit.
+  if (__popcll(laneHasMaxValmask) == 1) {
+    if (wgMax == laneMax)
+      outputBuffer[output_offset] = laneResult;
+    return;
+  }
+  // if there are multiple max value holder, find smallest index (argmax
+  // semantics).
+  int32_t indexVal = wgMax == laneMax ? laneResult : __INT32_MAX__;
+  laneResult = __ockl_wfred_min_i32(indexVal);
+  if (laneID == 0)
+    outputBuffer[output_offset] = laneResult;
+}
+
+extern "C" __device__ void __iree_uk_rocm_argmax_F32I64(float *inputBuffer,
+                                                        size_t input_offset,
+                                                        int64_t *outputBuffer,
+                                                        size_t output_offset,
+                                                        size_t reductionSize) {
+  uint laneID = __builtin_amdgcn_workitem_id_x();
+  // Set identity value to handle problem non divisible by subgroupSize.
+  float laneMax =
+      laneID >= reductionSize ? -FLT_MAX : inputBuffer[input_offset + laneID];
+  int64_t laneResult = laneID;
+
+  // NOTE: On F32 kernels with clang, reductionSize/blockDim.x has numerical
+  // inaccuracy.
+  uint numBatches = (reductionSize + warpSize - 1) / warpSize;
+  for (int i = 1; i < numBatches; ++i) {
+    uint idx = warpSize * i + laneID;
+    float newIn =
+        idx >= reductionSize ? -FLT_MAX : inputBuffer[input_offset + idx];
+    if (newIn == laneMax)
+      continue;
+    laneMax = __ocml_fmax_f32(newIn, laneMax);
+    laneResult = newIn == laneMax ? idx : laneResult;
+  }
+
+  // Final reduction with one subgroup
+  // NOTE: __ockl_wfred_max_f32 has correctness issue on gfx1100 documented on
+  // https://github.com/openxla/iree/issues/16112.
+  float wgMax = laneMax;
+  for (int i = 1; i < warpSize; i *= 2) {
+    wgMax = __ocml_fmax_f32(__shfl_xor(wgMax, i), wgMax);
+  }
+  // Check if there are multiple max value holders.
+  uint64_t laneHasMaxValmask = __ballot(wgMax == laneMax);
+  // if there is only one max value holder, write and exit.
+  if (__popcll(laneHasMaxValmask) == 1) {
+    if (wgMax == laneMax)
+      outputBuffer[output_offset] = laneResult;
+    return;
+  }
+  // if there are multiple max value holder, find smallest index (argmax
+  // semantics).
+  int32_t indexVal = wgMax == laneMax ? laneResult : __INT64_MAX__;
+  laneResult = __ockl_wfred_min_i64(indexVal);
+  if (laneID == 0)
+    outputBuffer[output_offset] = laneResult;
+}
+
+extern "C" __device__ void __iree_uk_rocm_argmax_F16I32(half *inputBuffer,
+                                                        size_t input_offset,
+                                                        int32_t *outputBuffer,
+                                                        size_t output_offset,
+                                                        size_t reductionSize) {
+  half NEG_F16_MAX = __float2half(-65504.0f);
+  uint laneID = __builtin_amdgcn_workitem_id_x();
+  // Set identity value to handle problem non divisible by subgroupSize.
+  half laneMax = laneID >= reductionSize ? NEG_F16_MAX
+                                         : inputBuffer[input_offset + laneID];
+  int32_t laneResult = laneID;
+
+  uint numBatches = (reductionSize + warpSize - 1) / warpSize;
+  for (int i = 1; i < numBatches; ++i) {
+    uint idx = warpSize * i + laneID;
+    half newIn =
+        idx >= reductionSize ? NEG_F16_MAX : inputBuffer[input_offset + idx];
+    if (newIn == laneMax)
+      continue;
+    laneMax = __ocml_fmax_f16(newIn, laneMax);
+    laneResult = newIn == laneMax ? idx : laneResult;
+  }
+
+  // Final reduction with one subgroup
+  half wgMax = __ockl_wfred_max_f16(laneMax);
+  // Check if there are multiple max value holders.
+  uint64_t laneHasMaxValmask = __ballot(wgMax == laneMax);
+  // if there is only one max value holder, write and exit.
+  if (__popcll(laneHasMaxValmask) == 1) {
+    if (wgMax == laneMax)
+      outputBuffer[output_offset] = laneResult;
+    return;
+  }
+  // if there are multiple max value holder, find smallest index (argmax
+  // semantics).
+  int32_t indexVal = wgMax == laneMax ? laneResult : __INT32_MAX__;
+  laneResult = __ockl_wfred_min_i32(indexVal);
+  if (laneID == 0)
+    outputBuffer[output_offset] = laneResult;
+}
+
+extern "C" __device__ void __iree_uk_rocm_argmax_F16I64(half *inputBuffer,
+                                                        size_t input_offset,
+                                                        int64_t *outputBuffer,
+                                                        size_t output_offset,
+                                                        size_t reductionSize) {
+  half NEG_F16_MAX = __float2half(-65504.0f);
+  uint laneID = __builtin_amdgcn_workitem_id_x();
+  // Set identity value to handle problem non divisible by subgroupSize.
+  half laneMax = laneID >= reductionSize ? NEG_F16_MAX
+                                         : inputBuffer[input_offset + laneID];
+  int64_t laneResult = laneID;
+
+  uint numBatches = (reductionSize + warpSize - 1) / warpSize;
+  for (int i = 1; i < numBatches; ++i) {
+    uint idx = warpSize * i + laneID;
+    half newIn =
+        idx >= reductionSize ? NEG_F16_MAX : inputBuffer[input_offset + idx];
+    if (newIn == laneMax)
+      continue;
+    laneMax = __ocml_fmax_f16(newIn, laneMax);
+    laneResult = newIn == laneMax ? idx : laneResult;
+  }
+
+  // Final reduction with one subgroup
+  half wgMax = __ockl_wfred_max_f16(laneMax);
+  // Check if there are multiple max value holders.
+  uint64_t laneHasMaxValmask = __ballot(wgMax == laneMax);
+  // if there is only one max value holder, write and exit.
+  if (__popcll(laneHasMaxValmask) == 1) {
+    if (wgMax == laneMax)
+      outputBuffer[output_offset] = laneResult;
+    return;
+  }
+  // if there are multiple max value holder, find smallest index (argmax
+  // semantics).
+  int32_t indexVal = wgMax == laneMax ? laneResult : __INT64_MAX__;
+  laneResult = __ockl_wfred_min_i64(indexVal);
+  if (laneID == 0)
+    outputBuffer[output_offset] = laneResult;
+}
diff --git a/compiler/plugins/target/ROCM/builtins/ukernel/test/CMakeLists.txt b/compiler/plugins/target/ROCM/builtins/ukernel/test/CMakeLists.txt
new file mode 100644
index 0000000..e18c17e
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/ukernel/test/CMakeLists.txt
@@ -0,0 +1,21 @@
+# Copyright 2023 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
+
+iree_add_all_subdirs()
+
+iree_lit_test_suite(
+  NAME
+    lit
+  SRCS
+    "argmax_linking.mlir"
+  TOOLS
+    ${IREE_LLD_TARGET}
+    FileCheck
+    iree-compile
+  LABELS
+    "driver=rocm"
+    "hostonly"
+)
diff --git a/compiler/plugins/target/ROCM/builtins/ukernel/test/argmax_linking.mlir b/compiler/plugins/target/ROCM/builtins/ukernel/test/argmax_linking.mlir
new file mode 100644
index 0000000..7907af5
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/ukernel/test/argmax_linking.mlir
@@ -0,0 +1,201 @@
+// RUN: [[ $IREE_ROCM_DISABLE == 1 ]] || iree-compile --split-input-file --iree-hal-target-backends=rocm --iree-rocm-enable-ukernels=all --iree-rocm-target-chip=gfx1100 --compile-to=executable-targets %s | FileCheck %s
+
+// We want to check that uKernel is indeed generated from e2e workflow.
+
+// CHECK: llvm.func @__iree_uk_rocm_argmax_F32I64
+// CHECK: llvm.call @__iree_uk_rocm_argmax_F32I64
+func.func @argmax_1d_f32i64(%arg0: tensor<1x?xf32>) -> tensor<1x1xi64> {
+  %c0 = arith.constant 0 : index
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %cst_0 = arith.constant 0.000000e+00 : f16
+  %c-1_i64 = arith.constant -1 : i64
+  %15 = tensor.empty() : tensor<1xi64>
+  %16 = linalg.fill ins(%c0_i64 : i64) outs(%15 : tensor<1xi64>) -> tensor<1xi64>
+  %17 = tensor.empty() : tensor<1xf32>
+  %18 = linalg.fill ins(%cst : f32) outs(%17 : tensor<1xf32>) -> tensor<1xf32>
+  %19:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf32>) outs(%18, %16 : tensor<1xf32>, tensor<1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_2: i64):
+    %20 = linalg.index 1 : index
+    %21 = arith.index_cast %20 : index to i64
+    %22 = arith.maximumf %in, %out : f32
+    %23 = arith.cmpf ogt, %in, %out : f32
+    %24 = arith.select %23, %21, %out_2 : i64
+    linalg.yield %22, %24 : f32, i64
+  } -> (tensor<1xf32>, tensor<1xi64>)
+  %expanded_1 = tensor.expand_shape %19#1 [[0, 1]] : tensor<1xi64> into tensor<1x1xi64>
+  return %expanded_1 : tensor<1x1xi64>
+}
+
+// -----
+
+// CHECK: llvm.func @__iree_uk_rocm_argmax_F16I64
+// CHECK: llvm.call @__iree_uk_rocm_argmax_F16I64
+func.func @argmax_1d_f16i64(%arg0: tensor<1x?xf16>) -> tensor<1x1xi64> {
+  %c0 = arith.constant 0 : index
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFC00 : f16
+  %cst_0 = arith.constant 0.000000e+00 : f16
+  %c-1_i64 = arith.constant -1 : i64
+  %15 = tensor.empty() : tensor<1xi64>
+  %16 = linalg.fill ins(%c0_i64 : i64) outs(%15 : tensor<1xi64>) -> tensor<1xi64>
+  %17 = tensor.empty() : tensor<1xf16>
+  %18 = linalg.fill ins(%cst : f16) outs(%17 : tensor<1xf16>) -> tensor<1xf16>
+  %19:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf16>) outs(%18, %16 : tensor<1xf16>, tensor<1xi64>) {
+  ^bb0(%in: f16, %out: f16, %out_2: i64):
+    %20 = linalg.index 1 : index
+    %21 = arith.index_cast %20 : index to i64
+    %22 = arith.maximumf %in, %out : f16
+    %23 = arith.cmpf ogt, %in, %out : f16
+    %24 = arith.select %23, %21, %out_2 : i64
+    linalg.yield %22, %24 : f16, i64
+  } -> (tensor<1xf16>, tensor<1xi64>)
+  %expanded_1 = tensor.expand_shape %19#1 [[0, 1]] : tensor<1xi64> into tensor<1x1xi64>
+  return %expanded_1 : tensor<1x1xi64>
+}
+
+
+// -----
+
+// CHECK: llvm.func @__iree_uk_rocm_argmax_F32I64
+// CHECK: llvm.call @__iree_uk_rocm_argmax_F32I64
+func.func @argmax_2d_f32i64(%arg0: tensor<16x?xf32>) -> tensor<16x1xi64> {
+  %c0 = arith.constant 0 : index
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %cst_0 = arith.constant 0.000000e+00 : f16
+  %c-1_i64 = arith.constant -1 : i64
+  %15 = tensor.empty() : tensor<16xi64>
+  %16 = linalg.fill ins(%c0_i64 : i64) outs(%15 : tensor<16xi64>) -> tensor<16xi64>
+  %17 = tensor.empty() : tensor<16xf32>
+  %18 = linalg.fill ins(%cst : f32) outs(%17 : tensor<16xf32>) -> tensor<16xf32>
+  %19:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<16x?xf32>) outs(%18, %16 : tensor<16xf32>, tensor<16xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_2: i64):
+    %20 = linalg.index 1 : index
+    %21 = arith.index_cast %20 : index to i64
+    %22 = arith.maximumf %in, %out : f32
+    %23 = arith.cmpf ogt, %in, %out : f32
+    %24 = arith.select %23, %21, %out_2 : i64
+    linalg.yield %22, %24 : f32, i64
+  } -> (tensor<16xf32>, tensor<16xi64>)
+  %expanded_1 = tensor.expand_shape %19#1 [[0, 1]] : tensor<16xi64> into tensor<16x1xi64>
+  return %expanded_1 : tensor<16x1xi64>
+}
+
+// -----
+
+// CHECK: llvm.func @__iree_uk_rocm_argmax_F32I32
+// CHECK: llvm.call @__iree_uk_rocm_argmax_F32I32
+#map = affine_map<(d0, d1, d2) -> (d0, d1, d2)>
+#map1 = affine_map<(d0, d1, d2) -> (d0, d1)>
+func.func @argmax_3d_dyn_f32i32(%arg0: tensor<?x?x?xf32>) -> tensor<?x?xf32> {
+  %c0 = arith.constant 0 : index
+  %c0_i32 = arith.constant 0 : i32
+  %c1 = arith.constant 1 : index
+  %cst = arith.constant 0xFF800000 : f32
+  %cst_0 = arith.constant 0.000000e+00 : f16
+  %dim = tensor.dim %arg0, %c0 : tensor<?x?x?xf32>
+  %dim_1 = tensor.dim %arg0, %c1 : tensor<?x?x?xf32>
+  %0 = tensor.empty(%dim, %dim_1) : tensor<?x?xi32>
+  %1 = linalg.fill ins(%c0_i32 : i32) outs(%0 : tensor<?x?xi32>) -> tensor<?x?xi32>
+  %2 = tensor.empty(%dim, %dim_1) : tensor<?x?xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<?x?xf32>) -> tensor<?x?xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1, d2) -> (d0, d1, d2)>, affine_map<(d0, d1, d2) -> (d0, d1)>, affine_map<(d0, d1, d2) -> (d0, d1)>], iterator_types = ["parallel", "parallel", "reduction"]} ins(%arg0 : tensor<?x?x?xf32>) outs(%3, %1 : tensor<?x?xf32>, tensor<?x?xi32>) {
+  ^bb0(%in: f32, %out: f32, %out_2: i32):
+    %6 = linalg.index 2 : index
+    %7 = arith.index_cast %6 : index to i32
+    %8 = arith.maximumf %in, %out : f32
+    %9 = arith.cmpf ogt, %in, %out : f32
+    %10 = arith.select %9, %7, %out_2 : i32
+    linalg.yield %8, %10 : f32, i32
+  } -> (tensor<?x?xf32>, tensor<?x?xi32>)
+  %5 = arith.sitofp %4#1 : tensor<?x?xi32> to tensor<?x?xf32>
+  return %5 : tensor<?x?xf32>
+}
+
+// -----
+
+// CHECK: llvm.func @__iree_uk_rocm_argmax_F32I64
+// CHECK: llvm.call @__iree_uk_rocm_argmax_F32I64
+func.func @argmax_3d_dyn_f32i64(%arg0: tensor<?x?x?xf32>) -> tensor<?x?xf32> {
+  %c0 = arith.constant 0 : index
+  %c0_i64 = arith.constant 0 : i64
+  %c1 = arith.constant 1 : index
+  %cst = arith.constant 0xFF800000 : f32
+  %cst_0 = arith.constant 0.000000e+00 : f16
+  %dim = tensor.dim %arg0, %c0 : tensor<?x?x?xf32>
+  %dim_1 = tensor.dim %arg0, %c1 : tensor<?x?x?xf32>
+  %0 = tensor.empty(%dim, %dim_1) : tensor<?x?xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<?x?xi64>) -> tensor<?x?xi64>
+  %2 = tensor.empty(%dim, %dim_1) : tensor<?x?xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<?x?xf32>) -> tensor<?x?xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1, d2) -> (d0, d1, d2)>, affine_map<(d0, d1, d2) -> (d0, d1)>, affine_map<(d0, d1, d2) -> (d0, d1)>], iterator_types = ["parallel", "parallel", "reduction"]} ins(%arg0 : tensor<?x?x?xf32>) outs(%3, %1 : tensor<?x?xf32>, tensor<?x?xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_2: i64):
+    %6 = linalg.index 2 : index
+    %7 = arith.index_cast %6 : index to i64
+    %8 = arith.maximumf %in, %out : f32
+    %9 = arith.cmpf ogt, %in, %out : f32
+    %10 = arith.select %9, %7, %out_2 : i64
+    linalg.yield %8, %10 : f32, i64
+  } -> (tensor<?x?xf32>, tensor<?x?xi64>)
+  %5 = arith.sitofp %4#1 : tensor<?x?xi64> to tensor<?x?xf32>
+  return %5 : tensor<?x?xf32>
+}
+
+// -----
+
+// CHECK: llvm.func @__iree_uk_rocm_argmax_F16I32
+// CHECK: llvm.call @__iree_uk_rocm_argmax_F16I32
+func.func @argmax_3d_dyn_f16i32(%arg0: tensor<?x?x?xf16>) -> tensor<?x?xf32> {
+  %c0 = arith.constant 0 : index
+  %c0_i32 = arith.constant 0 : i32
+  %c1 = arith.constant 1 : index
+  %cst = arith.constant 0xFC00 : f16
+  %cst_0 = arith.constant 0.000000e+00 : f16
+  %dim = tensor.dim %arg0, %c0 : tensor<?x?x?xf16>
+  %dim_1 = tensor.dim %arg0, %c1 : tensor<?x?x?xf16>
+  %0 = tensor.empty(%dim, %dim_1) : tensor<?x?xi32>
+  %1 = linalg.fill ins(%c0_i32 : i32) outs(%0 : tensor<?x?xi32>) -> tensor<?x?xi32>
+  %2 = tensor.empty(%dim, %dim_1) : tensor<?x?xf16>
+  %3 = linalg.fill ins(%cst : f16) outs(%2 : tensor<?x?xf16>) -> tensor<?x?xf16>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1, d2) -> (d0, d1, d2)>, affine_map<(d0, d1, d2) -> (d0, d1)>, affine_map<(d0, d1, d2) -> (d0, d1)>], iterator_types = ["parallel", "parallel", "reduction"]} ins(%arg0 : tensor<?x?x?xf16>) outs(%3, %1 : tensor<?x?xf16>, tensor<?x?xi32>) {
+  ^bb0(%in: f16, %out: f16, %out_2: i32):
+    %6 = linalg.index 2 : index
+    %7 = arith.index_cast %6 : index to i32
+    %8 = arith.maximumf %in, %out : f16
+    %9 = arith.cmpf ogt, %in, %out : f16
+    %10 = arith.select %9, %7, %out_2 : i32
+    linalg.yield %8, %10 : f16, i32
+  } -> (tensor<?x?xf16>, tensor<?x?xi32>)
+  %5 = arith.sitofp %4#1 : tensor<?x?xi32> to tensor<?x?xf32>
+  return %5 : tensor<?x?xf32>
+}
+
+// -----
+
+// CHECK: llvm.func @__iree_uk_rocm_argmax_F16I64
+// CHECK: llvm.call @__iree_uk_rocm_argmax_F16I64
+func.func @argmax_3d_dyn_f16i64(%arg0: tensor<?x?x?xf16>) -> tensor<?x?xf32> {
+  %c0 = arith.constant 0 : index
+  %c0_i64 = arith.constant 0 : i64
+  %c1 = arith.constant 1 : index
+  %cst = arith.constant 0xFC00 : f16
+  %cst_0 = arith.constant 0.000000e+00 : f16
+  %dim = tensor.dim %arg0, %c0 : tensor<?x?x?xf16>
+  %dim_1 = tensor.dim %arg0, %c1 : tensor<?x?x?xf16>
+  %0 = tensor.empty(%dim, %dim_1) : tensor<?x?xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<?x?xi64>) -> tensor<?x?xi64>
+  %2 = tensor.empty(%dim, %dim_1) : tensor<?x?xf16>
+  %3 = linalg.fill ins(%cst : f16) outs(%2 : tensor<?x?xf16>) -> tensor<?x?xf16>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1, d2) -> (d0, d1, d2)>, affine_map<(d0, d1, d2) -> (d0, d1)>, affine_map<(d0, d1, d2) -> (d0, d1)>], iterator_types = ["parallel", "parallel", "reduction"]} ins(%arg0 : tensor<?x?x?xf16>) outs(%3, %1 : tensor<?x?xf16>, tensor<?x?xi64>) {
+  ^bb0(%in: f16, %out: f16, %out_2: i64):
+    %6 = linalg.index 2 : index
+    %7 = arith.index_cast %6 : index to i64
+    %8 = arith.maximumf %in, %out : f16
+    %9 = arith.cmpf ogt, %in, %out : f16
+    %10 = arith.select %9, %7, %out_2 : i64
+    linalg.yield %8, %10 : f16, i64
+  } -> (tensor<?x?xf16>, tensor<?x?xi64>)
+  %5 = arith.sitofp %4#1 : tensor<?x?xi64> to tensor<?x?xf32>
+  return %5 : tensor<?x?xf32>
+}
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/BUILD.bazel b/compiler/src/iree/compiler/Codegen/Common/GPU/BUILD.bazel
index 5c28076..fde7e6d 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/BUILD.bazel
@@ -53,6 +53,7 @@
         "GPUDistributeSharedMemoryCopy.cpp",
         "GPUDistributionPatterns.cpp",
         "GPUGeneralizeNamedOps.cpp",
+        "GPULowerToUKernels.cpp",
         "GPUMultiBuffering.cpp",
         "GPUPatterns.cpp",
         "GPUPipelining.cpp",
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/Common/GPU/CMakeLists.txt
index 235a09c..185ba86 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/CMakeLists.txt
@@ -52,6 +52,7 @@
     "GPUDistributeSharedMemoryCopy.cpp"
     "GPUDistributionPatterns.cpp"
     "GPUGeneralizeNamedOps.cpp"
+    "GPULowerToUKernels.cpp"
     "GPUMultiBuffering.cpp"
     "GPUPatterns.cpp"
     "GPUPipelining.cpp"
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/GPULowerToUKernels.cpp b/compiler/src/iree/compiler/Codegen/Common/GPU/GPULowerToUKernels.cpp
new file mode 100644
index 0000000..a21c26b
--- /dev/null
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/GPULowerToUKernels.cpp
@@ -0,0 +1,206 @@
+// Copyright 2023 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 "iree-dialects/Dialect/LinalgExt/IR/LinalgExtOps.h"
+#include "iree/compiler/Codegen/Common/GPU/PassDetail.h"
+#include "iree/compiler/Codegen/Common/GPU/Passes.h"
+#include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.h"
+#include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenOps.h"
+#include "iree/compiler/Codegen/Dialect/Codegen/IR/UKernelOps.h"
+#include "iree/compiler/Codegen/Utils/GPUUtils.h"
+#include "mlir/Dialect/Arith/IR/Arith.h"
+#include "mlir/Dialect/Func/IR/FuncOps.h"
+#include "mlir/Dialect/Linalg/IR/Linalg.h"
+#include "mlir/Dialect/Linalg/Utils/Utils.h"
+#include "mlir/Dialect/Tensor/IR/Tensor.h"
+#include "mlir/IR/Attributes.h"
+#include "mlir/IR/BuiltinAttributes.h"
+#include "mlir/IR/BuiltinTypes.h"
+#include "mlir/IR/MLIRContext.h"
+#include "mlir/IR/Matchers.h"
+#include "mlir/IR/TypeRange.h"
+#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
+
+namespace mlir {
+namespace iree_compiler {
+
+namespace {
+class GPULowerToUKernelsPass
+    : public GPULowerToUKernelsBase<GPULowerToUKernelsPass> {
+public:
+  void getDependentDialects(DialectRegistry &registry) const override {
+    registry.insert<IREE::Codegen::IREECodegenDialect>();
+  }
+
+  void runOnOperation() override;
+};
+} // namespace
+
+/// Holds a function name and attributes.
+struct FnNameAndDefAttrs {
+  std::string name;
+  SmallVector<NamedAttribute> defAttrs;
+};
+
+/// Returns the function name and attributes to use for a ukernel with given
+/// `ukernelName` on the target described by `targetAttr`.
+static FnNameAndDefAttrs
+getFnNameAndDefAttrs(const char *ukernelName, std::string &typeSuffixID,
+                     RewriterBase &rewriter,
+                     IREE::HAL::ExecutableTargetAttr targetAttr) {
+  FnNameAndDefAttrs result;
+  if (isROCMBackend(targetAttr)) {
+    result.name =
+        std::string("__iree_uk_rocm_") + ukernelName + "_" + typeSuffixID;
+    result.defAttrs.emplace_back(rewriter.getStringAttr("vm.import.module"),
+                                 rewriter.getStringAttr("rocm"));
+  }
+  return result;
+}
+
+/// Matches generic that represent argmax and check if
+/// we have the ukernel that matches it shape constraint, and types.
+/// If we do, then we convert into iree_codegen.ukernel.argmax operation,
+/// that is later lowered into a call to the microkernel.
+static FailureOr<IREE::Codegen::UKernelOpInterface>
+matchArgmaxDAGForUKernel(RewriterBase &rewriter, linalg::GenericOp op) {
+  auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(op);
+  const char ukernelName[] = "argmax";
+  if (!hasUkernel(targetAttr, ukernelName) ||
+      !hasUkernelSupportedGpuArch(targetAttr)) {
+    return failure();
+  }
+
+  // Currently only support argmax where parallel dims are 1.
+  // Tiling pipeline is also set to tile all parallel dims to 1, and
+  // reduction dim to be size of whole reduction problem. Which allow
+  // this constraint to be true for a lot of argmax variances.
+  // TODO: Support multi-row or grid-strided argmax ukernel.
+  SmallVector<int64_t, 4> bounds = op.getStaticLoopRanges();
+  SmallVector<unsigned> parallelDims;
+  op.getParallelDims(parallelDims);
+  int64_t parallelSize = 1;
+  for (int64_t dim : parallelDims) {
+    if (ShapedType::isDynamic(bounds[dim])) {
+      return failure();
+    }
+    parallelSize *= bounds[dim];
+  }
+  if (parallelSize != 1)
+    return failure();
+
+  // Get value/input type.
+  Value input = op.getDpsInputOperand(0)->get();
+  auto inputType = llvm::cast<ShapedType>(input.getType());
+  Type inputElemType = inputType.getElementType();
+  // Only support f16 and f32 values.
+  if (!inputElemType.isF16() && !inputElemType.isF32()) {
+    return failure();
+  }
+
+  // Get index type.
+  Value index = op.getDpsInitOperand(1)->get();
+  auto indexType = llvm::cast<ShapedType>(index.getType());
+  Type indexElemType = indexType.getElementType();
+  // Only support i32 and i64 index.
+  if (!indexElemType.isInteger(32) && !indexElemType.isInteger(64)) {
+    return failure();
+  }
+
+  std::string typeSuffixID = "";
+  if (inputElemType.isF16() && indexElemType.isInteger(32)) {
+    typeSuffixID = "F16I32";
+  } else if (inputElemType.isF16() && indexElemType.isInteger(64)) {
+    typeSuffixID = "F16I64";
+  } else if (inputElemType.isF32() && indexElemType.isInteger(32)) {
+    typeSuffixID = "F32I32";
+  } else if (inputElemType.isF32() && indexElemType.isInteger(64)) {
+    typeSuffixID = "F32I64";
+  } else {
+    return rewriter.notifyMatchFailure(
+        op, "unsupported combination of element types");
+  }
+
+  Location loc = op.getLoc();
+  // Currently only support 1D reduction, where reduc is on fastest dim.
+  // Tiling argmax ukernel is also set to enforce this structure.
+  const int kReductionDim = op.getNumLoops() - 1;
+  Value reductionDimSize =
+      rewriter.create<tensor::DimOp>(loc, input, kReductionDim);
+  auto fn =
+      getFnNameAndDefAttrs(ukernelName, typeSuffixID, rewriter, targetAttr);
+  auto genericMicroKernelOp = rewriter.create<IREE::Codegen::UKernelGenericOp>(
+      loc, indexType, fn.name, ValueRange{input}, index,
+      ValueRange{reductionDimSize},
+      /*fn_def_attrs=*/rewriter.getDictionaryAttr(fn.defAttrs),
+      /*strided_outer_dims=*/rewriter.getIndexAttr(0));
+  return cast<IREE::Codegen::UKernelOpInterface>(
+      genericMicroKernelOp.getOperation());
+}
+
+namespace {
+
+using TargetPredicate = std::function<bool(IREE::HAL::ExecutableTargetAttr)>;
+
+struct LowerArgmaxToUKernelPattern : OpRewritePattern<linalg::GenericOp> {
+  LowerArgmaxToUKernelPattern(MLIRContext *context,
+                              TargetPredicate targetPredicate)
+      : OpRewritePattern<linalg::GenericOp>(context),
+        targetPredicate(targetPredicate) {}
+
+  LogicalResult matchAndRewrite(linalg::GenericOp op,
+                                PatternRewriter &rewriter) const override {
+    if (targetPredicate &&
+        !targetPredicate(IREE::HAL::ExecutableTargetAttr::lookup(op))) {
+      return failure();
+    }
+    if (failed(isArgmaxOp(op))) {
+      return failure();
+    }
+    FailureOr<IREE::Codegen::UKernelOpInterface> ukernelOp =
+        matchArgmaxDAGForUKernel(rewriter, op);
+    if (failed(ukernelOp)) {
+      return rewriter.notifyMatchFailure(
+          op, "failed to find microkernel op to replace with");
+    }
+    rewriter.replaceAllUsesWith(op.getResults()[1],
+                                ukernelOp.value()->getResults());
+    return success();
+  }
+
+  TargetPredicate targetPredicate;
+};
+
+} // namespace
+
+void GPULowerToUKernelsPass::runOnOperation() {
+  MLIRContext *context = &getContext();
+  RewritePatternSet patterns(context);
+  // Enabling a lowering of an op to a microkernel is a trade-off between the
+  // potential performance advantage of a microkernel over pure code generation
+  // for that op, and the potential benefits of fusions. Indeed, once an op
+  // lowered into a microkernel, it will never be fused at any MLIR level.
+  // Since microkernels are linked as bitcode, they will still undergo LTO-like
+  // optimization in their calling contexts, but we shouldn't expect this to
+  // achieve similar results as fusing structured ops.
+
+  // These patterns are unconditionally enabled, because we have strong evidence
+  // that it is difficult for codegen to consistently approach microkernels
+  // performance, and that consideration overrides the benefit of fusions for
+  // these ops.
+  patterns.insert<LowerArgmaxToUKernelPattern>(context, isROCMBackend);
+  if (failed(
+          applyPatternsAndFoldGreedily(getOperation(), std::move(patterns)))) {
+    return signalPassFailure();
+  }
+}
+
+std::unique_ptr<OperationPass<>> createGPULowerToUKernelsPass() {
+  return std::make_unique<GPULowerToUKernelsPass>();
+}
+
+} // namespace iree_compiler
+} // namespace mlir
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.h b/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.h
index 07ca997..baafb2b 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.h
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.h
@@ -131,6 +131,10 @@
 // This pass generalizes named Linalg ops that are better off as generics.
 std::unique_ptr<OperationPass<func::FuncOp>> createGPUGeneralizeNamedOpsPass();
 
+/// Pass to lower a sequence of operations to a iree_codegen.ukernel.*
+/// operation.
+std::unique_ptr<OperationPass<>> createGPULowerToUKernelsPass();
+
 /// Register Common GPU passes.
 void registerCodegenCommonGPUPasses();
 
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.td b/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.td
index 5b88f4e..879a8b2 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.td
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.td
@@ -37,6 +37,14 @@
   let constructor = "mlir::iree_compiler::createGPUGeneralizeNamedOpsPass()";
 }
 
+def GPULowerToUKernels :
+    Pass<"iree-codegen-gpu-lower-to-ukernels", ""> {
+  let summary =
+      "Separate out parts of the IR that lower to a micro-kernel";
+  let constructor =
+      "mlir::iree_compiler::createGPULowerToUKernelsPass()";
+}
+
 def GPUMultiBuffering :
     Pass<"iree-codegen-gpu-multi-buffering", "func::FuncOp"> {
   let summary = "Pass to do multi buffering.";
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 73ddd5c..78db631 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel
@@ -22,6 +22,7 @@
             "gpu_distribute.mlir",
             "gpu_distribute_shared_memory.mlir",
             "gpu_generalize_named_ops.mlir",
+            "gpu_lower_to_ukernels.mlir",
             "gpu_pipeline.mlir",
             "gpu_tensor_alloc.mlir",
             "gpu_tensor_tile.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 b748cca..011b72c 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt
@@ -18,6 +18,7 @@
     "gpu_distribute.mlir"
     "gpu_distribute_shared_memory.mlir"
     "gpu_generalize_named_ops.mlir"
+    "gpu_lower_to_ukernels.mlir"
     "gpu_pipeline.mlir"
     "gpu_tensor_alloc.mlir"
     "gpu_tensor_tile.mlir"
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_lower_to_ukernels.mlir b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_lower_to_ukernels.mlir
new file mode 100644
index 0000000..2118681
--- /dev/null
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_lower_to_ukernels.mlir
@@ -0,0 +1,282 @@
+// RUN: iree-opt --pass-pipeline="builtin.module(func.func(iree-codegen-gpu-lower-to-ukernels,cse,canonicalize))" %s | FileCheck %s
+
+func.func @argmax_2d_f32i64(%arg0 : tensor<1x?xf32>) -> tensor<1xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "all"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %0 = tensor.empty() : tensor<1xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<1xi64>) -> tensor<1xi64>
+  %2 = tensor.empty() : tensor<1xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<1xf32>) -> tensor<1xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf32>) outs(%3, %1 : tensor<1xf32>, tensor<1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<1xf32>, tensor<1xi64>)
+  return %4#1 : tensor<1xi64>
+}
+
+//      CHECK: func @argmax_2d_f32i64(
+// CHECK-SAME:     %[[ARG0:[a-zA-Z0-9]+]]: tensor<1x?xf32>
+//  CHECK-DAG:   %[[C1_index:.+]] = arith.constant 1 : index
+//  CHECK-DAG:   %[[C0_i64:.+]] = arith.constant 0
+//  CHECK-DAG:   %[[FILL:.+]] = linalg.fill ins(%[[C0_i64]]
+//      CHECK:   %[[MICRO_KERNEL:.+]] = iree_codegen.ukernel.generic "__iree_uk_rocm_argmax_F32I64"
+// CHECK-SAME:       ins(%[[ARG0]] :
+// CHECK-SAME:       outs(%[[FILL]] :
+//      CHECK:   return %[[MICRO_KERNEL]]
+
+// -----
+
+func.func @argmax_4d_unit_parallel_f32i64(%arg0 : tensor<1x1x1x?xf32>) -> tensor<1x1x1xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "all"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %0 = tensor.empty() : tensor<1x1x1xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<1x1x1xi64>) -> tensor<1x1x1xi64>
+  %2 = tensor.empty() : tensor<1x1x1xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<1x1x1xf32>) -> tensor<1x1x1xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)>, affine_map<(d0, d1, d2, d3) -> (d0, d1, d2)>, affine_map<(d0, d1, d2, d3) -> (d0, d1, d2)>], iterator_types = ["parallel", "parallel", "parallel", "reduction"]} ins(%arg0 : tensor<1x1x1x?xf32>) outs(%3, %1 : tensor<1x1x1xf32>, tensor<1x1x1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<1x1x1xf32>, tensor<1x1x1xi64>)
+  return %4#1 : tensor<1x1x1xi64>
+}
+
+//      CHECK: func @argmax_4d_unit_parallel_f32i64(
+//      CHECK: iree_codegen.ukernel.generic
+//      CHECK-NOT: linalg.generic
+
+// -----
+
+func.func @argmax_2d_non_unit_parallel_f32i64(%arg0 : tensor<4x?xf32>) -> tensor<4xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "all"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %0 = tensor.empty() : tensor<4xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<4xi64>) -> tensor<4xi64>
+  %2 = tensor.empty() : tensor<4xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<4xf32>) -> tensor<4xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<4x?xf32>) outs(%3, %1 : tensor<4xf32>, tensor<4xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<4xf32>, tensor<4xi64>)
+  return %4#1 : tensor<4xi64>
+}
+
+//      CHECK: func @argmax_2d_non_unit_parallel_f32i64(
+//      CHECK-NOT: iree_codegen.ukernel.generic
+//      CHECK: linalg.generic
+
+// -----
+
+func.func @argmax_2d_dyn_parallel_f32i64(%arg0 : tensor<?x?xf32>) -> tensor<?xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "all"}>
+} {
+  %c0 = arith.constant 0 : index
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %dim = tensor.dim %arg0, %c0 : tensor<?x?xf32>
+  %0 = tensor.empty(%dim) : tensor<?xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<?xi64>) -> tensor<?xi64>
+  %2 = tensor.empty(%dim) : tensor<?xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<?xf32>) -> tensor<?xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<?x?xf32>) outs(%3, %1 : tensor<?xf32>, tensor<?xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<?xf32>, tensor<?xi64>)
+  return %4#1 : tensor<?xi64>
+}
+
+//      CHECK: func @argmax_2d_dyn_parallel_f32i64(
+//      CHECK-NOT: iree_codegen.ukernel.generic
+//      CHECK: linalg.generic
+
+// -----
+
+func.func @argmax_none_ukernel_enabled(%arg0 : tensor<1x?xf32>) -> tensor<1xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "none"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %0 = tensor.empty() : tensor<1xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<1xi64>) -> tensor<1xi64>
+  %2 = tensor.empty() : tensor<1xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<1xf32>) -> tensor<1xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf32>) outs(%3, %1 : tensor<1xf32>, tensor<1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<1xf32>, tensor<1xi64>)
+  return %4#1 : tensor<1xi64>
+}
+
+//      CHECK: func @argmax_none_ukernel_enabled(
+//      CHECK-NOT: iree_codegen.ukernel.generic
+//      CHECK: linalg.generic
+
+// -----
+
+func.func @argmax_only_argmax_ukernel_enabled(%arg0 : tensor<1x?xf32>) -> tensor<1xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx90a", ukernels = "argmax"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %0 = tensor.empty() : tensor<1xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<1xi64>) -> tensor<1xi64>
+  %2 = tensor.empty() : tensor<1xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<1xf32>) -> tensor<1xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf32>) outs(%3, %1 : tensor<1xf32>, tensor<1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<1xf32>, tensor<1xi64>)
+  return %4#1 : tensor<1xi64>
+}
+
+//      CHECK: func @argmax_only_argmax_ukernel_enabled(
+//      CHECK: iree_codegen.ukernel.generic
+//      CHECK-NOT: linalg.generic
+
+// -----
+
+func.func @argmax_only_foo_argmax_bar_ukernel_enabled(%arg0 : tensor<1x?xf32>) -> tensor<1xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "foo,argmax,bar"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %0 = tensor.empty() : tensor<1xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<1xi64>) -> tensor<1xi64>
+  %2 = tensor.empty() : tensor<1xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<1xf32>) -> tensor<1xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf32>) outs(%3, %1 : tensor<1xf32>, tensor<1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<1xf32>, tensor<1xi64>)
+  return %4#1 : tensor<1xi64>
+}
+
+//      CHECK: func @argmax_only_foo_argmax_bar_ukernel_enabled(
+//      CHECK: iree_codegen.ukernel.generic
+//      CHECK-NOT: linalg.generic
+
+// -----
+
+func.func @argmax_only_foo_ukernel_enabled(%arg0 : tensor<1x?xf32>) -> tensor<1xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "foo"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %0 = tensor.empty() : tensor<1xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<1xi64>) -> tensor<1xi64>
+  %2 = tensor.empty() : tensor<1xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<1xf32>) -> tensor<1xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf32>) outs(%3, %1 : tensor<1xf32>, tensor<1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<1xf32>, tensor<1xi64>)
+  return %4#1 : tensor<1xi64>
+}
+
+//      CHECK: func @argmax_only_foo_ukernel_enabled(
+//      CHECK-NOT: iree_codegen.ukernel.generic
+//      CHECK: linalg.generic
+
+// -----
+
+// Currently we do only handle -Inf case as initial values.
+func.func @argmax_2d_f32i64_not_neg_inf_init(%arg0 : tensor<1x?xf32>) -> tensor<1xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "all"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0.0 : f32
+  %0 = tensor.empty() : tensor<1xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<1xi64>) -> tensor<1xi64>
+  %2 = tensor.empty() : tensor<1xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<1xf32>) -> tensor<1xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf32>) outs(%3, %1 : tensor<1xf32>, tensor<1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<1xf32>, tensor<1xi64>)
+  return %4#1 : tensor<1xi64>
+}
+
+//      CHECK: func @argmax_2d_f32i64_not_neg_inf_init(
+//      CHECK-NOT: iree_codegen.ukernel.generic
+//      CHECK: linalg.generic
+
+// -----
+
+// TODO: No technical reason this architecture is not supported.
+//       Currently just picking out popular chips to support,
+//       to minimize compile time and space.
+
+func.func @argmax_ukernel_unsupported_arch(%arg0 : tensor<1x?xf32>) -> tensor<1xi64> attributes {
+  hal.executable.target = #hal.executable.target<"rocm", "rocm-hsaco-fb", {target_arch = "gfx800", ukernels = "all"}>
+} {
+  %c0_i64 = arith.constant 0 : i64
+  %cst = arith.constant 0xFF800000 : f32
+  %0 = tensor.empty() : tensor<1xi64>
+  %1 = linalg.fill ins(%c0_i64 : i64) outs(%0 : tensor<1xi64>) -> tensor<1xi64>
+  %2 = tensor.empty() : tensor<1xf32>
+  %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor<1xf32>) -> tensor<1xf32>
+  %4:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%arg0 : tensor<1x?xf32>) outs(%3, %1 : tensor<1xf32>, tensor<1xi64>) {
+  ^bb0(%in: f32, %out: f32, %out_0: i64):
+    %5 = linalg.index 1 : index
+    %6 = arith.index_cast %5 : index to i64
+    %7 = arith.maximumf %in, %out : f32
+    %8 = arith.cmpf ogt, %in, %out : f32
+    %9 = arith.select %8, %6, %out_0 : i64
+    linalg.yield %7, %9 : f32, i64
+  } -> (tensor<1xf32>, tensor<1xi64>)
+  return %4#1 : tensor<1xi64>
+}
+
+//      CHECK: func @argmax_ukernel_unsupported_arch(
+//      CHECK-NOT: iree_codegen.ukernel.generic
+//      CHECK: linalg.generic
diff --git a/compiler/src/iree/compiler/Codegen/LLVMGPU/KernelConfig.cpp b/compiler/src/iree/compiler/Codegen/LLVMGPU/KernelConfig.cpp
index 520f12e..d36220f 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMGPU/KernelConfig.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMGPU/KernelConfig.cpp
@@ -1062,6 +1062,83 @@
       workgroupSize);
 }
 
+/// Set the configuration for argmax that can be mapped to argmax uKernel.
+/// Distribute all parallel dim across different workgroups, and only use single
+/// subgroup per workgroup.
+static LogicalResult setArgmaxUkernelConfig(func::FuncOp entryPoint,
+                                            linalg::GenericOp op,
+                                            const TargetInfo &targetInfo) {
+
+  // Checks if UKernels are enabled.
+  if (auto variantOp =
+          entryPoint->getParentOfType<IREE::HAL::ExecutableVariantOp>()) {
+    auto target = variantOp.getTarget();
+    const char ukernelName[] = "argmax";
+    if (!hasUkernel(target, ukernelName) ||
+        !hasUkernelSupportedGpuArch(target)) {
+      return failure();
+    }
+  }
+
+  if (!targetInfo.hasWarpShuffle)
+    return failure();
+
+  if (failed(isArgmaxOp(op)))
+    return failure();
+  SmallVector<unsigned> parallelDims;
+  SmallVector<unsigned> reductionDims;
+  op.getParallelDims(parallelDims);
+  op.getReductionDims(reductionDims);
+
+  // Currently Argmax UKernel only support 1 reduction dim.
+  if (reductionDims.size() != 1)
+    return failure();
+
+  // Make sure reduction dimensions are static and innermost ones.
+  SmallVector<int64_t, 4> bounds = op.getStaticLoopRanges();
+  int64_t numParallelDims = op.getNumParallelLoops();
+  int64_t numDynamicReductionDims = 0;
+  for (unsigned dim : reductionDims) {
+    if (ShapedType::isDynamic(bounds[dim])) {
+      numDynamicReductionDims++;
+    }
+    if (dim < numParallelDims) {
+      return failure();
+    }
+  }
+
+  // Distribution of multi-dim masked writes currently aren't fully supported.
+  if (numDynamicReductionDims > 1) {
+    return failure();
+  }
+
+  // Tile all the parallel dimension to 1.
+  SmallVector<unsigned> partitionedLoops =
+      cast<PartitionableLoopsInterface>(op.getOperation())
+          .getPartitionableLoops(kNumMaxParallelDims);
+  size_t numLoops = partitionedLoops.empty() ? 0 : partitionedLoops.back() + 1;
+  SmallVector<int64_t> workgroupTileSizes(numLoops, 1);
+
+  // Currently Argmax Ukernel let's every thread reduce reductionDim/WarpSize
+  // number of elements, and then it does a single step butterfly warp reduce.
+  // Hence it expects workgroupSize to be warpSize(subgroupSize), and
+  // reductionTileSize to be size of the reduction dim.
+  SmallVector<int64_t> reductionTileSizes(op.getNumLoops(), 0);
+  int64_t preferredSubgroupSize = targetInfo.supportedSubgroupSizes.front();
+  reductionTileSizes[reductionDims[0]] = preferredSubgroupSize;
+  TileSizesListType tileSizes;
+  tileSizes.emplace_back(std::move(workgroupTileSizes)); // Workgroup level
+  tileSizes.emplace_back(std::move(reductionTileSizes)); // Reduction level
+  std::array<int64_t, 3> workgroupSize = {preferredSubgroupSize, 1, 1};
+  if (failed(setOpConfigAndEntryPointFnTranslation(
+          entryPoint, op, tileSizes,
+          IREE::Codegen::DispatchLoweringPassPipeline::LLVMGPUDefault,
+          workgroupSize))) {
+    return failure();
+  }
+  return success();
+}
+
 /// Make UKernels take the LLVMGPUDefault lowering pipeline.
 static LogicalResult
 setUKernelConfig(func::FuncOp entryPoint,
@@ -1256,6 +1333,9 @@
     auto genericOp = dyn_cast<linalg::GenericOp>(computeOp);
     if (genericOp && succeeded(setTransposeConfig(entryPointFn, genericOp))) {
       return success();
+    } else if (genericOp && succeeded(setArgmaxUkernelConfig(
+                                entryPointFn, genericOp, targetInfo))) {
+      return success();
     }
   }
 
diff --git a/compiler/src/iree/compiler/Codegen/LLVMGPU/LLVMGPULowerExecutableTarget.cpp b/compiler/src/iree/compiler/Codegen/LLVMGPU/LLVMGPULowerExecutableTarget.cpp
index cef1407..350a8fe 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMGPU/LLVMGPULowerExecutableTarget.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMGPU/LLVMGPULowerExecutableTarget.cpp
@@ -75,10 +75,11 @@
     return signalPassFailure();
   }
 
+  bool enableMicrokernels = hasUkernel(variantOp.getTarget());
   OpPassManager pipeline(IREE::HAL::ExecutableVariantOp::getOperationName());
   switch (translationInfo.value().getDispatchLoweringPassPipeline()) {
   case IREE::Codegen::DispatchLoweringPassPipeline::LLVMGPUDefault:
-    addGPUDefaultPassPipeline(pipeline);
+    addGPUDefaultPassPipeline(pipeline, enableMicrokernels);
     break;
   case IREE::Codegen::DispatchLoweringPassPipeline::LLVMGPUDistribute:
     addGPUSimpleDistributePassPipeline(pipeline);
diff --git a/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.cpp b/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.cpp
index 4727977..69b5858 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.cpp
@@ -484,9 +484,16 @@
       createRemoveSingleIterationLoopPass());
 }
 
-void addGPUDefaultPassPipeline(OpPassManager &pm) {
-  tileAndBufferize(pm);
+void addGPUDefaultPassPipeline(OpPassManager &pm, bool enableMicrokernels) {
+  tileAndDistributeToWorkgroup(pm, /*useWARForCooperativeMatrixCodegen=*/true);
   auto &nestedModulePM = pm.nest<ModuleOp>();
+  if (enableMicrokernels) {
+    nestedModulePM.addPass(createGPULowerToUKernelsPass());
+  }
+  nestedModulePM.addNestedPass<func::FuncOp>(createCanonicalizerPass());
+  nestedModulePM.addNestedPass<func::FuncOp>(createCSEPass());
+
+  addBufferizePasses(nestedModulePM);
   nestedModulePM.addNestedPass<func::FuncOp>(
       createRemoveSingleIterationLoopPass());
 }
diff --git a/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.h b/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.h
index 9860197..0a97fb6 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.h
+++ b/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.h
@@ -53,7 +53,7 @@
 void addGPUWarpReductionPassPipeline(OpPassManager &pm);
 
 /// Default pass pipeline on GPU, currently used only for the ukernel path.
-void addGPUDefaultPassPipeline(OpPassManager &pm);
+void addGPUDefaultPassPipeline(OpPassManager &pm, bool enableMicrokernels);
 
 /// Populates passes needed to preprocess and select the translation strategy.
 void buildLLVMGPUCodegenConfigurationPassPipeline(OpPassManager &pm);
diff --git a/compiler/src/iree/compiler/Codegen/LLVMGPU/test/BUILD.bazel b/compiler/src/iree/compiler/Codegen/LLVMGPU/test/BUILD.bazel
index be733ae..bd80615 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMGPU/test/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/LLVMGPU/test/BUILD.bazel
@@ -59,6 +59,7 @@
             "transform_gpu_pipelining.mlir",
             "transform_vector_to_mma.mlir",
             "transpose_pipeline_test.mlir",
+            "ukernel_pipeline_transform.mlir",
             "vector_lowering.mlir",
             "vector_to_gpu.mlir",
             "workgroup_specialization_pipeline_test.mlir",
diff --git a/compiler/src/iree/compiler/Codegen/LLVMGPU/test/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/LLVMGPU/test/CMakeLists.txt
index caa6dcb..d20d23d 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMGPU/test/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/LLVMGPU/test/CMakeLists.txt
@@ -55,6 +55,7 @@
     "transform_gpu_pipelining.mlir"
     "transform_vector_to_mma.mlir"
     "transpose_pipeline_test.mlir"
+    "ukernel_pipeline_transform.mlir"
     "vector_lowering.mlir"
     "vector_to_gpu.mlir"
     "workgroup_specialization_pipeline_test.mlir"
diff --git a/compiler/src/iree/compiler/Codegen/LLVMGPU/test/ukernel_pipeline_transform.mlir b/compiler/src/iree/compiler/Codegen/LLVMGPU/test/ukernel_pipeline_transform.mlir
new file mode 100644
index 0000000..809022f
--- /dev/null
+++ b/compiler/src/iree/compiler/Codegen/LLVMGPU/test/ukernel_pipeline_transform.mlir
@@ -0,0 +1,215 @@
+// RUN: iree-opt --split-input-file --pass-pipeline="builtin.module(hal.executable(hal.executable.variant(iree-llvmgpu-select-lowering-strategy, iree-llvmgpu-lower-executable-target)))" %s | FileCheck %s
+
+hal.executable @argmax_1d_f16i64 {
+hal.executable.variant public @rocm_hsaco_fb target(<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "argmax"}>) {
+  hal.executable.export public @argmax_1d_f16i64 ordinal(0) layout(#hal.pipeline.layout<push_constants = 2, sets = [<0, bindings = [<0, storage_buffer, ReadOnly>, <1, storage_buffer>]>]>) {
+  ^bb0(%arg0: !hal.device, %arg1: index):
+    %x, %y, %z = flow.dispatch.workgroup_count_from_slice %arg1
+    hal.return %x, %y, %z : index, index, index
+  }
+  builtin.module {
+    func.func @argmax_1d_f16i64() {
+      %c32_i64 = arith.constant 32 : i64
+      %cst = arith.constant 0xFC00 : f16
+      %c0_i64 = arith.constant 0 : i64
+      %c0 = arith.constant 0 : index
+      %0 = hal.interface.constant.load[0] : i32
+      %1 = hal.interface.constant.load[1] : i32
+      %2 = arith.extui %0 : i32 to i64
+      %3 = arith.extui %1 : i32 to i64
+      %4 = arith.shli %3, %c32_i64 : i64
+      %5 = arith.ori %2, %4 : i64
+      %6 = arith.index_castui %5 : i64 to index
+      %7 = hal.interface.binding.subspan set(0) binding(1) type(storage_buffer) alignment(64) offset(%c0) : !flow.dispatch.tensor<writeonly:tensor<i64>>
+      %8 = flow.dispatch.workload.ordinal %6, 0 : index
+      %9 = hal.interface.binding.subspan set(0) binding(0) type(storage_buffer) alignment(64) offset(%c0) flags(ReadOnly) : !flow.dispatch.tensor<readonly:tensor<?xf16>>{%8}
+      %10 = flow.dispatch.tensor.load %9, offsets = [0], sizes = [%8], strides = [1] : !flow.dispatch.tensor<readonly:tensor<?xf16>>{%8} -> tensor<?xf16>
+      %11 = tensor.empty() : tensor<i64>
+      %12 = tensor.empty() : tensor<f16>
+      %13 = linalg.fill ins(%c0_i64 : i64) outs(%11 : tensor<i64>) -> tensor<i64>
+      %14 = linalg.fill ins(%cst : f16) outs(%12 : tensor<f16>) -> tensor<f16>
+      %15:2 = linalg.generic {indexing_maps = [affine_map<(d0) -> (d0)>, affine_map<(d0) -> ()>, affine_map<(d0) -> ()>], iterator_types = ["reduction"]} ins(%10 : tensor<?xf16>) outs(%14, %13 : tensor<f16>, tensor<i64>) {
+      ^bb0(%in: f16, %out: f16, %out_0: i64):
+        %16 = linalg.index 0 : index
+        %17 = arith.index_cast %16 : index to i64
+        %18 = arith.maximumf %in, %out : f16
+        %19 = arith.cmpf ogt, %in, %out : f16
+        %20 = arith.select %19, %17, %out_0 : i64
+        linalg.yield %18, %20 : f16, i64
+      } -> (tensor<f16>, tensor<i64>)
+      flow.dispatch.tensor.store %15#1, %7, offsets = [], sizes = [], strides = [] : tensor<i64> -> !flow.dispatch.tensor<writeonly:tensor<i64>>
+      return
+    }
+  }
+}
+}
+
+// CHECK:       #[[$TRANSLATION:.+]] = #iree_codegen.translation_info<LLVMGPUDefault>
+// CHECK-LABEL: hal.executable.export public @argmax_1d_f16i64
+// CHECK-SAME:    translation_info = #[[$TRANSLATION]]
+// CHECK-SAME:    workgroup_size = [32 : index, 1 : index, 1 : index]
+//CHECK-LABEL:      func.func @argmax_1d_f16i64
+//      CHECK:        iree_codegen.ukernel.generic  "__iree_uk_rocm_argmax_F16I64"
+
+// -----
+
+hal.executable @argmax_2d_f32i64 {
+hal.executable.variant public @rocm_hsaco_fb target(<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "argmax"}>) {
+  hal.executable.export public @argmax_2d_f32i64 ordinal(0) layout(#hal.pipeline.layout<push_constants = 2, sets = [<0, bindings = [<0, storage_buffer, ReadOnly>, <1, storage_buffer>]>]>) {
+  ^bb0(%arg0: !hal.device, %arg1: index):
+    %x, %y, %z = flow.dispatch.workgroup_count_from_slice %arg1
+    hal.return %x, %y, %z : index, index, index
+  }
+  builtin.module {
+    func.func @argmax_2d_f32i64() {
+      %c32_i64 = arith.constant 32 : i64
+      %cst = arith.constant 0xFF800000 : f32
+      %c0_i64 = arith.constant 0 : i64
+      %c0 = arith.constant 0 : index
+      %0 = hal.interface.constant.load[0] : i32
+      %1 = hal.interface.constant.load[1] : i32
+      %2 = arith.extui %0 : i32 to i64
+      %3 = arith.extui %1 : i32 to i64
+      %4 = arith.shli %3, %c32_i64 : i64
+      %5 = arith.ori %2, %4 : i64
+      %6 = arith.index_castui %5 : i64 to index
+      %7 = hal.interface.binding.subspan set(0) binding(1) type(storage_buffer) alignment(64) offset(%c0) : !flow.dispatch.tensor<writeonly:tensor<16xi64>>
+      %8 = flow.dispatch.workload.ordinal %6, 0 : index
+      %9 = hal.interface.binding.subspan set(0) binding(0) type(storage_buffer) alignment(64) offset(%c0) flags(ReadOnly) : !flow.dispatch.tensor<readonly:tensor<16x?xf32>>{%8}
+      %10 = flow.dispatch.tensor.load %9, offsets = [0, 0], sizes = [16, %8], strides = [1, 1] : !flow.dispatch.tensor<readonly:tensor<16x?xf32>>{%8} -> tensor<16x?xf32>
+      %11 = tensor.empty() : tensor<16xi64>
+      %12 = tensor.empty() : tensor<16xf32>
+      %13 = linalg.fill ins(%c0_i64 : i64) outs(%11 : tensor<16xi64>) -> tensor<16xi64>
+      %14 = linalg.fill ins(%cst : f32) outs(%12 : tensor<16xf32>) -> tensor<16xf32>
+      %15:2 = linalg.generic {indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, affine_map<(d0, d1) -> (d0)>, affine_map<(d0, d1) -> (d0)>], iterator_types = ["parallel", "reduction"]} ins(%10 : tensor<16x?xf32>) outs(%14, %13 : tensor<16xf32>, tensor<16xi64>) {
+      ^bb0(%in: f32, %out: f32, %out_0: i64):
+        %16 = linalg.index 1 : index
+        %17 = arith.index_cast %16 : index to i64
+        %18 = arith.maximumf %in, %out : f32
+        %19 = arith.cmpf ogt, %in, %out : f32
+        %20 = arith.select %19, %17, %out_0 : i64
+        linalg.yield %18, %20 : f32, i64
+      } -> (tensor<16xf32>, tensor<16xi64>)
+      flow.dispatch.tensor.store %15#1, %7, offsets = [0], sizes = [16], strides = [1] : tensor<16xi64> -> !flow.dispatch.tensor<writeonly:tensor<16xi64>>
+      return
+    }
+  }
+}
+}
+
+// CHECK:       #[[$TRANSLATION:.+]] = #iree_codegen.translation_info<LLVMGPUDefault>
+// CHECK-LABEL: hal.executable.export public @argmax_2d_f32i64
+// CHECK-SAME:    translation_info = #[[$TRANSLATION]]
+// CHECK-SAME:    workgroup_size = [32 : index, 1 : index, 1 : index]
+//CHECK-LABEL:      func.func @argmax_2d_f32i64
+//  CHECK-DAG:        %[[SUBVIEW:.*]] = memref.subview{{.*}} memref<16x?xf32
+// CHECK-SAME:        to memref<1x?xf32
+//      CHECK:        iree_codegen.ukernel.generic  "__iree_uk_rocm_argmax_F32I64" ins(%[[SUBVIEW]]
+
+// -----
+
+// When the ukernel attribute is not set, we do not go through ukernel pipeline.
+hal.executable @no_ukernel_argmax_1d_f16i64 {
+hal.executable.variant public @rocm_hsaco_fb target(<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100"}>) {
+  hal.executable.export public @no_ukernel_argmax_1d_f16i64 ordinal(0) layout(#hal.pipeline.layout<push_constants = 2, sets = [<0, bindings = [<0, storage_buffer, ReadOnly>, <1, storage_buffer>]>]>) {
+  ^bb0(%arg0: !hal.device, %arg1: index):
+    %x, %y, %z = flow.dispatch.workgroup_count_from_slice %arg1
+    hal.return %x, %y, %z : index, index, index
+  }
+  builtin.module {
+    func.func @no_ukernel_argmax_1d_f16i64() {
+      %c32_i64 = arith.constant 32 : i64
+      %cst = arith.constant 0xFC00 : f16
+      %c0_i64 = arith.constant 0 : i64
+      %c0 = arith.constant 0 : index
+      %0 = hal.interface.constant.load[0] : i32
+      %1 = hal.interface.constant.load[1] : i32
+      %2 = arith.extui %0 : i32 to i64
+      %3 = arith.extui %1 : i32 to i64
+      %4 = arith.shli %3, %c32_i64 : i64
+      %5 = arith.ori %2, %4 : i64
+      %6 = arith.index_castui %5 : i64 to index
+      %7 = hal.interface.binding.subspan set(0) binding(1) type(storage_buffer) alignment(64) offset(%c0) : !flow.dispatch.tensor<writeonly:tensor<i64>>
+      %8 = flow.dispatch.workload.ordinal %6, 0 : index
+      %9 = hal.interface.binding.subspan set(0) binding(0) type(storage_buffer) alignment(64) offset(%c0) flags(ReadOnly) : !flow.dispatch.tensor<readonly:tensor<?xf16>>{%8}
+      %10 = flow.dispatch.tensor.load %9, offsets = [0], sizes = [%8], strides = [1] : !flow.dispatch.tensor<readonly:tensor<?xf16>>{%8} -> tensor<?xf16>
+      %11 = tensor.empty() : tensor<i64>
+      %12 = tensor.empty() : tensor<f16>
+      %13 = linalg.fill ins(%c0_i64 : i64) outs(%11 : tensor<i64>) -> tensor<i64>
+      %14 = linalg.fill ins(%cst : f16) outs(%12 : tensor<f16>) -> tensor<f16>
+      %15:2 = linalg.generic {indexing_maps = [affine_map<(d0) -> (d0)>, affine_map<(d0) -> ()>, affine_map<(d0) -> ()>], iterator_types = ["reduction"]} ins(%10 : tensor<?xf16>) outs(%14, %13 : tensor<f16>, tensor<i64>) {
+      ^bb0(%in: f16, %out: f16, %out_0: i64):
+        %16 = linalg.index 0 : index
+        %17 = arith.index_cast %16 : index to i64
+        %18 = arith.maximumf %in, %out : f16
+        %19 = arith.cmpf ogt, %in, %out : f16
+        %20 = arith.select %19, %17, %out_0 : i64
+        linalg.yield %18, %20 : f16, i64
+      } -> (tensor<f16>, tensor<i64>)
+      flow.dispatch.tensor.store %15#1, %7, offsets = [], sizes = [], strides = [] : tensor<i64> -> !flow.dispatch.tensor<writeonly:tensor<i64>>
+      return
+    }
+  }
+}
+}
+
+// CHECK:       #[[$TRANSLATION:.+]] = #iree_codegen.translation_info<LLVMGPUDistribute>
+// CHECK-LABEL: hal.executable.export public @no_ukernel_argmax_1d_f16i64
+// CHECK-SAME:    translation_info = #[[$TRANSLATION]]
+// CHECK-SAME:    workgroup_size = [1 : index, 1 : index, 1 : index]
+//CHECK-LABEL:      func.func @no_ukernel_argmax_1d_f16i64
+//      CHECK-NOT:        iree_codegen.ukernel.generic
+
+// -----
+
+// Currently we do only handle -Inf case as initial values.
+hal.executable @not_neg_inf_init_argmax_1d {
+hal.executable.variant public @rocm_hsaco_fb target(<"rocm", "rocm-hsaco-fb", {target_arch = "gfx1100", ukernels = "argmax"}>) {
+  hal.executable.export public @not_neg_inf_init_argmax_1d ordinal(0) layout(#hal.pipeline.layout<push_constants = 2, sets = [<0, bindings = [<0, storage_buffer, ReadOnly>, <1, storage_buffer>]>]>) {
+  ^bb0(%arg0: !hal.device, %arg1: index):
+    %x, %y, %z = flow.dispatch.workgroup_count_from_slice %arg1
+    hal.return %x, %y, %z : index, index, index
+  }
+  builtin.module {
+    func.func @not_neg_inf_init_argmax_1d() {
+      %c32_i64 = arith.constant 32 : i64
+      %cst = arith.constant 0.0 : f16
+      %c0_i64 = arith.constant 0 : i64
+      %c0 = arith.constant 0 : index
+      %0 = hal.interface.constant.load[0] : i32
+      %1 = hal.interface.constant.load[1] : i32
+      %2 = arith.extui %0 : i32 to i64
+      %3 = arith.extui %1 : i32 to i64
+      %4 = arith.shli %3, %c32_i64 : i64
+      %5 = arith.ori %2, %4 : i64
+      %6 = arith.index_castui %5 : i64 to index
+      %7 = hal.interface.binding.subspan set(0) binding(1) type(storage_buffer) alignment(64) offset(%c0) : !flow.dispatch.tensor<writeonly:tensor<i64>>
+      %8 = flow.dispatch.workload.ordinal %6, 0 : index
+      %9 = hal.interface.binding.subspan set(0) binding(0) type(storage_buffer) alignment(64) offset(%c0) flags(ReadOnly) : !flow.dispatch.tensor<readonly:tensor<?xf16>>{%8}
+      %10 = flow.dispatch.tensor.load %9, offsets = [0], sizes = [%8], strides = [1] : !flow.dispatch.tensor<readonly:tensor<?xf16>>{%8} -> tensor<?xf16>
+      %11 = tensor.empty() : tensor<i64>
+      %12 = tensor.empty() : tensor<f16>
+      %13 = linalg.fill ins(%c0_i64 : i64) outs(%11 : tensor<i64>) -> tensor<i64>
+      %14 = linalg.fill ins(%cst : f16) outs(%12 : tensor<f16>) -> tensor<f16>
+      %15:2 = linalg.generic {indexing_maps = [affine_map<(d0) -> (d0)>, affine_map<(d0) -> ()>, affine_map<(d0) -> ()>], iterator_types = ["reduction"]} ins(%10 : tensor<?xf16>) outs(%14, %13 : tensor<f16>, tensor<i64>) {
+      ^bb0(%in: f16, %out: f16, %out_0: i64):
+        %16 = linalg.index 0 : index
+        %17 = arith.index_cast %16 : index to i64
+        %18 = arith.maximumf %in, %out : f16
+        %19 = arith.cmpf ogt, %in, %out : f16
+        %20 = arith.select %19, %17, %out_0 : i64
+        linalg.yield %18, %20 : f16, i64
+      } -> (tensor<f16>, tensor<i64>)
+      flow.dispatch.tensor.store %15#1, %7, offsets = [], sizes = [], strides = [] : tensor<i64> -> !flow.dispatch.tensor<writeonly:tensor<i64>>
+      return
+    }
+  }
+}
+}
+
+// CHECK:       #[[$TRANSLATION:.+]] = #iree_codegen.translation_info<LLVMGPUDistribute>
+// CHECK-LABEL: hal.executable.export public @not_neg_inf_init_argmax_1d
+// CHECK-SAME:    translation_info = #[[$TRANSLATION]]
+// CHECK-SAME:    workgroup_size = [1 : index, 1 : index, 1 : index]
+//CHECK-LABEL:      func.func @not_neg_inf_init_argmax_1d
+//      CHECK-NOT:        iree_codegen.ukernel.generic
diff --git a/compiler/src/iree/compiler/Codegen/Utils/GPUUtils.cpp b/compiler/src/iree/compiler/Codegen/Utils/GPUUtils.cpp
index 2ddd654..69ab4e4 100644
--- a/compiler/src/iree/compiler/Codegen/Utils/GPUUtils.cpp
+++ b/compiler/src/iree/compiler/Codegen/Utils/GPUUtils.cpp
@@ -881,4 +881,42 @@
   return false;
 }
 
+//===----------------------------------------------------------------------===//
+// GPU UKernel Utils
+//===----------------------------------------------------------------------===//
+
+// TODO: Add more popular kernels into this list and the ukernel cmake.
+//       No real technical reason to only allow these aside from compile
+//       time and diskspace.
+bool hasUkernelSupportedRocmArch(StringRef targetChip) {
+  const char *kSupportedTargetChip[] = {"gfx90a", "gfx940", "gfx1030",
+                                        "gfx1100"};
+  size_t arraySize =
+      sizeof(kSupportedTargetChip) / sizeof(kSupportedTargetChip[0]);
+  for (int i = 0; i < arraySize; i++) {
+    // return true if targetChip is found inside kSupportedTargetChip.
+    if (targetChip.compare(kSupportedTargetChip[i]) == 0)
+      return true;
+  }
+  return false;
+}
+
+bool hasUkernelSupportedRocmArch(IREE::HAL::ExecutableTargetAttr targetAttr) {
+  auto targetArch = getConfigStringAttr(targetAttr, "target_arch");
+  if (!targetArch) {
+    return false;
+  }
+  StringRef targetArchStr = targetArch->getValue();
+  return hasUkernelSupportedRocmArch(targetArchStr);
+}
+
+/// Checks if target GPU has UKernel support.
+bool hasUkernelSupportedGpuArch(IREE::HAL::ExecutableTargetAttr targetAttr) {
+  if (isROCMBackend(targetAttr) && hasUkernelSupportedRocmArch(targetAttr)) {
+    return true;
+  }
+  // TODO: Once plumbed, add a CUDA backend and supported cuda arch check.
+  return false;
+}
+
 } // namespace mlir::iree_compiler
diff --git a/compiler/src/iree/compiler/Codegen/Utils/GPUUtils.h b/compiler/src/iree/compiler/Codegen/Utils/GPUUtils.h
index e885226..0abe311 100644
--- a/compiler/src/iree/compiler/Codegen/Utils/GPUUtils.h
+++ b/compiler/src/iree/compiler/Codegen/Utils/GPUUtils.h
@@ -110,6 +110,16 @@
 /// using shared memory when CodeGen towards the GPU.
 bool sharedMemTransposeFilter(AffineMap indexMap);
 
+//===----------------------------------------------------------------------===//
+// GPU UKernel Utils
+//===----------------------------------------------------------------------===//
+
+/// Checks if target Chip(StringRef) has UKernel support.
+bool hasUkernelSupportedRocmArch(StringRef targetChip);
+
+/// Checks if targetAttr's GPU target has UKernel support.
+bool hasUkernelSupportedGpuArch(IREE::HAL::ExecutableTargetAttr targetAttr);
+
 } // namespace mlir::iree_compiler
 
 #endif // IREE_COMPILER_CODEGEN_UTILS_GPUUTILS_H_
diff --git a/compiler/src/iree/compiler/Codegen/Utils/Utils.cpp b/compiler/src/iree/compiler/Codegen/Utils/Utils.cpp
index ad7844c..544501f 100644
--- a/compiler/src/iree/compiler/Codegen/Utils/Utils.cpp
+++ b/compiler/src/iree/compiler/Codegen/Utils/Utils.cpp
@@ -148,6 +148,10 @@
   return targetAttr && targetAttr.getBackend().getValue().starts_with("vmvx");
 }
 
+bool isROCMBackend(IREE::HAL::ExecutableTargetAttr targetAttr) {
+  return targetAttr && targetAttr.getBackend().getValue().starts_with("rocm");
+}
+
 bool hasUkernel(IREE::HAL::ExecutableTargetAttr targetAttr,
                 StringRef ukernelName) {
   auto enabledUkernels = getConfigStringAttr(targetAttr, "ukernels");
@@ -800,6 +804,104 @@
   }
 }
 
+LogicalResult isArgmaxOp(linalg::GenericOp genericOp) {
+  // Check for 2 results(value, index), and 1 input
+  if (genericOp.getNumDpsInits() != 2) {
+    return failure();
+  }
+  if (genericOp.getNumDpsInputs() != 1) {
+    return failure();
+  }
+
+  // If max value is being used, it is not a pure argmax.
+  if (!genericOp.getResults()[0].use_empty()) {
+    return failure();
+  }
+
+  // Check that the rank is at least 3 and all loops are parallel
+  unsigned numLoops = genericOp.getNumLoops();
+  unsigned numParallelLoops = genericOp.getNumParallelLoops();
+
+  // Argmax will require 1D reduction.
+  if (numParallelLoops != (numLoops - 1)) {
+    return failure();
+  }
+  // TODO: Add better affine map checks.
+  auto indexing_maps = genericOp.getIndexingMapsArray();
+  if (!indexing_maps[0].isIdentity())
+    return failure();
+
+  // Check that initial value is negative Infinite.
+  // TODO: Move this check to ukernel once we implement
+  //       variant to handle non neg-Inf initial value.
+  Value initVal = genericOp.getDpsInitOperand(0)->get();
+  auto fillOp = initVal.getDefiningOp<linalg::FillOp>();
+  if (!fillOp)
+    return failure();
+  Value fillVal = fillOp.getDpsInputOperand(0)->get();
+  if (!matchPattern(fillVal, m_NegInfFloat()))
+    return failure();
+
+  // Work back from linalg.yield and check body of genericOp.
+  // The genericOp should yield the result of an arith.select,
+  // preceded by an arith.cmpf, arith.maximumf, and arith.extui
+  auto yieldOp = cast<linalg::YieldOp>(genericOp.getBody()->getTerminator());
+  Value producerOutput;
+  Operation *producer;
+
+  // Producer of linalg.yield 1st arg is arith.maximumf
+  {
+    producerOutput = yieldOp->getOperand(0);
+    producer = producerOutput.getDefiningOp();
+    if (!producer || producer->getNumOperands() == 0) {
+      return failure();
+    }
+    if (!matchPattern(producer, m_Op<arith::MaximumFOp>())) {
+      return failure();
+    }
+  }
+
+  // Producer of linalg.yield op 2nd arg is arith.select
+  // TODO: Add check that select is selecting between linalg.index and index of
+  // current max.
+  {
+    producerOutput = yieldOp->getOperand(1);
+    producer = producerOutput.getDefiningOp();
+    if (!producer || producer->getNumOperands() == 0) {
+      return failure();
+    }
+    if (!matchPattern(producer, m_Op<arith::SelectOp>())) {
+      return failure();
+    }
+  }
+
+  // Producer of arith.select op is arith.cmpf
+  {
+    producerOutput = producer->getOperand(0);
+    producer = producerOutput.getDefiningOp();
+    if (!producer || producer->getNumOperands() == 0) {
+      return failure();
+    }
+    auto producerCmpFOp = dyn_cast<arith::CmpFOp>(producer);
+    if (!producerCmpFOp) {
+      return failure();
+    }
+    if (producerCmpFOp.getPredicate() != arith::CmpFPredicate::OGT) {
+      return failure();
+    }
+
+    // Check that in and out of cmpf are loop variables.
+    // Currently first operand is disabled because it may be mixed type
+    // which would lead it to be extf(%arg0).
+    // TODO: Add better mixed type support check.
+    if (producer->getOperand(1) != genericOp.getBody()->getArgument(1)) {
+      return failure();
+    }
+  }
+
+  return success();
+}
+
 //===---------------------------------------------------------------------===//
 // Replace Memref users (transitively)
 //===---------------------------------------------------------------------===//
diff --git a/compiler/src/iree/compiler/Codegen/Utils/Utils.h b/compiler/src/iree/compiler/Codegen/Utils/Utils.h
index 5a75bf0..2c1ab9a 100644
--- a/compiler/src/iree/compiler/Codegen/Utils/Utils.h
+++ b/compiler/src/iree/compiler/Codegen/Utils/Utils.h
@@ -73,6 +73,9 @@
 /// Methods to get target information.
 bool isVMVXBackend(IREE::HAL::ExecutableTargetAttr targetAttr);
 
+/// Methods to get target information.
+bool isROCMBackend(IREE::HAL::ExecutableTargetAttr targetAttr);
+
 // Returns true if the ukernel with given `ukernelName` is enabled.
 // If `ukernelName` is empty (the default), returns true if any ukernel
 // is enabled at all.
@@ -204,6 +207,9 @@
                                               OpFoldResult byteOffset,
                                               Type elementType);
 
+/// Check if a linalg.generic is representing an argmax operation.
+LogicalResult isArgmaxOp(linalg::GenericOp genericOp);
+
 /// Replace the uses of memref value `origValue` with the given
 /// `replacementValue`. Some uses of the memref value might require changes to
 /// the operation itself. Create new operations which can carry the change, and
diff --git a/experimental/regression_suite/pyproject.toml b/experimental/regression_suite/pyproject.toml
index 15c207c..7f590ee 100644
--- a/experimental/regression_suite/pyproject.toml
+++ b/experimental/regression_suite/pyproject.toml
@@ -7,6 +7,8 @@
     "plat_host_cpu: mark tests as running on the host CPU",
     "plat_rdna3_vulkan: mark tests as running on AMD RDNA3 Vulkan device",
     "plat_nvidia_a100: mark tests as running on NVIDIA A100 device",
+    "plat_gfx90a_rocm: mark tests as running on AMD GFX90A ROCm device",
+    "plat_gfx940_rocm: mark tests as running on AMD GFX940 ROCm device",
     "plat_rdna3_rocm: mark tests as running on AMD RDNA3 ROCm device",
     "presubmit: mark test as running on presubmit",
     "postsubmit: mark test as running on postsubmit",
diff --git a/experimental/regression_suite/tests/pregenerated/test_ukernel.py b/experimental/regression_suite/tests/pregenerated/test_ukernel.py
new file mode 100644
index 0000000..1fc5015
--- /dev/null
+++ b/experimental/regression_suite/tests/pregenerated/test_ukernel.py
@@ -0,0 +1,185 @@
+# Copyright 2023 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
+
+import pytest
+from ireers import *
+
+###############################################################################
+# Fixtures
+###############################################################################
+
+COMMON_FLAGS = [
+    "--iree-input-type=none",
+    "--iree-stream-resource-index-bits=64",
+    "--iree-vm-target-index-bits=64",
+    "--iree-stream-resource-max-allocation-size=3221225472",
+]
+
+argmax_ukernel_source = fetch_source_fixture(
+    "https://storage.googleapis.com/shark_tank/ukernel_regression/20231217/argmax/argmax_3d_linalg.mlir",
+    group="argmax_ukernel_linalg",
+)
+
+
+@pytest.fixture
+def argmax_ukernel_gfx90a_rocm_vmfb(argmax_ukernel_source):
+    return iree_compile(
+        argmax_ukernel_source,
+        "gfx90a_rocm",
+        flags=COMMON_FLAGS
+        + [
+            "--iree-hal-target-backends=rocm",
+            "--iree-rocm-target-chip=gfx90a",
+            "--iree-rocm-link-bc=true",
+            "--iree-rocm-enable-ukernels=argmax",
+        ],
+    )
+
+
+@pytest.fixture
+def argmax_ukernel_gfx940_rocm_vmfb(argmax_ukernel_source):
+    return iree_compile(
+        argmax_ukernel_source,
+        "gfx940_rocm",
+        flags=COMMON_FLAGS
+        + [
+            "--iree-hal-target-backends=rocm",
+            "--iree-rocm-target-chip=gfx940",
+            "--iree-rocm-link-bc=true",
+            "--iree-rocm-enable-ukernels=argmax",
+        ],
+    )
+
+
+###############################################################################
+# Correctness
+###############################################################################
+
+# Generation script:
+# argmax_input_f16 = np.random.normal(size=[2, 4, 33000]).astype(np.float32)
+# argmax_output_f16 = np.argmax(argmax_input_f16,axis=-1).astype(np.float32)
+# argmax_input_f32 = np.random.normal(size=[2, 4, 33000]).astype(np.float32)
+# argmax_output_f32 = np.argmax(argmax_input_f32,axis=-1).astype(np.float32)
+# TODO: Currently forcing sitofp (i32 -> f32) and (i64 -> f32) because expected_output
+#       cannot compare signless i64 from vmfb and by default si64 from npy.
+
+argmax_input_f16 = fetch_source_fixture(
+    "https://storage.googleapis.com/shark_tank/ukernel_regression/20231217/argmax/argmax_3d_input_f16.npy",
+    group="argmax_ukernel_input_f16",
+)
+
+argmax_output_f16 = fetch_source_fixture(
+    "https://storage.googleapis.com/shark_tank/ukernel_regression/20231217/argmax/argmax_3d_output_f16.npy",
+    group="argmax_ukernel_output_f16",
+)
+
+argmax_input_f32 = fetch_source_fixture(
+    "https://storage.googleapis.com/shark_tank/ukernel_regression/20231217/argmax/argmax_3d_input_f32.npy",
+    group="argmax_ukernel_input_f32",
+)
+
+argmax_output_f32 = fetch_source_fixture(
+    "https://storage.googleapis.com/shark_tank/ukernel_regression/20231217/argmax/argmax_3d_output_f32.npy",
+    group="argmax_ukernel_output_f32",
+)
+
+
+@pytest.mark.presubmit
+@pytest.mark.unstable_linalg
+@pytest.mark.plat_gfx90a_rocm
+def test_correctness_gfx90a_rocm(
+    argmax_ukernel_gfx90a_rocm_vmfb,
+    argmax_input_f16,
+    argmax_output_f16,
+    argmax_input_f32,
+    argmax_output_f32,
+):
+    iree_run_module(
+        argmax_ukernel_gfx90a_rocm_vmfb,
+        device="rocm",
+        function="argmax_3d_dyn_f16i32",
+        args=[
+            f"--input=@{argmax_input_f16.path}",
+            f"--expected_output=@{argmax_output_f16.path}",
+        ],
+    )
+    iree_run_module(
+        argmax_ukernel_gfx90a_rocm_vmfb,
+        device="rocm",
+        function="argmax_3d_dyn_f16i64",
+        args=[
+            f"--input=@{argmax_input_f16.path}",
+            f"--expected_output=@{argmax_output_f16.path}",
+        ],
+    )
+
+    iree_run_module(
+        argmax_ukernel_gfx90a_rocm_vmfb,
+        device="rocm",
+        function="argmax_3d_dyn_f32i32",
+        args=[
+            f"--input=@{argmax_input_f32.path}",
+            f"--expected_output=@{argmax_output_f32.path}",
+        ],
+    )
+    iree_run_module(
+        argmax_ukernel_gfx90a_rocm_vmfb,
+        device="rocm",
+        function="argmax_3d_dyn_f32i64",
+        args=[
+            f"--input=@{argmax_input_f32.path}",
+            f"--expected_output=@{argmax_output_f32.path}",
+        ],
+    )
+
+
+@pytest.mark.presubmit
+@pytest.mark.unstable_linalg
+@pytest.mark.plat_gfx940_rocm
+def test_correctness_gfx940_rocm(
+    argmax_ukernel_gfx940_rocm_vmfb,
+    argmax_input_f16,
+    argmax_output_f16,
+    argmax_input_f32,
+    argmax_output_f32,
+):
+    iree_run_module(
+        argmax_ukernel_gfx940_rocm_vmfb,
+        device="rocm",
+        function="argmax_3d_dyn_f16i32",
+        args=[
+            f"--input=@{argmax_input_f16.path}",
+            f"--expected_output=@{argmax_output_f16.path}",
+        ],
+    )
+    iree_run_module(
+        argmax_ukernel_gfx940_rocm_vmfb,
+        device="rocm",
+        function="argmax_3d_dyn_f16i64",
+        args=[
+            f"--input=@{argmax_input_f16.path}",
+            f"--expected_output=@{argmax_output_f16.path}",
+        ],
+    )
+
+    iree_run_module(
+        argmax_ukernel_gfx940_rocm_vmfb,
+        device="rocm",
+        function="argmax_3d_dyn_f32i32",
+        args=[
+            f"--input=@{argmax_input_f32.path}",
+            f"--expected_output=@{argmax_output_f32.path}",
+        ],
+    )
+    iree_run_module(
+        argmax_ukernel_gfx940_rocm_vmfb,
+        device="rocm",
+        function="argmax_3d_dyn_f32i64",
+        args=[
+            f"--input=@{argmax_input_f32.path}",
+            f"--expected_output=@{argmax_output_f32.path}",
+        ],
+    )