blob: 3a6cbe0a351091335478d07318c91125caad7e78 [file] [edit]
//===- ACCAtomicPatterns.cpp - ACC atomic to LLVM patterns ------*- C++ -*-===//
//
// Part of the LLVM Project, 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
//
//===----------------------------------------------------------------------===//
//
// Lowers OpenACC atomic operations (read, write, update, capture) to LLVM
// dialect atomicrmw / cmpxchg sequences.
//
//===----------------------------------------------------------------------===//
#include "mlir/Conversion/OpenACCToLLVM/ACCToLLVM.h"
#include "mlir/Conversion/LLVMCommon/Pattern.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Complex/IR/Complex.h"
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/OpenACC/Analysis/OpenACCSupport.h"
#include "mlir/Dialect/OpenACC/OpenACC.h"
#include "mlir/Dialect/OpenACC/OpenACCUtilsType.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/Interfaces/SideEffectInterfaces.h"
#include "llvm/ADT/TypeSwitch.h"
#include "llvm/Support/Debug.h"
#include <cstdint>
#include <optional>
#include <set>
#include <utility>
#define DEBUG_TYPE "acc-atomic-patterns"
using namespace mlir;
using namespace mlir::acc;
namespace {
constexpr uint64_t kBitsInByte = 8;
template <typename AtomicOpTy>
class ACCAtomicOpConversion : public ConvertOpToLLVMPattern<AtomicOpTy> {
using typename ConvertOpToLLVMPattern<AtomicOpTy>::OpAdaptor;
public:
ACCAtomicOpConversion(const LLVMTypeConverter &typeConverter,
OpenACCSupport &accSupport,
const ACCAtomicLoadAddressCallback &getLoadAddress)
: ConvertOpToLLVMPattern<AtomicOpTy>(typeConverter),
accSupport(accSupport), getLoadAddress(getLoadAddress) {}
LogicalResult
matchAndRewrite(AtomicOpTy op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override;
private:
OpenACCSupport &accSupport;
ACCAtomicLoadAddressCallback getLoadAddress;
size_t getComplexStructElementSizeInBits(Type ty) const {
auto structType = dyn_cast<LLVM::LLVMStructType>(ty);
if (!structType || structType.getBody().size() != 2 ||
structType.getBody()[0] != structType.getBody()[1] ||
!structType.getBody()[0].isIntOrFloat())
return 0;
size_t elementSizeInBits = structType.getBody()[0].getIntOrFloatBitWidth();
if (elementSizeInBits == 0 || elementSizeInBits > 64)
llvm_unreachable("unexpected complex type");
return elementSizeInBits;
}
/// Serialize a complex value into an integer.
Value serializeExpr(Value expr, ConversionPatternRewriter &rewriter) const {
size_t elementSizeInBits =
getComplexStructElementSizeInBits(expr.getType());
if (!elementSizeInBits)
return expr;
Location loc = expr.getLoc();
Value firstValue = LLVM::ExtractValueOp::create(rewriter, loc, expr, 0);
Value secondValue = LLVM::ExtractValueOp::create(rewriter, loc, expr, 1);
MLIRContext *context = rewriter.getContext();
Type intCastTy = IntegerType::get(context, elementSizeInBits);
Type intTy = IntegerType::get(context, elementSizeInBits * 2);
firstValue = LLVM::BitcastOp::create(rewriter, loc, intCastTy, firstValue);
secondValue =
LLVM::BitcastOp::create(rewriter, loc, intCastTy, secondValue);
firstValue = LLVM::ZExtOp::create(rewriter, loc, intTy, firstValue);
secondValue = LLVM::ZExtOp::create(rewriter, loc, intTy, secondValue);
Value shlVal =
LLVM::ConstantOp::create(rewriter, loc, intTy, elementSizeInBits);
Value result = LLVM::ShlOp::create(rewriter, loc, secondValue, shlVal);
return LLVM::OrOp::create(rewriter, loc, result, firstValue);
}
/// Deserialize an integer into a complex value.
Value deserializeExpr(Value expr, Type origTy,
ConversionPatternRewriter &rewriter) const {
size_t elementSizeInBits = getComplexStructElementSizeInBits(origTy);
if (!elementSizeInBits)
return expr;
auto intTy = dyn_cast<IntegerType>(expr.getType());
if (!intTy || intTy.getWidth() != elementSizeInBits * 2)
return expr;
Location loc = expr.getLoc();
MLIRContext *context = rewriter.getContext();
Type elemTy = IntegerType::get(context, elementSizeInBits);
Value low = LLVM::TruncOp::create(rewriter, loc, elemTy, expr);
Value shiftAmount =
LLVM::ConstantOp::create(rewriter, loc, intTy, elementSizeInBits);
Value highFull = LLVM::LShrOp::create(rewriter, loc, expr, shiftAmount);
Value high = LLVM::TruncOp::create(rewriter, loc, elemTy, highFull);
Type origElemTy = cast<LLVM::LLVMStructType>(origTy).getBody()[0];
if (origElemTy != elemTy) {
low = LLVM::BitcastOp::create(rewriter, loc, origElemTy, low);
high = LLVM::BitcastOp::create(rewriter, loc, origElemTy, high);
}
Value undefStruct = LLVM::UndefOp::create(rewriter, loc, origTy);
Value structWithLow = LLVM::InsertValueOp::create(
rewriter, loc, origTy, undefStruct, low, ArrayRef<int64_t>{0});
return LLVM::InsertValueOp::create(rewriter, loc, origTy, structWithLow,
high, ArrayRef<int64_t>{1});
}
uint64_t getAtomicSizeInBytes(Type originalTy, Type convertedTy,
ModuleOp module) const {
std::optional<TypeSizeAndAlignment> sizeAndAlignment =
acc::getTypeSizeAndAlignment(originalTy, module, &accSupport);
if (!sizeAndAlignment)
sizeAndAlignment =
acc::getTypeSizeAndAlignment(convertedTy, module, &accSupport);
assert(sizeAndAlignment && "atomic type size is not computable");
return sizeAndAlignment->first.getFixedValue();
}
Type getAtomicType(Type originalTy, Type convertedTy, ModuleOp module) const {
if (convertedTy.isIntOrFloat())
return convertedTy;
return IntegerType::get(
convertedTy.getContext(),
getAtomicSizeInBytes(originalTy, convertedTy, module) * kBitsInByte);
}
Type getReferencedElementType(Value ref, ModuleOp module = nullptr) const {
auto ptr = dyn_cast<PointerLikeType>(ref.getType());
if (!ptr)
llvm_unreachable("unexpected type");
Type elementTy = ptr.getElementType();
Type convertedTy = this->getTypeConverter()->convertType(elementTy);
if (module)
return getAtomicType(elementTy, convertedTy, module);
return convertedTy;
}
/// Memrefs are converted to descriptors rather than bare pointers, so the
/// element pointer must be recomputed from the descriptor.
Value getAtomicPointer(Value originalRef, Value convertedPtr, Location loc,
ConversionPatternRewriter &rewriter) const {
auto memrefTy = dyn_cast<MemRefType>(originalRef.getType());
if (!memrefTy)
return convertedPtr;
// Extract aligned base pointer (index 1) and offset (index 2).
Value alignedPtr =
LLVM::ExtractValueOp::create(rewriter, loc, convertedPtr, 1);
Value offset = LLVM::ExtractValueOp::create(rewriter, loc, convertedPtr, 2);
Type elemPtrType = LLVM::LLVMPointerType::get(rewriter.getContext());
return LLVM::GEPOp::create(
rewriter, loc, elemPtrType,
this->getTypeConverter()->convertType(memrefTy.getElementType()),
alignedPtr, offset);
}
Block *constructCmpxchgLoop(Value ptr, Type type, Value expr,
ConversionPatternRewriter &rewriter) const;
/// Emit a single atomicrmw for a `x = x <binop> expr` capture, or fail so
/// the caller falls back to the cmpxchg loop.
LogicalResult
tryEmitCaptureAtomicRMW(AtomicCaptureOp capture, AtomicUpdateOp update,
AtomicReadOp read,
ConversionPatternRewriter &rewriter) const;
Value genUpdateCmpxchgLoop(AtomicUpdateOp update,
ConversionPatternRewriter &rewriter) const;
};
template <>
LogicalResult ACCAtomicOpConversion<AtomicReadOp>::matchAndRewrite(
AtomicReadOp read, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const {
Location loc = read.getLoc();
Value xRef = read.getX();
Value xPtr = getAtomicPointer(xRef, adaptor.getX(), loc, rewriter);
ModuleOp mod = read->getParentOfType<ModuleOp>();
Type xType = getReferencedElementType(xRef, mod);
auto ordering = LLVM::AtomicOrdering::monotonic;
Value storeVal;
if (xType.isSignlessInteger()) {
Value zero = LLVM::ConstantOp::create(rewriter, loc, xType, 0);
storeVal = LLVM::AtomicRMWOp::create(rewriter, loc, LLVM::AtomicBinOp::_or,
xPtr, zero, ordering);
} else {
unsigned bitWidth = xType.getIntOrFloatBitWidth();
Type intType = IntegerType::get(rewriter.getContext(), bitWidth);
Value zero = LLVM::ConstantOp::create(rewriter, loc, intType, 0);
Value intVal = LLVM::AtomicRMWOp::create(
rewriter, loc, LLVM::AtomicBinOp::_or, xPtr, zero, ordering);
storeVal = LLVM::BitcastOp::create(rewriter, loc, xType, intVal);
}
Value vRef = read.getV();
Value vPtr = getAtomicPointer(vRef, adaptor.getV(), loc, rewriter);
Type vType = getReferencedElementType(vRef, mod);
if (xType != vType) {
// Convert `x` if the types do not match.
auto vPtrType = cast<PointerLikeType>(vRef.getType());
storeVal = vPtrType.genCast(rewriter, loc, storeVal, vType);
if (!storeVal) {
return rewriter.notifyMatchFailure(
read, "failed to convert the loaded value to the destination type");
}
}
auto storeOp = LLVM::StoreOp::create(rewriter, loc, storeVal, vPtr);
rewriter.replaceOp(read, storeOp);
return success();
}
template <>
LogicalResult ACCAtomicOpConversion<AtomicWriteOp>::matchAndRewrite(
AtomicWriteOp write, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const {
Location loc = write.getLoc();
Value expr = serializeExpr(adaptor.getExpr(), rewriter);
Value xRef = write.getX();
Value xPtr = getAtomicPointer(xRef, adaptor.getX(), loc, rewriter);
ModuleOp mod = write->getParentOfType<ModuleOp>();
Type xType = getReferencedElementType(xRef, mod);
auto ordering = LLVM::AtomicOrdering::monotonic;
if (!xType.isSignlessInteger()) {
unsigned bitWidth = xType.getIntOrFloatBitWidth();
Type intType = IntegerType::get(rewriter.getContext(), bitWidth);
expr = LLVM::BitcastOp::create(rewriter, loc, intType, expr);
}
LLVM::AtomicRMWOp::create(rewriter, loc, LLVM::AtomicBinOp::xchg, xPtr, expr,
ordering);
rewriter.eraseOp(write);
return success();
}
// Generate a cmpxchg loop based on the GenericAtomicRMWOpLowering algorithm.
template <typename AtomicOpTy>
Block *ACCAtomicOpConversion<AtomicOpTy>::constructCmpxchgLoop(
Value ptr, Type type, Value expr,
ConversionPatternRewriter &rewriter) const {
// Split the block into initial, loop, and ending parts.
Location loc = rewriter.getInsertionPoint()->getLoc();
Block *initBlock = rewriter.getInsertionBlock();
Block *loopBlock =
rewriter.splitBlock(initBlock, rewriter.getInsertionPoint());
loopBlock->addArgument(type, loc);
Block *endBlock =
rewriter.splitBlock(loopBlock, rewriter.getInsertionPoint());
// Compute the loaded value and branch to the loop block.
rewriter.setInsertionPointToEnd(initBlock);
Value init = LLVM::LoadOp::create(rewriter, loc, type, ptr);
LLVM::BrOp::create(rewriter, loc, init, loopBlock);
// Prepare the body of the loop block.
rewriter.setInsertionPointToStart(loopBlock);
Value loopArgument = loopBlock->getArgument(0);
Value result = serializeExpr(rewriter.getRemappedValue(expr), rewriter);
ModuleOp mod = initBlock->getParent()->getParentOfType<ModuleOp>();
Type convertedExprType =
this->getTypeConverter()->convertType(expr.getType());
Type exprType = getAtomicType(expr.getType(), convertedExprType, mod);
// Cast to an integer type.
if (!exprType.isSignlessInteger()) {
Type tmpType = IntegerType::get(
rewriter.getContext(),
getAtomicSizeInBytes(expr.getType(), convertedExprType, mod) *
kBitsInByte);
result = LLVM::BitcastOp::create(rewriter, loc, tmpType, result);
loopArgument =
LLVM::BitcastOp::create(rewriter, loc, tmpType, loopArgument);
}
// Prepare the epilog of the loop block.
// Append the cmpxchg op to the end of the loop block.
auto successOrdering = LLVM::AtomicOrdering::acq_rel;
auto failureOrdering = LLVM::AtomicOrdering::monotonic;
auto cmpxchg =
LLVM::AtomicCmpXchgOp::create(rewriter, loc, ptr, loopArgument, result,
successOrdering, failureOrdering);
// Extract the %new_loaded and %ok values from the pair.
Value newLoaded = LLVM::ExtractValueOp::create(rewriter, loc, cmpxchg, 0);
Value ok = LLVM::ExtractValueOp::create(rewriter, loc, cmpxchg, 1);
// Cast back to the original type.
if (!exprType.isSignlessInteger())
newLoaded = LLVM::BitcastOp::create(rewriter, loc, exprType, newLoaded);
// Conditionally branch to the end or back to the loop depending on %ok.
LLVM::CondBrOp::create(rewriter, loc, ok, endBlock, ArrayRef<Value>(),
loopBlock, newLoaded);
return loopBlock;
}
static Value skipUnrealizedConversionOp(Value v) {
if (auto convOp =
dyn_cast_or_null<UnrealizedConversionCastOp>(v.getDefiningOp()))
return skipUnrealizedConversionOp(convOp.getOperand(0));
return v;
}
/// Obtain the foremost value of addr to indicate the origin of the storage.
static Value getBaseStorage(Value addr, ConversionPatternRewriter &rewriter) {
Operation *op = skipUnrealizedConversionOp(addr).getDefiningOp();
if (auto gepOp = dyn_cast_or_null<LLVM::GEPOp>(op)) {
addr = gepOp.getBase();
op = addr.getDefiningOp();
}
if (auto extractOp = dyn_cast_or_null<LLVM::ExtractValueOp>(op)) {
// addr is in a struct. Get the inserted value corresponding to the
// extraction.
ArrayRef extractingPosition = extractOp.getPosition();
Value container = skipUnrealizedConversionOp(extractOp.getContainer());
Value remappedContainer = rewriter.getRemappedValue(container);
op = remappedContainer ? remappedContainer.getDefiningOp() : nullptr;
bool inserted = false;
while (auto insertValueOp = dyn_cast_or_null<LLVM::InsertValueOp>(op)) {
ArrayRef insertingPosition = insertValueOp.getPosition();
if (insertingPosition == extractingPosition) {
addr = insertValueOp.getValue();
inserted = true;
break;
}
op = insertValueOp.getContainer().getDefiningOp();
}
// The aggregate holding addr is not built by insertions, so the value it
// is converted from identifies the storage.
if (!inserted)
addr = container;
}
// op might be alloca already, or addr might be an argument.
return skipUnrealizedConversionOp(addr);
}
/// Include flow dependency (v -> expr) in the generated loop of
/// `{ atomic.read, atomic.write/update }`.
static void moveDependency(Value vRef, Value vPtr, Value expr,
Value loopArgument, Operation &loopHead,
ConversionPatternRewriter &rewriter,
const ACCAtomicLoadAddressCallback &getLoadAddress) {
Value vStorage = getBaseStorage(vPtr, rewriter);
// A dependency may be reached before it is itself converted, in which case
// its address is still expressed in terms of `vRef`.
Value vStorageRef = skipUnrealizedConversionOp(vRef);
LLVM_DEBUG({
llvm::dbgs() << "[acc-atomic] moveDependency\n";
llvm::dbgs() << " vPtr = " << vPtr << "\n";
llvm::dbgs() << " vStorage = " << vStorage << "\n";
llvm::dbgs() << " vStorageRef = " << vStorageRef << "\n";
llvm::dbgs() << " expr = " << expr << "\n";
});
llvm::DenseMap<Value, Value> remappedToOriginal;
std::set<std::pair<Operation *, std::set<Operation *>>> worklist;
std::set<Operation *> included;
Value mappedExpr = rewriter.getRemappedValue(expr);
remappedToOriginal[mappedExpr] = expr;
if (Operation *exprDef = mappedExpr.getDefiningOp())
worklist.insert(std::pair{exprDef, std::set<Operation *>{}});
while (!worklist.empty()) {
auto [dep, post] = worklist.extract(worklist.begin()).value();
if (!dep)
continue;
LLVM_DEBUG(llvm::dbgs() << " visit dep = " << *dep << "\n");
if (vPtr.getDefiningOp() &&
!vPtr.getDefiningOp()->getParentOp()->isAncestor(dep)) {
// Outside the parental region. No load found in this flow.
LLVM_DEBUG(llvm::dbgs() << " -> skipped: not in parental region\n");
continue;
}
Value addr;
if (auto load = dyn_cast<LLVM::LoadOp>(dep)) {
addr = load.getAddr();
} else if (auto load = dyn_cast<memref::LoadOp>(dep)) {
addr = load.getMemref();
} else if (getLoadAddress) {
addr = getLoadAddress(dep);
}
if (addr) {
Value baseStorage = getBaseStorage(addr, rewriter);
LLVM_DEBUG({
llvm::dbgs() << " addr = " << addr << "\n";
llvm::dbgs() << " baseStorage = " << baseStorage << "\n";
llvm::dbgs() << " remapped = "
<< rewriter.getRemappedValue(baseStorage) << "\n";
llvm::dbgs() << " matchRef=" << (baseStorage == vStorageRef)
<< " matchConv="
<< (rewriter.getRemappedValue(baseStorage) == vStorage)
<< "\n";
});
if (baseStorage == vStorageRef ||
rewriter.getRemappedValue(baseStorage) == vStorage) {
// Found the load of `v`. Include this flow in dependency.
Value load = dep->getResult(0);
auto replaceUses = [&](Operation *op) {
rewriter.modifyOpInPlace(op, [&] {
op->replaceUsesOfWith(remappedToOriginal[load], loopArgument);
});
};
for (Operation *p : post)
replaceUses(p);
replaceUses(&loopHead);
included.insert(post.begin(), post.end());
}
continue;
}
post.insert(dep);
for (Value operand : dep->getOperands()) {
Value mappedOperand = rewriter.getRemappedValue(operand);
if (auto *d = mappedOperand.getDefiningOp()) {
if (dep == d) {
if (auto *op = operand.getDefiningOp()) {
remappedToOriginal[op->getResult(0)] = operand;
worklist.insert(std::pair{op, post});
}
} else {
remappedToOriginal[mappedOperand] = operand;
worklist.insert(std::pair{d, post});
}
}
}
}
// Include dependency.
SmallVector<Operation *> includedInOrder;
vPtr.getParentRegion()->walk([&](Operation *op) {
if (included.find(op) != included.end())
includedInOrder.push_back(op);
});
for (Operation *d : includedInOrder)
d->moveBefore(&loopHead);
}
/// Generate a cmpxchg loop for update and return a stored value.
template <typename AtomicOpTy>
Value ACCAtomicOpConversion<AtomicOpTy>::genUpdateCmpxchgLoop(
AtomicUpdateOp update, ConversionPatternRewriter &rewriter) const {
Location loc = update.getLoc();
Value xRef = update.getX();
Value xPtr =
getAtomicPointer(xRef, rewriter.getRemappedValue(xRef), loc, rewriter);
ModuleOp mod = update->getParentOfType<ModuleOp>();
Type xType = getReferencedElementType(xRef, mod);
Type xTypeOrig = getReferencedElementType(xRef);
Block &updateBlock = update.getRegion().front();
Value updateArgument = updateBlock.getArgument(0);
Operation *terminator = updateBlock.getTerminator();
Value expr = terminator->getOperand(0);
Block *loopBlock = constructCmpxchgLoop(xPtr, xType, expr, rewriter);
Value loopArgument = loopBlock->getArgument(0);
Operation &loopHead = loopBlock->front();
rewriter.setInsertionPointToStart(loopBlock);
loopArgument = deserializeExpr(loopArgument, xTypeOrig, rewriter);
// Move in and out flow dependency (x -> expr). Some computation might be
// outside atomic regions.
moveDependency(xRef, xPtr, expr, loopArgument, loopHead, rewriter,
getLoadAddress);
// Move out the residue.
rewriter.replaceAllUsesWith(cast<BlockArgument>(updateArgument),
{loopArgument});
updateBlock.walk([&](Operation *op) {
if (!op->hasTrait<OpTrait::IsTerminator>())
rewriter.moveOpBefore(op, &loopHead);
});
if (auto cmpxchg = dyn_cast<LLVM::AtomicCmpXchgOp>(loopHead))
return cmpxchg.getVal();
if (auto bitcast = dyn_cast<LLVM::BitcastOp>(loopHead))
// Handling a non-integer type.
return bitcast.getArg();
if (auto extract = dyn_cast<LLVM::ExtractValueOp>(loopHead))
// Handling a complex type.
return extract.getContainer();
llvm_unreachable("invalid cmpxchg loop");
}
static std::optional<LLVM::AtomicBinOp> getAtomicBinOp(Operation *op,
bool updateIsLhs) {
return TypeSwitch<Operation *, std::optional<LLVM::AtomicBinOp>>(op)
.Case<arith::AddFOp>([](auto) { return LLVM::AtomicBinOp::fadd; })
.Case<arith::AddIOp>([](auto) { return LLVM::AtomicBinOp::add; })
.Case<arith::SubFOp>(
[updateIsLhs](auto) -> std::optional<LLVM::AtomicBinOp> {
// atomicrmw fsub is always `*ptr = *ptr - val`.
if (!updateIsLhs)
return std::nullopt;
return LLVM::AtomicBinOp::fsub;
})
.Case<arith::SubIOp>(
[updateIsLhs](auto) -> std::optional<LLVM::AtomicBinOp> {
// atomicrmw sub is always `*ptr = *ptr - val`.
if (!updateIsLhs)
return std::nullopt;
return LLVM::AtomicBinOp::sub;
})
.Case<arith::AndIOp>([](auto) { return LLVM::AtomicBinOp::_and; })
.Case<arith::OrIOp>([](auto) { return LLVM::AtomicBinOp::_or; })
.Case<arith::XOrIOp>([](auto) { return LLVM::AtomicBinOp::_xor; })
.Case<arith::MaxSIOp>([](auto) { return LLVM::AtomicBinOp::max; })
.Case<arith::MinSIOp>([](auto) { return LLVM::AtomicBinOp::min; })
.Case<arith::MaxUIOp>([](auto) { return LLVM::AtomicBinOp::umax; })
.Case<arith::MinUIOp>([](auto) { return LLVM::AtomicBinOp::umin; })
.Case<arith::MaximumFOp>([](auto) { return LLVM::AtomicBinOp::fmaximum; })
.Case<arith::MinimumFOp>([](auto) { return LLVM::AtomicBinOp::fminimum; })
.Case<arith::MaxNumFOp>(
[](auto) { return LLVM::AtomicBinOp::fmaximumnum; })
.Case<arith::MinNumFOp>(
[](auto) { return LLVM::AtomicBinOp::fminimumnum; })
.Default([](Operation *) { return std::nullopt; });
}
/// Match an update region computing `x = x <binop> expr`, and return the
/// atomicrmw kind with the binary operation. Shared by the update and capture
/// conversions so the same region cannot take atomicrmw in one and the cmpxchg
/// loop in the other.
static std::optional<std::pair<LLVM::AtomicBinOp, Operation *>>
matchAtomicBinOpUpdate(AtomicUpdateOp update) {
Block &block = update.getRegion().front();
Value arg = block.getArgument(0);
Operation *yield = block.getTerminator();
if (!yield || yield->getNumOperands() != 1)
return std::nullopt;
Operation *binOp = yield->getOperand(0).getDefiningOp();
if (!binOp || binOp->getBlock() != &block || binOp->getNumOperands() != 2 ||
binOp->getNumResults() != 1)
return std::nullopt;
// The updated value has to feed the binop and nothing else.
if (!arg.hasOneUse() || arg.use_begin()->getOwner() != binOp)
return std::nullopt;
bool updateIsLhs = binOp->getOperand(0) == arg;
std::optional<LLVM::AtomicBinOp> kind = getAtomicBinOp(binOp, updateIsLhs);
if (!kind)
return std::nullopt;
return std::make_pair(*kind, binOp);
}
/// Generate llvm.atomicrmw or an llvm.cmpxchg loop.
template <>
LogicalResult ACCAtomicOpConversion<AtomicUpdateOp>::matchAndRewrite(
AtomicUpdateOp update, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const {
Block &updateBlock = update.getRegion().front();
Value updateArgument = updateBlock.getArgument(0);
// Collect operations that depend on the update argument.
std::set<Operation *> dependents;
SmallVector<Value> worklist;
worklist.push_back(updateArgument);
while (!worklist.empty()) {
Value value = worklist.back();
worklist.pop_back();
for (OpOperand &use : value.getUses()) {
Operation *useOp = use.getOwner();
dependents.insert(useOp);
if (useOp->getNumResults() == 1)
worklist.push_back(useOp->getResult(0));
}
}
// Move independent operations out of the update block.
SmallVector<Operation *> independent;
for (Operation &op : updateBlock.getOperations()) {
if (dependents.find(&op) == dependents.end()) {
if (op.hasTrait<OpTrait::IsTerminator>())
llvm_unreachable("invalid update operation");
independent.push_back(&op);
}
}
for (Operation *op : independent)
rewriter.moveOpBefore(op, update);
// Map arith op to atomicrmw kind.
//
// In the current version of LLVM, these are the equivalences for float
// min/max across the different instructions intrinsics and operations -
// confusingly they have slightly different and not very descriptive names.
//
// | MLIR | atomicrmw inst | llvm intrinsic |
// |------------+----------------+-------------------|
// | - | fmax | llvm.maxnum.* |
// | MaximumFOp | fmaximum | llvm.maximum.* |
// | MaxNumFOp | fmaximumnum | llvm.maximumnum.* |
//
// Sources:
// https://llvm.org/docs/LangRef.html#id236
// https://llvm.org/docs/LangRef.html#floating-point-min-max-intrinsics-comparison
// https://mlir.llvm.org/docs/Dialects/ArithOps/#arithmaximumf-arithmaximumfop
// https://mlir.llvm.org/docs/Dialects/ArithOps/#arithmaxnumf-arithmaxnumfop
// Select the kind and the val of atomicrmw.
std::optional<Value> val = std::nullopt;
std::optional<LLVM::AtomicBinOp> kind = std::nullopt;
if (auto matched = matchAtomicBinOpUpdate(update)) {
Operation *binOp = matched->second;
bool updateIsLhs = binOp->getOperand(0) == updateArgument;
kind = matched->first;
val = binOp->getOperand(updateIsLhs ? 1 : 0);
}
// Per-component atomicrmw info for complex type decomposition.
// Decomposed complex ops (complex.re/im + arith binop + complex.create)
// produce per-component binary ops that each need a separate atomicrmw.
struct ComponentAtomic {
LLVM::AtomicBinOp kind;
Value val;
int32_t fieldIdx;
};
SmallVector<ComponentAtomic, 2> componentAtomics;
if (!val || !kind) {
Type convertedArgTy =
this->getTypeConverter()->convertType(updateArgument.getType());
if (auto structTy = dyn_cast<LLVM::LLVMStructType>(convertedArgTy)) {
if (structTy.getBody().size() == 2 &&
structTy.getBody()[0] == structTy.getBody()[1] &&
structTy.getBody()[0].isIntOrFloat() &&
structTy.getBody()[0].getIntOrFloatBitWidth() > 32) {
for (Operation &op : updateBlock.getOperations()) {
if (op.hasTrait<OpTrait::IsTerminator>() || op.getNumOperands() < 2)
continue;
int32_t fieldIdx = -1;
Value externalVal = nullptr;
bool updateIsLhs = false;
for (unsigned i = 0; i < 2; ++i) {
Value operand = op.getOperand(i);
if (auto reOp = operand.getDefiningOp<complex::ReOp>()) {
if (reOp.getOperand() == updateArgument) {
fieldIdx = 0;
externalVal = op.getOperand(1 - i);
updateIsLhs = i == 0;
}
} else if (auto imOp = operand.getDefiningOp<complex::ImOp>()) {
if (imOp.getOperand() == updateArgument) {
fieldIdx = 1;
externalVal = op.getOperand(1 - i);
updateIsLhs = i == 0;
}
}
}
if (fieldIdx < 0)
continue;
auto componentKind = getAtomicBinOp(&op, updateIsLhs);
if (!componentKind)
continue;
componentAtomics.push_back({*componentKind, externalVal, fieldIdx});
}
}
}
}
Location loc = update.getLoc();
Value xPtr = getAtomicPointer(update.getX(), adaptor.getX(), loc, rewriter);
// Require distinct real/imag lanes; duplicate fieldIdx values must fall back
// to cmpxchg rather than emitting two atomicrmw ops on the same component.
bool hasDistinctComplexLanes = false;
if (componentAtomics.size() == 2) {
unsigned lanes = 0;
for (const ComponentAtomic &ca : componentAtomics) {
if (ca.fieldIdx == 0 || ca.fieldIdx == 1)
lanes |= 1u << ca.fieldIdx;
}
hasDistinctComplexLanes = lanes == 0b11;
}
if (val && kind) {
auto ordering = LLVM::AtomicOrdering::monotonic;
LLVM::AtomicRMWOp::create(rewriter, loc, *kind, xPtr,
rewriter.getRemappedValue(*val), ordering);
} else if (hasDistinctComplexLanes) {
auto structTy = cast<LLVM::LLVMStructType>(
this->getTypeConverter()->convertType(updateArgument.getType()));
Type ptrType = LLVM::LLVMPointerType::get(rewriter.getContext());
auto ordering = LLVM::AtomicOrdering::monotonic;
for (ComponentAtomic &ca : componentAtomics) {
Value elemPtr =
LLVM::GEPOp::create(rewriter, loc, ptrType, structTy, xPtr,
ArrayRef<LLVM::GEPArg>{0, ca.fieldIdx});
LLVM::AtomicRMWOp::create(rewriter, loc, ca.kind, elemPtr,
rewriter.getRemappedValue(ca.val), ordering);
}
} else {
// Fallback to the llvm.cmpxchg loop generation.
genUpdateCmpxchgLoop(update, rewriter);
}
rewriter.eraseOp(update);
return success();
}
/// The cmpxchg loop re-points reads of `v`/`x` inside `expr` at the atomically
/// loaded value (see moveDependency). An atomicrmw cannot do that, so any
/// `expr` that loads from memory keeps the loop: the load may be the captured
/// value itself, and the address is not reliably comparable here.
static bool exprReadsMemory(Value expr) {
SmallVector<Value> worklist{expr};
llvm::DenseSet<Operation *> seen;
while (!worklist.empty()) {
Operation *def = worklist.pop_back_val().getDefiningOp();
if (!def || !seen.insert(def).second)
continue;
if (!isMemoryEffectFree(def))
return true;
worklist.append(def->getOperands().begin(), def->getOperands().end());
}
return false;
}
/// Emit a single atomicrmw for a `x = x <binop> expr` capture.
template <typename AtomicOpTy>
LogicalResult ACCAtomicOpConversion<AtomicOpTy>::tryEmitCaptureAtomicRMW(
AtomicCaptureOp capture, AtomicUpdateOp update, AtomicReadOp read,
ConversionPatternRewriter &rewriter) const {
if (read.getX() != update.getX())
return failure();
auto matched = matchAtomicBinOpUpdate(update);
if (!matched)
return failure();
auto [kind, binOp] = *matched;
Value arg = update.getRegion().front().getArgument(0);
bool updateIsLhs = binOp->getOperand(0) == arg;
Value expr = binOp->getOperand(updateIsLhs ? 1 : 0);
// Keep serialized and aggregate types on the cmpxchg path.
Type argTy = arg.getType();
if (!argTy.isIntOrFloat() ||
this->getTypeConverter()->convertType(argTy) != argTy)
return failure();
// The operand must already be available, and must not be the captured value.
Operation *exprDef = expr.getDefiningOp();
if (exprDef && exprDef->getBlock() == &update.getRegion().front())
return failure();
if (exprReadsMemory(expr))
return failure();
Location loc = capture.getLoc();
Value xRef = update.getX();
Value vRef = read.getV();
Value xPtr =
getAtomicPointer(xRef, rewriter.getRemappedValue(xRef), loc, rewriter);
Value vPtr =
getAtomicPointer(vRef, rewriter.getRemappedValue(vRef), loc, rewriter);
rewriter.setInsertionPoint(capture);
auto rmw = LLVM::AtomicRMWOp::create(rewriter, loc, kind, xPtr,
rewriter.getRemappedValue(expr),
LLVM::AtomicOrdering::monotonic);
// atomicrmw yields the old value; `{update, read}` captures the new one.
Value captured = rmw.getRes();
if (capture.getFirstOp() == update.getOperation()) {
rewriter.moveOpAfter(binOp, rmw);
binOp->replaceUsesOfWith(arg, captured);
captured = binOp->getResult(0);
}
rewriter.replaceOpWithNewOp<LLVM::StoreOp>(capture, captured, vPtr);
return success();
}
/// Generate an llvm.cmpxchg loop.
template <>
LogicalResult ACCAtomicOpConversion<AtomicCaptureOp>::matchAndRewrite(
AtomicCaptureOp capture, OpAdaptor /*adaptor*/,
ConversionPatternRewriter &rewriter) const {
Operation *firstOp = capture.getFirstOp();
Operation *secondOp = capture.getSecondOp();
Value vPtr = nullptr;
Value storeVal = nullptr;
// A single `x = x <binop> expr` capture becomes one atomicrmw. The cmpxchg
// loop below serializes retries and collapses under contention.
if (AtomicUpdateOp update = capture.getAtomicUpdateOp())
if (AtomicReadOp read = capture.getAtomicReadOp())
if (succeeded(tryEmitCaptureAtomicRMW(capture, update, read, rewriter)))
return success();
if (auto firstReadStmt = dyn_cast<AtomicReadOp>(firstOp)) {
Location loc = capture.getLoc();
Value xRef = firstReadStmt.getX();
Value xPtr =
getAtomicPointer(xRef, rewriter.getRemappedValue(xRef), loc, rewriter);
ModuleOp mod = capture->getParentOfType<ModuleOp>();
Type xType = getReferencedElementType(xRef, mod);
Type xTypeOrig = getReferencedElementType(xRef);
Value vRef = firstReadStmt.getV();
vPtr =
getAtomicPointer(vRef, rewriter.getRemappedValue(vRef), loc, rewriter);
Value expr = nullptr;
if (auto secondWriteStmt = dyn_cast<AtomicWriteOp>(secondOp)) {
// 1. `{ atomic.read, atomic.write }` pattern
expr = secondWriteStmt.getExpr();
} else if (auto secondUpdateStmt = dyn_cast<AtomicUpdateOp>(secondOp)) {
// 2. `{ atomic.read, atomic.update }` pattern
Block &updateBlock = secondUpdateStmt.getRegion().front();
Operation *terminator = updateBlock.getTerminator();
expr = terminator->getOperand(0);
}
Block *loopBlock = constructCmpxchgLoop(xPtr, xType, expr, rewriter);
Value loopArgument = loopBlock->getArgument(0);
Operation &loopHead = loopBlock->front();
auto condBr = cast<LLVM::CondBrOp>(loopBlock->back());
storeVal = condBr.getFalseDestOperands()[0];
rewriter.setInsertionPointToStart(loopBlock);
loopArgument = deserializeExpr(loopArgument, xTypeOrig, rewriter);
// Include flow dependency (v -> expr).
if (auto secondUpdateStmt = dyn_cast<AtomicUpdateOp>(secondOp)) {
Block &updateBlock = secondUpdateStmt.getRegion().front();
Value updateArgument = updateBlock.getArgument(0);
updateArgument.replaceAllUsesWith(loopArgument);
updateBlock.walk([&](Operation *op) {
if (!op->hasTrait<OpTrait::IsTerminator>())
rewriter.moveOpBefore(op, &loopHead);
});
}
moveDependency(vRef, vPtr, expr, loopArgument, loopHead, rewriter,
getLoadAddress);
} else if (auto firstUpdateStmt = dyn_cast<AtomicUpdateOp>(firstOp)) {
if (auto secondReadStmt = dyn_cast<AtomicReadOp>(secondOp)) {
// 3. `{ atomic.update, atomic.read }` pattern
storeVal = genUpdateCmpxchgLoop(firstUpdateStmt, rewriter);
Value vRef = secondReadStmt.getV();
vPtr = getAtomicPointer(vRef, rewriter.getRemappedValue(vRef),
capture.getLoc(), rewriter);
}
}
// Generate `v = x`.
rewriter.setInsertionPoint(capture);
rewriter.replaceOpWithNewOp<LLVM::StoreOp>(capture, storeVal, vPtr);
return success();
}
} // namespace
namespace mlir {
void configureACCAtomicConversionLegality(ConversionTarget &target) {
target.addIllegalOp<AtomicReadOp, AtomicWriteOp, AtomicUpdateOp,
AtomicCaptureOp>();
}
void populateACCAtomicPatterns(const LLVMTypeConverter &converter,
RewritePatternSet &patterns,
acc::OpenACCSupport &accSupport,
ACCAtomicLoadAddressCallback getLoadAddress) {
patterns.add<ACCAtomicOpConversion<AtomicReadOp>,
ACCAtomicOpConversion<AtomicWriteOp>,
ACCAtomicOpConversion<AtomicUpdateOp>,
ACCAtomicOpConversion<AtomicCaptureOp>>(converter, accSupport,
getLoadAddress);
}
} // namespace mlir