| //===- 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 |