// Copyright 2021 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/compiler/Codegen/Utils/GPUUtils.h"

#include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenAttrs.h"
#include "iree/compiler/Codegen/Dialect/GPU/IR/IREEGPUAttrs.h"
#include "iree/compiler/Codegen/Dialect/GPU/TargetUtils/KnownTargets.h"
#include "iree/compiler/Codegen/Utils/MarkerUtils.h"
#include "iree/compiler/Codegen/Utils/Utils.h"
#include "iree/compiler/Dialect/HAL/IR/HALTypes.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/DebugLog.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/InterleavedRange.h"
#include "mlir/Dialect/AMDGPU/IR/AMDGPUDialect.h"
#include "mlir/Dialect/AMDGPU/Utils/Chipset.h"
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Arith/Utils/Utils.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/Utils/IndexingUtils.h"
#include "mlir/Dialect/Vector/IR/VectorOps.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Matchers.h"
#include "mlir/Interfaces/SideEffectInterfaces.h"

#include <cassert>
#include <cstdint>
#include <optional>

#define DEBUG_TYPE "iree-codegen-gpu-utils"
#define DBGS() (llvm::dbgs() << "[" DEBUG_TYPE "]: ")
#define DBGSNL() (llvm::dbgs() << "\n")

constexpr unsigned kShuffleBitWidth = 32;
// TODO: These are AMD GPU specific. These need to find a better home.
constexpr char kWavesPerEuAttrName[] = "waves_per_eu";

static llvm::cl::opt<std::string> clTestTarget(
    "iree-gpu-test-target",
    llvm::cl::desc(
        "The target for IR LIT tests. Format is '<arch>:<feature>@<api>', "
        "where <feature> and <api> are optional; e.g., "
        "'gfx942:+sramecc,-xnack@hip'. If <api> is missing, it will be deduced "
        "from <arch>; e.g., 'gfx*' defaults to HIP, 'sm_*' defaults to CUDA"),
    llvm::cl::init(""));

namespace mlir::iree_compiler {

//===----------------------------------------------------------------------===//
// GPU processor IDs and sizes
//===----------------------------------------------------------------------===//

llvm::SmallVector<mlir::linalg::ProcInfo, 2>
getGPUThreadIdsAndCounts(mlir::OpBuilder &builder, mlir::Location loc,
                         unsigned numDims,
                         llvm::ArrayRef<int64_t> workgroupSize) {
  assert(numDims <= kNumGPUDims);
  llvm::SmallVector<mlir::linalg::ProcInfo, 2> procInfo(numDims);
  std::array<gpu::Dimension, kNumGPUDims> dimAttr{
      gpu::Dimension::x, gpu::Dimension::y, gpu::Dimension::z};
  mlir::Type indexType = builder.getIndexType();
  for (unsigned i = 0; i < numDims; ++i) {
    procInfo[numDims - 1 - i] = {
        mlir::gpu::ThreadIdOp::create(builder, loc, indexType, dimAttr[i]),
        mlir::arith::ConstantOp::create(builder, loc,
                                        builder.getIndexAttr(workgroupSize[i])),
        linalg::DistributionMethod::Cyclic};
  }
  return procInfo;
}

llvm::SmallVector<mlir::linalg::ProcInfo, 2>
getSubgroupIdsAndCounts(mlir::OpBuilder &builder, mlir::Location loc,
                        unsigned warpSize, unsigned numDims,
                        llvm::ArrayRef<int64_t> numSubgroups) {
  assert(numDims <= kNumGPUDims);
  llvm::SmallVector<mlir::linalg::ProcInfo, 2> procInfo(numDims);
  std::array<gpu::Dimension, kNumGPUDims> dimAttr{
      gpu::Dimension::x, gpu::Dimension::y, gpu::Dimension::z};
  mlir::Type indexType = builder.getIndexType();
  for (unsigned i = 0; i < numDims; ++i) {
    mlir::Value subgroupId =
        mlir::gpu::ThreadIdOp::create(builder, loc, indexType, dimAttr[i]);
    if (i == 0) {
      subgroupId = affine::AffineDelinearizeIndexOp::create(
                       builder, loc, subgroupId,
                       ArrayRef<int64_t>{numSubgroups[i], warpSize})
                       .getResult(0);
    }
    procInfo[numDims - 1 - i] = {
        subgroupId,
        mlir::arith::ConstantOp::create(builder, loc,
                                        builder.getIndexAttr(numSubgroups[i])),
        linalg::DistributionMethod::Cyclic};
  }
  return procInfo;
}

bool isDescendingRelativeMappingIndices(ArrayRef<Attribute> array) {
  int64_t prev = cast<DeviceMappingAttrInterface>(array[0]).getRelativeIndex();
  for (Attribute attr : array.drop_front()) {
    int64_t relativeIndex =
        cast<DeviceMappingAttrInterface>(attr).getRelativeIndex();
    if (relativeIndex != prev - 1) {
      return false;
    }
    prev = relativeIndex;
  }
  return true;
}

//===----------------------------------------------------------------------===//
// GPU vectorization
//===----------------------------------------------------------------------===//

bool canPerformVectorAccessUsingAllThreads(ArrayRef<int64_t> shape,
                                           int64_t threadCount,
                                           int64_t vectorSize) {
  // Verify that each dimension of the shape can be distributed on the
  // threads
  // For zero dim tensor, consider it's too small to access using all threads.
  if (shape.empty()) {
    return false;
  }
  int64_t threadsAvailable = threadCount;
  for (const auto &[index, dim] : llvm::enumerate(llvm::reverse(shape))) {
    int64_t numElementPerThread = index == 0 ? vectorSize : 1;
    int64_t numThreads = dim / numElementPerThread;
    if (numThreads == 0) {
      return false;
    }
    if (numThreads > threadsAvailable) {
      // If there are no enough remaining threads to distribute the current
      // dimension, try to use all remaining threads. But we still need to make
      // sure all work can be distributed to these threads evenly.
      if (numThreads % threadsAvailable != 0) {
        return false;
      }
      numThreads = threadsAvailable;
    }
    if (threadsAvailable % numThreads != 0) {
      return false;
    }
    threadsAvailable = threadsAvailable / numThreads;
    if (threadsAvailable == 1) {
      break;
    }
  }
  return threadsAvailable == 1;
}

/// Pick an unrolling order that will allow tensorcore operation to reuse LHS
/// register. This is needed to get good performance on sm_80 target.
std::optional<SmallVector<int64_t>>
gpuMmaUnrollOrder(vector::ContractionOp contract) {
  SmallVector<int64_t> order;
  // First make reduction the outer dimensions.
  for (auto [index, iter] : llvm::enumerate(contract.getIteratorTypes())) {
    if (vector::isReductionIterator(iter)) {
      order.push_back(index);
    }
  }

  llvm::SmallDenseSet<int64_t> dims;
  for (AffineExpr expr : contract.getIndexingMapsArray()[0].getResults()) {
    dims.insert(cast<AffineDimExpr>(expr).getPosition());
  }
  // Then parallel dimensions that are part of Lhs as we want to re-use Lhs.
  for (auto [index, iter] : llvm::enumerate(contract.getIteratorTypes())) {
    if (vector::isParallelIterator(iter) && dims.count(index)) {
      order.push_back(index);
    }
  }
  // Then the remaining parallel loops.
  for (auto [index, iter] : llvm::enumerate(contract.getIteratorTypes())) {
    if (vector::isParallelIterator(iter) && !dims.count(index)) {
      order.push_back(index);
    }
  }
  return order;
}

//===----------------------------------------------------------------------===//
// GPU tiling and distribution
//===----------------------------------------------------------------------===//

const char *getGPUDistributeAttrName() { return "iree.gpu.distribute_dim"; }

FailureOr<SmallVector<int64_t>> getGPUTileSize(mlir::FunctionOpInterface funcOp,
                                               int tilingLevel) {
  SmallVector<Operation *> computeOps = getComputeOps(funcOp);
  FailureOr<IREE::Codegen::LoweringConfigAttr> config =
      getFirstLoweringConfig<IREE::Codegen::LoweringConfigAttr>(computeOps);
  if (failed(config)) {
    return funcOp.emitOpError("failed to get lowering configuration");
  }

  return config->getTileSizeVals(tilingLevel);
}

FailureOr<scf::SCFTileSizeComputationFunction>
getGPUScfTileSizeComputeFn(mlir::FunctionOpInterface funcOp, int tilingLevel) {
  FailureOr<SmallVector<int64_t>> tileSizes =
      getGPUTileSize(funcOp, tilingLevel);
  if (failed(tileSizes)) {
    return failure();
  }
  scf::SCFTileSizeComputationFunction computeFn =
      [tileSizes](OpBuilder &builder,
                  Operation *op) -> SmallVector<OpFoldResult> {
    auto tileSizesOfr = getAsIndexOpFoldResult(op->getContext(), *tileSizes);
    auto zeroAttr = builder.getIndexAttr(0);
    int numLoops = cast<TilingInterface>(op).getLoopIteratorTypes().size();
    tileSizesOfr.resize(numLoops, zeroAttr);
    return tileSizesOfr;
  };
  return computeFn;
}

bool isNonZeroRank(TypedValue<VectorType> val) {
  return val.getType().getRank() != 0;
}

//===----------------------------------------------------------------------===//
// GPU workgroup memory
//===----------------------------------------------------------------------===//

std::optional<Value> allocateWorkgroupMemory(OpBuilder &builder,
                                             memref::SubViewOp subview,
                                             ArrayRef<Value> sizeBounds,
                                             DataLayout &) {
  OpBuilder::InsertionGuard guard(builder);

  mlir::FunctionOpInterface funcOp =
      subview->getParentOfType<mlir::FunctionOpInterface>();
  if (!funcOp) {
    return std::nullopt;
  }

  // The subview size bounds are expected to be constant; they specify the shape
  // of the allocation.
  SmallVector<int64_t, 2> shape;
  for (Value bound : sizeBounds) {
    APInt value;
    if (!matchPattern(bound, m_ConstantInt(&value))) {
      return std::nullopt;
    }
    shape.push_back(value.getSExtValue());
  }

  builder.setInsertionPoint(&funcOp.front(), funcOp.front().begin());
  auto type = MemRefType::get(
      shape, subview.getType().getElementType(), MemRefLayoutAttrInterface{},
      gpu::AddressSpaceAttr::get(builder.getContext(),
                                 gpu::GPUDialect::getWorkgroupAddressSpace()));
  Value buffer = memref::AllocOp::create(builder, funcOp.getLoc(), type);
  return buffer;
}

LogicalResult deallocateWorkgroupMemory(OpBuilder &, Value /*buffer*/) {
  return success();
}

LogicalResult copyToWorkgroupMemory(OpBuilder &b, Value src, Value dst) {
  Operation *copyOp = memref::CopyOp::create(b, src.getLoc(), src, dst);
  setMarker(copyOp, getCopyToWorkgroupMemoryMarker());
  return success();
}

static bool propagateCopyDestIntoProducerFill(memref::CopyOp copyOp) {
  // Look for a fill Op writing into the copyOp source.
  Operation *prevOp = copyOp->getPrevNode();
  while (prevOp) {
    if (isMemoryEffectFree(prevOp)) {
      prevOp = prevOp->getPrevNode();
      continue;
    }

    auto fillOp = dyn_cast<linalg::FillOp>(prevOp);
    if (!fillOp) {
      break;
    }
    if (fillOp.output() != copyOp.getSource()) {
      break;
    }
    // Move the fillOp and change the destination to the copy destination.
    fillOp->moveBefore(copyOp);
    fillOp.getOutputsMutable().assign(copyOp.getTarget());
    return true;
  }
  return false;
}

// Split input/output operand from copy from shared memory into a separate
// input.
static void insertInputValueIntoGeneric(Value source, linalg::GenericOp op) {
  SmallVector<Value> newOperands;
  SmallVector<AffineMap> maps;
  for (OpOperand *in : op.getDpsInputOperands()) {
    newOperands.push_back(in->get());
    maps.push_back(op.getMatchingIndexingMap(in));
  }
  newOperands.push_back(source);
  assert(op.getNumDpsInits() == 1);
  OpOperand *outOperand = op.getDpsInitOperand(0);
  maps.push_back(op.getMatchingIndexingMap(outOperand));
  maps.push_back(op.getMatchingIndexingMap(outOperand));
  Location loc = op.getLoc();
  SmallVector<utils::IteratorType> iterTypes(op.getNumLoops(),
                                             utils::IteratorType::parallel);
  OpBuilder builder(op);
  auto newOp = linalg::GenericOp::create(builder, loc, newOperands,
                                         outOperand->get(), maps, iterTypes);
  newOp.getRegion().getBlocks().splice(newOp.getRegion().begin(),
                                       op.getRegion().getBlocks());

  Block &payload = newOp.getRegion().front();
  payload.addArgument(payload.getArguments().back().getType(), loc);
  setMarker(newOp, getCopyToWorkgroupMemoryMarker());
}

/// Propagate the shared memory copy into the consumer op if it's a fully
/// parallel linalg.generic.
static bool
propagateCopySourceIntoConsumerGeneric(memref::CopyOp copyOp,
                                       SmallVector<Operation *> &toDelete) {
  // Look for a generic Op reading the copyOp target.
  Operation *nextOp = copyOp->getNextNode();
  while (nextOp) {
    if (isMemoryEffectFree(nextOp)) {
      nextOp = nextOp->getNextNode();
      continue;
    }
    auto consumer = dyn_cast<linalg::GenericOp>(nextOp);
    if (!consumer || consumer.getNumDpsInits() != 1 ||
        !consumer.getMatchingIndexingMap(consumer.getDpsInitOperand(0))
             .isIdentity()) {
      break;
    }
    if (*consumer.getOutputs().begin() != copyOp.getTarget()) {
      break;
    }
    insertInputValueIntoGeneric(copyOp.getSource(), consumer);
    toDelete.push_back(consumer);
    return true;
  }
  return false;
}

/// This is needed because we are doing promotion to shared memory on buffers.
/// This is a fragile and temporary solution until we move to be able to do this
/// kind of transformations on tensors.
void propagateSharedMemoryCopy(mlir::FunctionOpInterface funcOp) {
  SmallVector<Operation *> toDelete;
  funcOp.walk([&toDelete](memref::CopyOp copyOp) {
    if (hasMarker(copyOp, getCopyToWorkgroupMemoryMarker())) {
      if (propagateCopyDestIntoProducerFill(copyOp) ||
          propagateCopySourceIntoConsumerGeneric(copyOp, toDelete)) {
        toDelete.push_back(copyOp.getOperation());
      }
    }
  });
  for (Operation *op : toDelete) {
    op->erase();
  }
}

void insertBarriersAroundSharedMemoryCopy(mlir::FunctionOpInterface funcOp) {
  OpBuilder builder(funcOp.getContext());
  // Insert barriers before and after copies to workgroup memory and skip
  // insert barriers between back to back copy to workgroup memory.
  funcOp.walk([&builder](Operation *copyOp) {
    if (hasMarker(copyOp, getCopyToWorkgroupMemoryMarker())) {
      Operation *prevOp = copyOp->getPrevNode();
      if (!prevOp || !hasMarker(prevOp, getCopyToWorkgroupMemoryMarker())) {
        builder.setInsertionPoint(copyOp);
        gpu::BarrierOp::create(builder, copyOp->getLoc(),
                               gpu::AddressSpace::Workgroup);
      }
      Operation *nextOp = copyOp->getNextNode();
      if (!nextOp || !hasMarker(nextOp, getCopyToWorkgroupMemoryMarker())) {
        builder.setInsertionPointAfter(copyOp);
        gpu::BarrierOp::create(builder, copyOp->getLoc(),
                               gpu::AddressSpace::Workgroup);
      }
    }
  });
}

//===----------------------------------------------------------------------===//
// Reduction utils
//===----------------------------------------------------------------------===//

/// Packs scalar element to its vector equivalent.
/// (i.e f16 -> vector<1xf16> and f32 -> vector<1xf32>)
static Value promoteElementToVector(Location loc, OpBuilder &builder,
                                    Value input) {
  VectorType vectorTypeBroadcast = VectorType::get({1}, input.getType());
  Value vectorInput =
      vector::BroadcastOp::create(builder, loc, vectorTypeBroadcast, input);
  return vectorInput;
}

Value packVectorToSupportedWidth(Location loc, OpBuilder &builder,
                                 Value input) {
  LLVM_DEBUG({
    auto vecType = cast<VectorType>(input.getType());
    Type elementType = vecType.getElementType();
    assert(vecType.getDimSize(0) * elementType.getIntOrFloatBitWidth() ==
               kShuffleBitWidth &&
           "vecSize * vecBitWidth needs to packable into 32-bitwidth.");
    assert(elementType.isIntOrFloat() &&
           "Only int and float packing is supported.");
  });
  VectorType packed32Type = VectorType::get({1}, builder.getI32Type());
  Value packedInputVec =
      vector::BitCastOp::create(builder, loc, packed32Type, input);
  Value packedInput =
      vector::ExtractOp::create(builder, loc, packedInputVec, 0);
  return packedInput;
}

Value unpackToVector(Location loc, OpBuilder &builder, Value packedInput,
                     VectorType targetVecType) {
  LLVM_DEBUG({
    Type packedType = packedInput.getType();
    assert(packedType.isIntOrFloat() && "Only ints and floats are unpackable.");
    Type elementType = targetVecType.getElementType();
    assert(targetVecType.getDimSize(0) * elementType.getIntOrFloatBitWidth() ==
               packedType.getIntOrFloatBitWidth() &&
           "packed width needs to be unpackable to vecSize * vecBitWidth.");
  });
  Value packedVector = promoteElementToVector(loc, builder, packedInput);
  Value unpackedVector =
      vector::BitCastOp::create(builder, loc, targetVecType, packedVector);
  return unpackedVector;
}

/// Emit warp reduction code sequence for a given scalar input value.
static Value warpReduction(Location loc, OpBuilder &builder, Value input,
                           vector::CombiningKind kind, uint32_t warpSize,
                           uint32_t numLaneToReduce,
                           bool expandSubgroupReduce) {
  assert(llvm::isPowerOf2_32(numLaneToReduce));
  assert((isa<IntegerType, FloatType>(input.getType())) &&
         "Input must be a scalar");
  IntegerType shuffleIntType = builder.getIntegerType(kShuffleBitWidth);
  Type origInputType = input.getType();
  const unsigned origBitWidth = origInputType.getIntOrFloatBitWidth();
  assert(origBitWidth <= kShuffleBitWidth && "Unsupported input type bitwidth");

  const bool needsPacking = kShuffleBitWidth != origBitWidth;
  IntegerType equivIntType = builder.getIntegerType(origBitWidth);

  // Defer expansion of subgroup reduction until later in pass pipeline to
  // enable conditional lowering to DPP ops, for potential perf gains over
  // gpu.shuffle ops.
  if (!expandSubgroupReduce && numLaneToReduce <= warpSize &&
      warpSize % numLaneToReduce == 0) {
    gpu::AllReduceOperation gpuReduceKind = combiningKindToAllReduce(kind);

    // SPIRV currently doesn't have a lowering for clustered reduction,
    // so if possible avoid adding problematic attribute until it is supported.
    if (numLaneToReduce == warpSize) {
      return gpu::SubgroupReduceOp::create(builder, loc, input, gpuReduceKind,
                                           /*uniform=*/false);
    }
    return gpu::SubgroupReduceOp::create(builder, loc, input, gpuReduceKind,
                                         /*uniform=*/false, numLaneToReduce);
  }

  // Otherwise, perform the shuffles over the supported scalar type. For inputs
  // of smaller bitwidth, perform packing and unpacking via the supported
  // integer type.
  auto unpack = [loc, &builder, needsPacking, equivIntType,
                 origInputType](Value packedVal) -> Value {
    if (!needsPacking) {
      return packedVal;
    }
    auto asInt = arith::TruncIOp::create(builder, loc, equivIntType, packedVal);
    return arith::BitcastOp::create(builder, loc, origInputType, asInt);
  };

  auto pack = [loc, &builder, needsPacking, equivIntType,
               shuffleIntType](Value unpackedVal) -> Value {
    if (!needsPacking) {
      return unpackedVal;
    }
    auto asInt =
        arith::BitcastOp::create(builder, loc, equivIntType, unpackedVal);
    return arith::ExtUIOp::create(builder, loc, shuffleIntType, asInt);
  };

  // Lane value always stays in the original type. We use it to perform arith
  // reductions.
  Value laneVal = input;
  // Parallel reduction using butterfly shuffles.
  for (uint64_t i = 1; i < numLaneToReduce; i <<= 1) {
    Value shuffled = gpu::ShuffleOp::create(builder, loc, pack(laneVal), i,
                                            /*width=*/warpSize,
                                            /*mode=*/gpu::ShuffleMode::XOR)
                         .getShuffleResult();
    laneVal = makeArithReduction(builder, loc, kind, laneVal, unpack(shuffled));
  }
  // Broadcast the result to all the lanes.
  if (warpSize != numLaneToReduce) {
    Value shuffled = gpu::ShuffleOp::create(builder, loc, pack(laneVal), 0,
                                            /*width=*/warpSize,
                                            /*mode=*/gpu::ShuffleMode::IDX)
                         .getShuffleResult();
    laneVal = unpack(shuffled);
  }

  return laneVal;
}

// List of identity elements by operation.
// https://en.wikipedia.org/wiki/Identity_element
static TypedAttr getCombiningKindIdentity(OpBuilder &builder,
                                          vector::CombiningKind combiningKind,
                                          Type type) {
  switch (combiningKind) {
  case vector::CombiningKind::ADD:
    return builder.getZeroAttr(type);
  case vector::CombiningKind::MUL: {
    if (type.isIntOrIndex()) {
      return builder.getIntegerAttr(type, 1);
    }
    return builder.getFloatAttr(type, 1);
  }
  case vector::CombiningKind::MINUI:
  case vector::CombiningKind::MINSI:
    return builder.getIntegerAttr(type, std::numeric_limits<int64_t>::max());
  case vector::CombiningKind::MAXUI:
  case vector::CombiningKind::MAXSI:
    return builder.getIntegerAttr(type, std::numeric_limits<int64_t>::min());
  case vector::CombiningKind::AND:
    return builder.getIntegerAttr(type, 1);
  case vector::CombiningKind::OR:
  case vector::CombiningKind::XOR:
    return builder.getZeroAttr(type);
  case vector::CombiningKind::MINIMUMF:
  case vector::CombiningKind::MINNUMF: {
    auto posInfApFloat = APFloat::getInf(
        cast<FloatType>(type).getFloatSemantics(), /*Negative=*/false);
    return builder.getFloatAttr(type, posInfApFloat);
  }
  case vector::CombiningKind::MAXIMUMF:
  case vector::CombiningKind::MAXNUMF: {
    auto negInfApFloat = APFloat::getInf(
        cast<FloatType>(type).getFloatSemantics(), /*Negative=*/true);
    return builder.getFloatAttr(type, negInfApFloat);
  }
  }
  return TypedAttr();
}

/// Emit identity variable.
Value getCombiningIdentityValue(Location loc, OpBuilder &builder,
                                vector::CombiningKind kind, Type identityType) {
  auto vectorType = dyn_cast<VectorType>(identityType);
  Type elementType = identityType;
  if (vectorType) {
    elementType = vectorType.getElementType();
  }
  TypedAttr identityAttr = getCombiningKindIdentity(builder, kind, elementType);
  if (vectorType) {
    identityAttr = DenseElementsAttr::get(vectorType, identityAttr);
  }
  assert(identityAttr && "Unknown identity value for the reduction");
  Value identity =
      arith::ConstantOp::create(builder, loc, identityType, identityAttr);
  return identity;
}

/// Returns the matching GPU reduction operation.
gpu::AllReduceOperation combiningKindToAllReduce(vector::CombiningKind kind) {
  switch (kind) {
#define MAP_CASE(X)                                                            \
  case vector::CombiningKind::X:                                               \
    return gpu::AllReduceOperation::X

    MAP_CASE(ADD);
    MAP_CASE(MUL);
    MAP_CASE(MINUI);
    MAP_CASE(MINSI);
    MAP_CASE(MINNUMF);
    MAP_CASE(MAXSI);
    MAP_CASE(MAXUI);
    MAP_CASE(MAXNUMF);
    MAP_CASE(AND);
    MAP_CASE(OR);
    MAP_CASE(XOR);
    MAP_CASE(MINIMUMF);
    MAP_CASE(MAXIMUMF);
#undef MAP_CASE
  }
  // Upstream LLVM has the same assertion for the reverse direction (see
  // mlir::gpu::convertReductionKind).
  llvm_unreachable("Vector and GPU reduction kinds should match 1:1");
}

/// Emit reduction across a group for a given input.
Value emitGPUGroupReduction(Location loc, OpBuilder &builder, Value input,
                            vector::CombiningKind kind, uint32_t size,
                            int warpSize, bool expandSubgroupReduce) {
  assert(
      size % warpSize == 0 &&
      "Group reduction only support for sizes aligned on warp size for now.");

  // First reduce on a single thread to get per lane reduction value.
  Value laneVal = vector::ReductionOp::create(builder, loc, kind, input);
  laneVal = warpReduction(loc, builder, laneVal, kind, warpSize, warpSize,
                          expandSubgroupReduce);
  // Simple case -- emit `gpu.subgroup_reduce` directly.
  if (!expandSubgroupReduce && size == warpSize) {
    return laneVal;
  }

  // More-involved case -- generate `gpu.shuffle` ops over i32 values (using the
  // butterfly shuffle algorithm).
  // if we have more than one warp, reduce across warps.
  if (size > warpSize) {
    uint32_t numWarp = size / warpSize;
    assert(numWarp <= warpSize &&
           "Only support 1 level, need to implement recursive/loop for this "
           "case.");
    auto addressSpaceAttr = gpu::AddressSpaceAttr::get(
        builder.getContext(), gpu::GPUDialect::getWorkgroupAddressSpace());
    MemRefType memrefType =
        MemRefType::get(numWarp, laneVal.getType(), MemRefLayoutAttrInterface{},
                        addressSpaceAttr);
    Value alloc = memref::AllocOp::create(builder, loc, memrefType);
    Value threadX = gpu::ThreadIdOp::create(
        builder, loc, builder.getIndexType(), gpu::Dimension::x);
    Value cstWarpSize = arith::ConstantIndexOp::create(builder, loc, warpSize);
    Value warpId = arith::DivUIOp::create(builder, loc, threadX, cstWarpSize);
    Value laneId = arith::RemUIOp::create(builder, loc, threadX, cstWarpSize);
    Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
    Value lane0 = arith::CmpIOp::create(builder, loc, arith::CmpIPredicate::eq,
                                        laneId, zero);
    // Store the reduction for each warp.
    SmallVector<Value> indices = {warpId};
    scf::IfOp::create(builder, loc, lane0, [&](OpBuilder &b, Location l) {
      memref::StoreOp::create(b, l, laneVal, alloc, indices);
      scf::YieldOp::create(b, l);
    });
    gpu::BarrierOp::create(builder, loc, gpu::AddressSpace::Workgroup);
    // Further reduce the outputs from each warps with a single warp reduce.
    Value memrefSize =
        arith::ConstantIndexOp::create(builder, loc, numWarp - 1);
    Value laneIdInBounds =
        arith::MinUIOp::create(builder, loc, laneId, memrefSize);
    Value loadVal = memref::LoadOp::create(builder, loc, alloc, laneIdInBounds);
    Value cstNumWarp = arith::ConstantIndexOp::create(builder, loc, numWarp);
    if (!llvm::isPowerOf2_32(numWarp)) {
      // Pad with identity element if numel < warpSize for valid warp reduction.
      Value useIdentityElement = arith::CmpIOp::create(
          builder, loc, arith::CmpIPredicate::sge, laneId, cstNumWarp);
      numWarp = llvm::PowerOf2Ceil(numWarp);
      Value identity =
          getCombiningIdentityValue(loc, builder, kind, loadVal.getType());
      loadVal = arith::SelectOp::create(builder, loc, useIdentityElement,
                                        identity, loadVal);
    }
    laneVal = warpReduction(loc, builder, loadVal, kind, warpSize, numWarp,
                            /*expandSubgroupReduce=*/true);
  }

  return laneVal;
}

std::optional<SmallVector<int64_t>> getWmmaNativeVectorSize(Operation *op) {
  // Currently hardcode the size of wmma operation. When more cases are
  // supported this should be picked based on what the backend supports.
  int64_t m = 16;
  int64_t n = 16;
  if (auto contract = dyn_cast<vector::ContractionOp>(op)) {
    int64_t k = contract.getLhsType().getElementType().isF16() ? 16 : 8;
    SmallVector<int64_t> nativeSize(contract.getIteratorTypes().size() - 3, 1);
    nativeSize.append({m, n, k});
    return nativeSize;
  }
  if (auto writeOp = dyn_cast<vector::TransferWriteOp>(op)) {
    if (writeOp.getVectorType().getRank() < 2) {
      return std::nullopt;
    }
    SmallVector<int64_t> nativeSize(writeOp.getVectorType().getRank() - 2, 1);
    nativeSize.append({m, n});
    return nativeSize;
  }
  if (auto readOp = dyn_cast<vector::TransferReadOp>(op)) {
    // Transfer read ops may need different shapes based on how they are being
    // used. For simplicity just match the shape used by the extract strided op.
    VectorType sliceType;
    for (Operation *users : op->getUsers()) {
      auto extract = dyn_cast<vector::ExtractStridedSliceOp>(users);
      if (!extract) {
        return std::nullopt;
      }
      auto vecType = cast<VectorType>(extract.getResult().getType());
      if (sliceType && sliceType != vecType) {
        return std::nullopt;
      }
      sliceType = vecType;
    }
    return llvm::to_vector(sliceType.getShape());
  }
  if ((OpTrait::hasElementwiseMappableTraits(op) && op->getNumResults() == 1)) {
    if (auto vecType = dyn_cast<VectorType>(op->getResultTypes()[0])) {
      // TODO: The condition for unrolling elementwise should be restricted
      // only to operations that need unrolling (connected to the contract).
      if (vecType.getRank() < 2) {
        return std::nullopt;
      }

      // First check whether there is a slice to infer the shape from. This is
      // required for cases where the accumulator type differs from the input
      // types, in which case we will see an `arith.ext_` between the contract
      // and transfer_read which needs to be unrolled.
      VectorType sliceType;
      for (Operation *users : op->getUsers()) {
        auto extract = dyn_cast<vector::ExtractStridedSliceOp>(users);
        if (!extract) {
          return std::nullopt;
        }
        auto vecType = cast<VectorType>(extract.getResult().getType());
        if (sliceType && sliceType != vecType) {
          return std::nullopt;
        }
        sliceType = vecType;
      }
      if (sliceType) {
        return llvm::to_vector(sliceType.getShape());
      }

      // Else unroll for trailing elementwise.
      SmallVector<int64_t> nativeSize(vecType.getRank() - 2, 1);
      // Map elementwise ops to the output shape.
      nativeSize.append({m, n});
      return nativeSize;
    }
  }
  return std::nullopt;
}

static FailureOr<int64_t>
getOperandBitwidth(IREE::Codegen::InnerTileDescAttrInterface intrinsic,
                   int operandIndex) {
  SmallVector<Type> elementTypes;
  intrinsic.getElementTypes(elementTypes);
  assert(operandIndex >= 0 && "operand index must be non-negative");
  return elementTypes[operandIndex].getIntOrFloatBitWidth();
}

/// Returns the number of elements along the K dimensions (all reduction
/// dimensions) of the given intrinsic. For intrinsics with multiple K
/// dimensions, returns the product of all K dimensions. This is determined by
/// looking at all reduction dimensions in the undistributed tile shape.
static FailureOr<int64_t>
getKSize(IREE::Codegen::InnerTileDescAttrInterface intrinsic) {
  int64_t kSize = 1;
  int64_t lhsOperandIndex = 0;
  SmallVector<VectorType> undistributedTypes;
  intrinsic.getUndistributedTileTypes(undistributedTypes);
  ArrayRef<int64_t> shape = undistributedTypes[lhsOperandIndex].getShape();
  SmallVector<utils::IteratorType> lhsIteratorTypes =
      intrinsic.getOperandIteratorTypes()[lhsOperandIndex];
  for (auto [dim, iteratorType] : llvm::zip_equal(shape, lhsIteratorTypes)) {
    if (iteratorType == utils::IteratorType::reduction) {
      kSize *= dim;
    }
  }
  return kSize;
}

/// Returns the number of elements each thread accesses for the given intrinsic
/// and operand index. This is determined by the `element` field of the
/// subgroup layout.
static FailureOr<int64_t>
getNumAccessElems(IREE::Codegen::InnerTileDescAttrInterface intrinsic,
                  int operandIndex) {
  IREE::GPU::MMASingleSubgroupLayout layout =
      IREE::GPU::getSingleSubgroupLayout(intrinsic, operandIndex);
  return llvm::product_of(layout.element);
}

/// Returns the total number of elements in the shape of the given intrinsic
/// with the given operand index. This is the product of all dimensions in the
/// distributed shape: outer × thread × element.
static FailureOr<int64_t>
getTotalTileElems(IREE::Codegen::InnerTileDescAttrInterface intrinsic,
                  int operandIndex) {
  IREE::GPU::MMASingleSubgroupLayout layout =
      IREE::GPU::getSingleSubgroupLayout(intrinsic, operandIndex);
  return llvm::product_of(layout.outer) * llvm::product_of(layout.thread) *
         llvm::product_of(layout.element);
}

static FailureOr<XorShuffleParams> getXorShuffleParamsForGfx950(
    IREE::GPU::TargetAttr target,
    IREE::Codegen::InnerTileDescAttrInterface intrinsic) {
  if (auto smma = dyn_cast<IREE::GPU::ScaledMMAAttr>(intrinsic)) {
    switch (smma.getIntrinsic()) {
    case IREE::GPU::ScaledMMAIntrinsic::MFMA_SCALE_F32_16x16x128_B32:
      return XorShuffleParams({/*rowElems=*/256,
                               /*accessElems=*/32});
    default:
      // TODO(muzasyed): Add more intrinsics for gfx950.
      return failure();
    }
  }
  if (auto mma = dyn_cast<IREE::GPU::MMAAttr>(intrinsic)) {
    switch (mma.getIntrinsic()) {
    case IREE::GPU::MMAIntrinsic::MFMA_F32_16x16x32_F16:
    case IREE::GPU::MMAIntrinsic::MFMA_F32_32x32x16_F16:
    case IREE::GPU::MMAIntrinsic::MFMA_F32_16x16x32_BF16:
    case IREE::GPU::MMAIntrinsic::MFMA_F32_32x32x16_BF16:
      return XorShuffleParams({/*rowElems=*/64,
                               /*accessElems=*/8});
    default:
      return failure();
    }
  }
  return failure();
}

/// Validate the XOR shuffle parameters for the given intrinsic and operand
/// index. If the parameters produce an XOR Shuffle that is not valid, return
/// failure. Else, return the swizzle parameters.
static FailureOr<XorShuffleParams>
validateXorShuffle(FailureOr<XorShuffleParams> swizzle,
                   IREE::Codegen::InnerTileDescAttrInterface intrinsic,
                   int operandIndex) {
  FailureOr<int64_t> maybeTotalTileElems =
      getTotalTileElems(intrinsic, operandIndex);
  if (failed(maybeTotalTileElems)) {
    return failure();
  }
  int64_t totalTileElems = *maybeTotalTileElems;
  if (!isXORShuffleValid(swizzle->rowElems, swizzle->accessElems,
                         totalTileElems)) {
    return failure();
  }
  return swizzle;
}

// Disabling clang-tidy for the following functions, as it will be externally
// linked to the CAPI in a future PR.
// NOLINTBEGIN(misc-use-internal-linkage)
FailureOr<XorShuffleBounds>
getXorShuffleBounds(IREE::Codegen::InnerTileDescAttrInterface intrinsic,
                    int operandIndex) {
  FailureOr<int64_t> maybeMinimumAccessElems =
      getNumAccessElems(intrinsic, operandIndex);
  FailureOr<int64_t> maybeTotalTileElems =
      getTotalTileElems(intrinsic, operandIndex);
  if (failed(maybeMinimumAccessElems) || failed(maybeTotalTileElems)) {
    return failure();
  }
  return XorShuffleBounds{*maybeMinimumAccessElems, *maybeTotalTileElems};
}

bool isXORShuffleValid(int64_t numRowElems, int64_t numAccessElems,
                       int64_t totalTileElems) {
  // The number of total tile elements we want to swizzle must be greater than
  // or equal to the number of elements in a row.
  if (totalTileElems < numRowElems) {
    LDBG()
        << "The number of row elements exceed the total elements in the tile";
    return false;
  }
  // We need at least one column of access elements to swizzle within a row.
  if (numRowElems < numAccessElems) {
    LDBG()
        << "The number of access elements exceed the total elements in the row";
    return false;
  }
  // The size of a row must evenly divide the total number of tile elements.
  if (totalTileElems % numRowElems != 0) {
    LDBG() << "The number of row elements does not evenly divide the total "
              "elements in the tile";
    return false;
  }
  // The number of access elements must evenly divide the number of row
  // elements.
  if (numRowElems % numAccessElems != 0) {
    LDBG() << "The number of access elements does not evenly divide the number "
              "of row elements";
    return false;
  }
  // The number of columns in a row must evenly divide the total number of tile
  // elements. This is to avoid incomplete rows at the end of the tile.
  if (totalTileElems % (numRowElems / numAccessElems) != 0) {
    LDBG() << "The number of columns in a row does not evenly divide the total "
              "elements in the tile";
    return false;
  }
  return true;
}

FailureOr<XorShuffleParams> getXorShuffleParamsForTunedChipset(
    IREE::GPU::TargetAttr target,
    IREE::Codegen::InnerTileDescAttrInterface intrinsic, int operandIndex) {
  FailureOr<amdgpu::Chipset> maybeChipset =
      amdgpu::Chipset::parse(target.getArch());
  if (failed(maybeChipset)) {
    return failure();
  }
  if (*maybeChipset == amdgpu::Chipset(9, 5, 0)) {
    return validateXorShuffle(getXorShuffleParamsForGfx950(target, intrinsic),
                              intrinsic, operandIndex);
  }
  return failure();
}

FailureOr<XorShuffleParams> getXorShuffleParamsForUntunedChipset(
    IREE::GPU::TargetAttr target,
    IREE::Codegen::InnerTileDescAttrInterface intrinsic,
    ArrayRef<int64_t> reductionTileSizes, int operandIndex) {
  // Compute XOR shuffle swizzle parameters for bank conflict avoidance.
  // - rowElems: Select entirety of K Tile size, may not prevent bank
  //              conflicts if the K tile size is too small.
  // - accessElems: number of contiguous elements each thread accesses,
  //                 derived from the MMA intrinsic's element layout.
  int64_t numAccessElems = getNumAccessElems(intrinsic, operandIndex).value();

  // Calculate K tile size (total K elements in shared memory) to use as row
  // width. For small K tiles, this may not reduce bank conflicts effectively.
  int64_t kTileSize =
      llvm::product_of(reductionTileSizes) * getKSize(intrinsic).value();

  // Figure out how many elements can fit across all banks of LDS.
  IREE::GPU::TargetWgpAttr wgp = target.getWgp();
  FailureOr<int64_t> bitwidth = getOperandBitwidth(intrinsic, operandIndex);
  if (failed(bitwidth)) {
    return failure();
  }
  std::optional<int64_t> workgroupMemoryBankCount =
      wgp.getWorkgroupMemoryBankCount();
  if (!workgroupMemoryBankCount.has_value()) {
    return failure();
  }
  // Assuming each bank is 4 bytes wide (32 bits).
  int64_t ldsBankWidthBits =
      (workgroupMemoryBankCount.value() * int64_t(32)) / *bitwidth;

  // Row width must be less than or equal to the row size (in elements) of LDS
  // bank width to prevent bank conflicts.
  int64_t effectiverowElems = std::min(ldsBankWidthBits, kTileSize);

  // Ensure row width is at least access width (minimum 1 column).
  effectiverowElems = std::max(effectiverowElems, numAccessElems);
  return validateXorShuffle(XorShuffleParams({/*rowElems=*/effectiverowElems,
                                              /*accessElems=*/numAccessElems}),
                            intrinsic, operandIndex);
}

FailureOr<XorShuffleParams>
getXorShuffleParams(IREE::GPU::TargetAttr target,
                    IREE::Codegen::InnerTileDescAttrInterface intrinsic,
                    ArrayRef<int64_t> reductionTileSizes, int operandIndex,
                    bool skipUntunedFallback) {
  FailureOr<XorShuffleParams> xorShuffleAttr =
      getXorShuffleParamsForTunedChipset(target, intrinsic, operandIndex);
  if (failed(xorShuffleAttr) && !skipUntunedFallback) {
    xorShuffleAttr = getXorShuffleParamsForUntunedChipset(
        target, intrinsic, reductionTileSizes, operandIndex);
  }
  return xorShuffleAttr;
}
// NOLINTEND(misc-use-internal-linkage)

FailureOr<Attribute>
getXorShuffleAttr(MLIRContext *context, Attribute baseConfigAttr,
                  IREE::GPU::TargetAttr target,
                  IREE::Codegen::InnerTileDescAttrInterface intrinsic,
                  ArrayRef<int64_t> reductionTileSizes, int operandIndex,
                  bool skipUntunedFallback) {
  FailureOr<XorShuffleParams> xorShuffleParams = getXorShuffleParams(
      target, intrinsic, reductionTileSizes, operandIndex, skipUntunedFallback);
  if (failed(xorShuffleParams)) {
    return failure();
  }
  int64_t effectiverowElems = xorShuffleParams.value().rowElems;
  int64_t numAccessElems = xorShuffleParams.value().accessElems;
  auto swizzleAttr = IREE::Codegen::XORShuffleAttr::get(
      context, effectiverowElems, numAccessElems,
      /*row_stride=*/int64_t(0),
      /*per_phase=*/int64_t(0));
  return IREE::GPU::SwizzleOperandAttr::get(context, baseConfigAttr,
                                            swizzleAttr);
}

//===----------------------------------------------------------------------===//
// getMmaNativeVectorSize
//===----------------------------------------------------------------------===//
/// Returns vector::ContractionOp operand's index where the result is used.
static std::optional<int>
getVectorContractOpOperandId(vector::ContractionOp contractOp,
                             OpResult result) {
  if (contractOp.getLhs() == result) {
    return 0;
  }
  if (contractOp.getRhs() == result) {
    return 1;
  }
  if (contractOp.getAcc() == result) {
    return 2;
  }
  return std::nullopt;
}

/// Returns vector::ContractionOp operand's index  where the
/// vector::TransferReadOp is consumed either consumed directly or via
/// vector::ExtractStridedSliceOp.
static std::optional<int>
getVectorContractOpOperandIdForVectorReadOp(Operation *op) {
  vector::ContractionOp contractOp;

  // Check if the vector::TransferReadOp is consumed directly by
  // vector::ContractionOp.
  if (op->use_empty()) {
    return std::nullopt;
  }
  Operation *firstLevelUser = *((op->getUsers()).begin());
  if (!firstLevelUser) {
    return std::nullopt;
  }
  if (auto contractOp = dyn_cast<vector::ContractionOp>(firstLevelUser)) {
    return getVectorContractOpOperandId(contractOp, op->getResult(0));
  }

  // Check if the vector::TransferReadOp is consumed indirectly by
  // vector::ContractionOp. Only check until the second level of use-def chain.
  if (firstLevelUser->use_empty()) {
    return std::nullopt;
  }
  Operation *secondLevelUser = *((firstLevelUser->getUsers()).begin());
  if (!secondLevelUser) {
    return std::nullopt;
  }
  if (auto contractOp = dyn_cast<vector::ContractionOp>(secondLevelUser)) {
    return getVectorContractOpOperandId(contractOp,
                                        firstLevelUser->getResult(0));
  }
  return std::nullopt;
}

/// Helper function to return native size for MMA.SYNC-based operations.
std::optional<SmallVector<int64_t>> getMmaNativeVectorSize(Operation *op) {
  // Shape of native Tensor Core GPU mma.sync operations.
  int64_t mmaShapeM = 16;
  int64_t mmaShapeN = 8;
  int64_t mmaShapeK;

  // Shape the mma.sync warp-level operation.
  if (auto contract = dyn_cast<vector::ContractionOp>(op)) {
    Type sourceType = contract.getLhsType().getElementType();

    // Set mmaShapeK based on sourceType.
    if (sourceType.isInteger(4)) {
      mmaShapeK = 64;
    } else if (sourceType.isInteger(8)) {
      mmaShapeK = 32;
    } else if (sourceType.isF16() || sourceType.isBF16()) {
      mmaShapeK = 16;
    } else if (sourceType.isF32()) {
      mmaShapeK = 8;
    } else {
      LDBG() << "unsupported shape for vector.contract: ";
      return std::nullopt;
    }

    // Initialize/set the starting dims of the ranked shape, such as batch,
    // to 1.
    SmallVector<int64_t> mmaShape(contract.getIteratorTypes().size() - 3, 1);
    mmaShape.append({mmaShapeM, mmaShapeN, mmaShapeK});
    LDBG() << "shape for vector.contract: " << llvm::interleaved(mmaShape);
    return mmaShape;
  }

  // Shape of warp-level vector write operation.
  if (auto writeOp = dyn_cast<vector::TransferWriteOp>(op)) {
    if (writeOp.getVectorType().getRank() < 2) {
      return std::nullopt;
    }
    SmallVector<int64_t> outputShape(writeOp.getVectorType().getRank() - 2, 1);
    outputShape.append({mmaShapeM, mmaShapeN});
    LDBG() << "shape for vector.xfer_write: " << llvm::interleaved(outputShape);
    return outputShape;
  }

  // Shape of warp-level vector read (load) operation.
  if (auto readOp = dyn_cast<vector::TransferReadOp>(op)) {
    auto resultVectorType = cast<VectorType>(readOp.getVector().getType());
    Type resultElementType = resultVectorType.getElementType();

    std::optional<int> operandId =
        getVectorContractOpOperandIdForVectorReadOp(op);
    if (!operandId) {
      LDBG() << "Failed to get operandId for vector::xfer_read: " << *op;
      return std::nullopt;
    }

    // Loading F16 values from Shared Memory to Registers.
    if (resultElementType.isF16() || resultElementType.isBF16()) {
      // For matrixC.
      if (*operandId == 2) {
        SmallVector<int64_t> readShape;
        readShape.append({mmaShapeM, mmaShapeN});
        LLVM_DEBUG({
          llvm::interleaveComma(readShape,
                                DBGS() << "shape for vector.xfer_read: ");
          llvm::dbgs() << "\n";
        });
        return readShape;
      }

      // For matrixA and matrixB.
      if (*operandId == 0 || *operandId == 1) {
        // MmaSyncOp input operands: matrixA and matrixB.
        // LDSMx1, x2, x4:
        // - LDSMx1 loads a 1 tile  of 8x8.
        // - LDSMx2 loads a 2 tiles of 8x8.
        // - LDSMx4 loads a 4 tiles of 8x8. (in use)
        // IREE uses the largest tiled load, i.e., LDSMx4.

        // MmaSyncOp source operand: matrixC.
        // matrixC is also read/written in tiled block of 16x16. In the pass
        // OptimizeVectorTransfer, matrixC reads are moved above the mainloop
        // and writes are moved below the mainloop. Thus, mma.sync read/write
        // accumulator inplace.
        SmallVector<int64_t> readShape;
        readShape.append({16, 16});
        LLVM_DEBUG({
          llvm::interleaveComma(readShape,
                                DBGS() << "shape for vector.xfer_read: ");
          llvm::dbgs() << "\n";
        });
        return readShape;
      }
    }

    // Loading F32 values from Shared Memory to Registers.
    if (resultElementType.isF32()) {
      // Set mmaShapeK for F32 datatype mma.sync.f32.tf32.m16n8k8.
      mmaShapeK = 8;

      // For matrixC.
      if (*operandId == 2) {
        SmallVector<int64_t> readShape;
        readShape.append({mmaShapeM, mmaShapeN});
        LDBG() << "shape for vector.xfer_read: "
               << llvm::interleaved(readShape);
        return readShape;
      }
      // For matrixA.
      if (*operandId == 0) {
        SmallVector<int64_t> readShape;
        readShape.append({mmaShapeM, mmaShapeK});
        LDBG() << "shape for vector.xfer_read: "
               << llvm::interleaved(readShape);
        return readShape;
      }
      // For matrixB.
      if (*operandId == 1) {
        // Do not use ldmatrix for matrixB.
        // Transfer read ops may need different shapes based on how they are
        // being used. For simplicity just match the shape used by the extract
        // strided op.
        VectorType sliceType;
        for (Operation *users : op->getUsers()) {
          auto extract = dyn_cast<vector::ExtractStridedSliceOp>(users);
          if (!extract) {
            return std::nullopt;
          }
          auto vecType = cast<VectorType>(extract.getResult().getType());
          if (sliceType && sliceType != vecType) {
            return std::nullopt;
          }
          sliceType = vecType;
        }
        LDBG() << "shape for vector.xfer_read: "
               << llvm::interleaved(sliceType.getShape());
        return llvm::to_vector(sliceType.getShape());
      }
    }
  }
  LDBG() << "unsupported shape for " << op->getName().getStringRef();
  return std::nullopt;
}

bool hasGlobalMemoryAddressSpace(MemRefType memrefType) {
  Attribute addrSpace = memrefType.getMemorySpace();
  if (!addrSpace) {
    return true;
  }
  auto intAttr = dyn_cast<IntegerAttr>(addrSpace);
  // Accept both default numeric address space and HAL descriptor type address
  // space--the former is used by LLVMGPU while the latter is used by SPIR-V.
  if (intAttr && intAttr.getInt() == 0) {
    return true;
  }
  auto gpuAttr = dyn_cast<gpu::AddressSpaceAttr>(addrSpace);
  if (gpuAttr && gpuAttr.getValue() == gpu::AddressSpace::Global) {
    return true;
  }
  auto amdgpuAttr = dyn_cast<amdgpu::AddressSpaceAttr>(addrSpace);
  if (amdgpuAttr &&
      amdgpuAttr.getValue() == amdgpu::AddressSpace::FatRawBuffer) {
    return true;
  }
  return isa<IREE::HAL::DescriptorTypeAttr>(addrSpace);
}

bool hasAMDGPUFatRawBufferAddressSpace(MemRefType memrefType) {
  Attribute addrSpace = memrefType.getMemorySpace();
  if (!addrSpace) {
    return false;
  }
  auto amdgpuAttr = dyn_cast<amdgpu::AddressSpaceAttr>(addrSpace);
  if (amdgpuAttr &&
      amdgpuAttr.getValue() == amdgpu::AddressSpace::FatRawBuffer) {
    return true;
  }
  return false;
}

bool hasSharedMemoryAddressSpace(MemRefType memrefType) {
  auto addrSpace =
      dyn_cast_if_present<gpu::AddressSpaceAttr>(memrefType.getMemorySpace());
  return addrSpace &&
         addrSpace.getValue() == gpu::GPUDialect::getWorkgroupAddressSpace();
}

//===----------------------------------------------------------------------===//
// GPU CodeGen op filter
//===----------------------------------------------------------------------===//

/// Returns true if the index map represents a transpose that benefits from
/// shared mem.
bool sharedMemTransposeFilter(AffineMap indexMap) {
  if (!indexMap.isEmpty() && indexMap.isPermutation()) {
    // Ensure that the fasted moving dimension (the last one) is permuted,
    // Otherwise shared memory promotion will not benefit the operation.
    if (indexMap.getDimPosition(indexMap.getNumDims() - 1) !=
        indexMap.getNumDims() - 1) {
      return true;
    }
  }
  return false;
}

//===----------------------------------------------------------------------===//
// GPU Target Information
//===----------------------------------------------------------------------===//

IREE::GPU::TargetAttr getCLGPUTarget(MLIRContext *context) {
  if (clTestTarget.empty()) {
    return nullptr;
  }

  auto [archAndFeatures, backend] = StringRef(clTestTarget).split("@");
  if (backend.empty()) {
    // Guess what the target API is based on common scheme. This does not work
    // for cases like "ampere" which can be accepted by both CUDA and Vulkan;
    // it's very limited. So it's targeting common cases to make writing tests
    // simpler.
    if (StringRef(clTestTarget).starts_with("sm_")) {
      backend = "cuda";
    } else if (StringRef(clTestTarget).starts_with("gfx")) {
      backend = "hip";
    } else if (StringRef(clTestTarget).starts_with("adreno")) {
      backend = "vulkan";
    } else if (StringRef(clTestTarget).starts_with("apple")) {
      backend = "vulkan";
    } else if (StringRef(clTestTarget).starts_with("valhall")) {
      backend = "vulkan";
    } else if (StringRef(clTestTarget).starts_with("videocore")) {
      backend = "vulkan";
    }
  }
  auto [arch, features] = StringRef(archAndFeatures).split(':');
  // Use the target specified in the command line for testing purposes.
  return IREE::GPU::getFullTarget(backend, arch, features, context);
}

IREE::GPU::TargetAttr getGPUTargetAttr(DictionaryAttr attr) {
  return dyn_cast_if_present<IREE::GPU::TargetAttr>(getConfigTargetInfo(attr));
}

IREE::GPU::TargetAttr getGPUTargetAttr(MLIRContext *context,
                                       IREE::HAL::ExecutableTargetAttr target) {
  IREE::GPU::TargetAttr gpuTargetAttr;
  if (target) {
    gpuTargetAttr = getGPUTargetAttr(target.getConfiguration());
  }
  if (!gpuTargetAttr) {
    gpuTargetAttr = getCLGPUTarget(context);
  }
  return gpuTargetAttr;
}

IREE::GPU::TargetAttr getGPUTargetAttr(Operation *op) {
  return getGPUTargetAttr(op->getContext(),
                          IREE::HAL::ExecutableTargetAttr::lookup(op));
}

bool targetSupportsGlobalLoadDMA(IREE::GPU::TargetAttr target) {
  if (!target) {
    return false;
  }
  FailureOr<amdgpu::Chipset> chipset = amdgpu::Chipset::parse(target.getArch());
  if (failed(chipset)) {
    return false;
  }
  return chipset->majorVersion == 9 && chipset->minorVersion >= 5;
}

bool targetSupportsShuffleBitwidth(IREE::GPU::TargetAttr target,
                                   unsigned bitwidth) {
  if (bitwidth <= kShuffleNativeBits) {
    return true;
  }
  return target && target.isAMD();
}

void addConfigGPUTarget(MLIRContext *context,
                        IREE::GPU::TargetAttr gpuTargetAttr,
                        SmallVectorImpl<NamedAttribute> &config) {
  addConfigTargetInfo(context, gpuTargetAttr, config);
}

IntegerAttr getConfigWavesPerEuAttr(DictionaryAttr targetConfig) {
  return targetConfig.getAs<IntegerAttr>(kWavesPerEuAttrName);
}
std::optional<int64_t> getConfigWavesPerEu(DictionaryAttr targetConfig) {
  auto attr = getConfigWavesPerEuAttr(targetConfig);
  if (attr) {
    return attr.getInt();
  }
  return std::nullopt;
}
void addConfigWavesPerEu(MLIRContext *context, int64_t wavesPerEu,
                         SmallVectorImpl<NamedAttribute> &config) {
  config.emplace_back(
      StringAttr::get(context, kWavesPerEuAttrName),
      IntegerAttr::get(IntegerType::get(context, 64), wavesPerEu));
}

std::optional<int> getGPUSubgroupSize(mlir::FunctionOpInterface func) {
  // First try to see if there is a subgroup size chosen in the CodeGen pipeline
  // configuration.
  if (std::optional<int64_t> subgroupSize = getSubgroupSize(func)) {
    return subgroupSize.value();
  }
  // Then try to find the subgroup size from the target description.
  if (IREE::GPU::TargetAttr target = getGPUTargetAttr(func)) {
    return target.getPreferredSubgroupSize();
  }
  return std::nullopt;
}

SmallVector<IREE::HAL::ExecutableVariantOp>
getExecutableVariantOps(mlir::ModuleOp moduleOp) {
  // The variant ops must have a ExecutableOp parent. Thus we iterate on the
  // ExecutableOp using `getOps()` for efficiency. We do not need to walk
  // through all the ops in the `moduleOp` in a nested fashion.
  SmallVector<IREE::HAL::ExecutableVariantOp> executableVariantOps;
  for (auto executableOp : moduleOp.getOps<IREE::HAL::ExecutableOp>()) {
    auto iter = executableOp.getOps<IREE::HAL::ExecutableVariantOp>();
    executableVariantOps.append(iter.begin(), iter.end());
  }
  return executableVariantOps;
}

Value applyInverseXorSwizzleToDMASourceOffset(
    OpBuilder &builder, Location loc, Value srcLinearOffset,
    IREE::Codegen::XORShuffleAttr swizzle, Value dest) {
  // Compute the subgroup's base offset within the full allocation by tracing
  // through view-like ops and accumulating the linear element offset.
  Value destTrace = dest;
  OpFoldResult totalOffset = builder.getIndexAttr(0);
  while (auto viewOp = destTrace.getDefiningOp<ViewLikeOpInterface>()) {
    if (auto subviewOp = dyn_cast<memref::SubViewOp>(viewOp.getOperation())) {
      auto parentType = cast<MemRefType>(subviewOp.getSource().getType());
      SmallVector<int64_t> strides;
      int64_t offset;
      LogicalResult res = parentType.getStridesAndOffset(strides, offset);
      assert(succeeded(res) && "expected strided layout on subview source");
      (void)res;
      SmallVector<OpFoldResult> strideOFRs =
          getAsIndexOpFoldResult(builder.getContext(), strides);
      auto &&[expr, values] = computeLinearIndex(totalOffset, strideOFRs,
                                                 subviewOp.getMixedOffsets());
      totalOffset =
          affine::makeComposedFoldedAffineApply(builder, loc, expr, values);
    } else {
      assert((isa<memref::ExpandShapeOp, memref::CollapseShapeOp>(
                 viewOp.getOperation())) &&
             "unexpected view-like op in dest chain");
    }
    destTrace = viewOp.getViewSource();
  }

  // Only the offset unaligned to the swizzle period affects the XOR
  // computation.
  auto cst = getConstantIntValue(totalOffset);
  Value swizzleBaseOffset;
  int64_t swizzlePeriod = swizzle.getSwizzlePeriod();
  if (!cst || (*cst % swizzlePeriod != 0)) {
    swizzleBaseOffset =
        getValueOrCreateConstantIndexOp(builder, loc, totalOffset);
  }

  // Add the subgroup's base offset within the full allocation.
  Value swizzleInput = srcLinearOffset;
  if (swizzleBaseOffset) {
    swizzleInput =
        arith::AddIOp::create(builder, loc, swizzleBaseOffset, srcLinearOffset);
  }

  // Apply the swizzle (handles access-width alignment internally).
  Value swizzled = getValueOrCreateConstantIndexOp(
      builder, loc, swizzle.swizzleOffset(builder, loc, swizzleInput, dest));

  // Subtract the subgroup base offset.
  if (swizzleBaseOffset) {
    return arith::SubIOp::create(builder, loc, swizzled, swizzleBaseOffset);
  }
  return swizzled;
}

} // namespace mlir::iree_compiler
