blob: a895a97bb8dac4562fb27c7aed28925316fbba32 [file] [edit]
//===- ACCDataDirectivePatterns.cpp - ACC data to LLVM ----------*- 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
//
//===----------------------------------------------------------------------===//
//
// Lowering of the OpenACC data constructs - data, enter data, exit data,
// update and declare - to the data entry points of the OpenACC runtime. Each
// construct becomes one call per point where it establishes or tears down its
// mappings, which carries the argument arrays its operands are packed into,
// the queue its async clause selects and, before them, the wait its wait
// clause asks for. An if clause guards the calls it applies to, and the body
// of a structured construct is left in place of the construct.
//
//===----------------------------------------------------------------------===//
#include "mlir/Conversion/OpenACCToLLVM/ACCDataRuntime.h"
#include "mlir/Conversion/OpenACCToLLVM/ACCToLLVM.h"
#include "mlir/Conversion/OpenACCToLLVM/ACCToLLVMUtils.h"
#include "mlir/Conversion/LLVMCommon/Pattern.h"
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
#include "mlir/Dialect/OpenACC/Analysis/OpenACCSupport.h"
#include "mlir/Dialect/OpenACC/OpenACC.h"
#include "mlir/Dialect/OpenACC/OpenACCUtilsCG.h"
#include "mlir/Interfaces/FunctionInterfaces.h"
using namespace mlir;
using namespace mlir::acc;
/// Emits the call to the mapping entry point \p fn for \p mappingOperands, the
/// data-clause operands of a data construct, paired with their type converted
/// values in \p convertedOperands.
static LogicalResult emitDataRuntimeCall(
Location loc, RuntimeFunction fn, ValueRange mappingOperands,
ValueRange convertedOperands, ACCDataCallKind callKind,
ConversionPatternRewriter &rewriter, OpenACCSupport &accSupport,
Region &globalSymbolRegion, SymbolTable &symbolTable,
const ACCRuntimeCallConfig &config, ArrayRef<Value> trailingArguments) {
ACCDataCallOptions options;
options.callKind = callKind;
return emitACCDataRuntimeCall(
loc, fn, mappingOperands, convertedOperands, rewriter, accSupport,
globalSymbolRegion, symbolTable, config, trailingArguments, options);
}
/// Returns the action that the declare-action recipe holding \p op carries
/// out, which the recipe states by naming itself in the matching slot of its
/// `acc.declare_action` attribute. Returns a null attribute when \p op is not
/// in such a recipe.
static DeclareActionAttr getEnclosingDeclareAction(Operation *op) {
auto func = dyn_cast<FunctionOpInterface>(op);
if (!func)
func = op->getParentOfType<FunctionOpInterface>();
if (!func)
return {};
return func->getAttrOfType<DeclareActionAttr>(getDeclareActionAttrName());
}
static Value getDeclareAsyncQueue(Location loc,
ConversionPatternRewriter &rewriter,
const ACCRuntimeCallConfig &config) {
return LLVM::ConstantOp::create(rewriter, loc, rewriter.getI64Type(),
config.getAsyncSyncRuntimeValue());
}
namespace {
template <typename OpTy>
struct ACCDataDirectivePattern : public ConvertOpToLLVMPattern<OpTy> {
ACCDataDirectivePattern(const LLVMTypeConverter &converter,
OpenACCSupport &accSupport,
Region &globalSymbolRegion, SymbolTable &symbolTable,
const ACCRuntimeCallConfig &config,
DeviceType clauseDeviceType,
PatternBenefit benefit = 1)
: ConvertOpToLLVMPattern<OpTy>(converter, benefit),
accSupport(accSupport), globalSymbolRegion(globalSymbolRegion),
symbolTable(symbolTable), config(config),
clauseDeviceType(clauseDeviceType) {}
/// Returns the queue that the mapping calls of \p op run on.
Value getAsyncQueue(OpTy op, ConversionPatternRewriter &rewriter) const {
return acc::getAsyncQueue(op, clauseDeviceType, rewriter, config);
}
/// Emits the wait that a wait clause on \p op asks for before its mapping
/// calls.
LogicalResult emitWaitClause(OpTy op, Value asyncQueue,
ConversionPatternRewriter &rewriter) const {
return acc::emitWaitClause(op, clauseDeviceType, asyncQueue, rewriter,
accSupport, globalSymbolRegion, symbolTable,
config);
}
/// Emits one mapping call for the data-clause operands of \p op.
LogicalResult emitMappingCall(OpTy op, ValueRange convertedOperands,
Location loc, RuntimeFunction fn,
ACCDataCallKind callKind, Value asyncQueue,
ConversionPatternRewriter &rewriter) const {
return emitDataRuntimeCall(loc, fn, op.getDataClauseOperands(),
convertedOperands, callKind, rewriter,
accSupport, globalSymbolRegion, symbolTable,
config, {asyncQueue});
}
OpenACCSupport &accSupport;
Region &globalSymbolRegion;
SymbolTable &symbolTable;
ACCRuntimeCallConfig config;
DeviceType clauseDeviceType;
};
struct DataOpLowering : public ACCDataDirectivePattern<DataOp> {
using ACCDataDirectivePattern<DataOp>::ACCDataDirectivePattern;
LogicalResult
matchAndRewrite(DataOp op, DataOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
// The mappings are established where the clause operations are, and torn
// down where the exit operations of the construct were.
ValueRange mappingOperands = op.getDataClauseOperands();
Location beginLoc = mappingOperands.empty()
? op.getLoc()
: mappingOperands.front().getLoc();
Location endLoc =
acc::getMappingExitLoc(mappingOperands).value_or(beginLoc);
Value asyncQueue = getAsyncQueue(op, rewriter);
if (failed(emitWaitClause(op, asyncQueue, rewriter)))
return failure();
auto emitBegin = [&]() {
return emitMappingCall(op, adaptor.getDataClauseOperands(), beginLoc,
RuntimeFunction::ACCRTL_tgt_acc_data_begin,
ACCDataCallKind::DataEnter, asyncQueue, rewriter);
};
if (failed(acc::emitGuardedByIfCond(beginLoc, adaptor.getIfCond(), rewriter,
emitBegin)))
return failure();
rewriter.setInsertionPointAfter(op);
auto emitEnd = [&]() {
return emitMappingCall(op, adaptor.getDataClauseOperands(), endLoc,
RuntimeFunction::ACCRTL_tgt_acc_data_end,
ACCDataCallKind::DataExit, asyncQueue, rewriter);
};
if (failed(acc::emitGuardedByIfCond(endLoc, adaptor.getIfCond(), rewriter,
emitEnd)))
return failure();
acc::spliceConstructRegion(op, op.getRegion(), rewriter);
rewriter.eraseOp(op);
return success();
}
};
/// Lowering shared by the directives that map their operands with a single
/// call: enter data, exit data and update.
template <typename OpTy, RuntimeFunction fn, ACCDataCallKind callKind>
struct ACCDataCallLowering : public ACCDataDirectivePattern<OpTy> {
using ACCDataDirectivePattern<OpTy>::ACCDataDirectivePattern;
LogicalResult
matchAndRewrite(OpTy op, typename OpTy::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
Location loc = op.getLoc();
Value asyncQueue = this->getAsyncQueue(op, rewriter);
if (failed(this->emitWaitClause(op, asyncQueue, rewriter)))
return failure();
auto emit = [&]() {
return this->emitMappingCall(op, adaptor.getDataClauseOperands(), loc, fn,
callKind, asyncQueue, rewriter);
};
if (failed(
acc::emitGuardedByIfCond(loc, adaptor.getIfCond(), rewriter, emit)))
return failure();
rewriter.eraseOp(op);
return success();
}
};
using EnterDataOpLowering =
ACCDataCallLowering<EnterDataOp, RuntimeFunction::ACCRTL_tgt_acc_data_enter,
ACCDataCallKind::DataEnter>;
using ExitDataOpLowering =
ACCDataCallLowering<ExitDataOp, RuntimeFunction::ACCRTL_tgt_acc_data_exit,
ACCDataCallKind::DataExit>;
using UpdateOpLowering =
ACCDataCallLowering<UpdateOp, RuntimeFunction::ACCRTL_tgt_acc_data_update,
ACCDataCallKind::DataEnter>;
/// Structured `acc.declare_enter` (token used by a matching exit) maps like
/// `acc.data` enter. In a declare-action recipe the object the directive
/// names is allocated by the host, so only the device mirror of it is
/// allocated here. An unstructured enter registers the objects it names with
/// `__tgt_acc_declare`.
struct DeclareEnterOpLowering : public ConvertOpToLLVMPattern<DeclareEnterOp> {
DeclareEnterOpLowering(const LLVMTypeConverter &converter,
OpenACCSupport &accSupport, Region &globalSymbolRegion,
SymbolTable &symbolTable,
const ACCRuntimeCallConfig &config,
PatternBenefit benefit = 1)
: ConvertOpToLLVMPattern<DeclareEnterOp>(converter, benefit),
accSupport(accSupport), globalSymbolRegion(globalSymbolRegion),
symbolTable(symbolTable), config(config) {}
LogicalResult
matchAndRewrite(DeclareEnterOp op, DeclareEnterOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
Location loc = op.getLoc();
ValueRange mappingOperands = op.getDataClauseOperands();
ValueRange convertedOperands = adaptor.getDataClauseOperands();
Value asyncQueue = getDeclareAsyncQueue(loc, rewriter, config);
LogicalResult emitted = success();
if (getEnclosingDeclareAction(op)) {
emitted = emitDataRuntimeCall(
loc, RuntimeFunction::ACCRTL_tgt_acc_mirror_alloc, mappingOperands,
convertedOperands, ACCDataCallKind::DataEnter, rewriter, accSupport,
globalSymbolRegion, symbolTable, config, {});
} else if (!op.getToken().use_empty()) {
emitted = emitDataRuntimeCall(
loc, RuntimeFunction::ACCRTL_tgt_acc_data_begin, mappingOperands,
convertedOperands, ACCDataCallKind::DataEnter, rewriter, accSupport,
globalSymbolRegion, symbolTable, config, {asyncQueue});
} else {
Value binaryDescriptor =
config.createDeclareBinaryDescriptor(loc, rewriter);
emitted = emitDataRuntimeCall(
loc, RuntimeFunction::ACCRTL_tgt_acc_declare, mappingOperands,
convertedOperands, ACCDataCallKind::DataEnter, rewriter, accSupport,
globalSymbolRegion, symbolTable, config,
{asyncQueue, binaryDescriptor});
}
if (failed(emitted))
return failure();
if (op.getToken().use_empty()) {
rewriter.eraseOp(op);
} else {
// The token only ties a structured enter to its exit. Keep a converted
// placeholder until the exit is erased; it carries no runtime state.
Type tokenTy = getTypeConverter()->convertType(op.getToken().getType());
rewriter.replaceOp(op, LLVM::ZeroOp::create(rewriter, loc, tokenTy));
}
return success();
}
OpenACCSupport &accSupport;
Region &globalSymbolRegion;
SymbolTable &symbolTable;
ACCRuntimeCallConfig config;
};
/// A recipe that runs before the host object is deallocated frees the device
/// mirror of it, as the mapping is torn down once the host storage is gone.
/// Every other `acc.declare_exit` ends the mapping with `__tgt_acc_data_end`.
struct DeclareExitOpLowering : public ConvertOpToLLVMPattern<DeclareExitOp> {
DeclareExitOpLowering(const LLVMTypeConverter &converter,
OpenACCSupport &accSupport, Region &globalSymbolRegion,
SymbolTable &symbolTable,
const ACCRuntimeCallConfig &config,
PatternBenefit benefit = 1)
: ConvertOpToLLVMPattern<DeclareExitOp>(converter, benefit),
accSupport(accSupport), globalSymbolRegion(globalSymbolRegion),
symbolTable(symbolTable), config(config) {}
LogicalResult
matchAndRewrite(DeclareExitOp op, DeclareExitOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
Location loc = op.getLoc();
ValueRange mappingOperands = op.getDataClauseOperands();
ValueRange convertedOperands = adaptor.getDataClauseOperands();
Value asyncQueue = getDeclareAsyncQueue(loc, rewriter, config);
LogicalResult emitted = success();
DeclareActionAttr action = getEnclosingDeclareAction(op);
if (action && action.getPreDealloc()) {
emitted = emitDataRuntimeCall(
loc, RuntimeFunction::ACCRTL_tgt_acc_mirror_dealloc, mappingOperands,
convertedOperands, ACCDataCallKind::DataEnter, rewriter, accSupport,
globalSymbolRegion, symbolTable, config, {});
} else {
emitted = emitDataRuntimeCall(
loc, RuntimeFunction::ACCRTL_tgt_acc_data_end, mappingOperands,
convertedOperands, ACCDataCallKind::DataExit, rewriter, accSupport,
globalSymbolRegion, symbolTable, config, {asyncQueue});
}
if (failed(emitted))
return failure();
rewriter.eraseOp(op);
return success();
}
OpenACCSupport &accSupport;
Region &globalSymbolRegion;
SymbolTable &symbolTable;
ACCRuntimeCallConfig config;
};
/// A data clause operation only describes a mapping; the runtime calls of the
/// construct carry everything it said, so the operation itself goes away once
/// they are in place. Whatever else referred to the mapped object is given the
/// address of that object, which is what the clause result stood for.
template <typename OpTy>
struct DataEntryOpLowering : public ConvertOpToLLVMPattern<OpTy> {
using ConvertOpToLLVMPattern<OpTy>::ConvertOpToLLVMPattern;
using OpAdaptor = typename OpTy::Adaptor;
LogicalResult
matchAndRewrite(OpTy op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
rewriter.replaceOp(op, adaptor.getVar());
return success();
}
};
/// An `acc.getdeviceptr` clause stands for the device address of an object
/// whose mapping was established elsewhere. Where it names the object of a
/// data exit operation, the runtime call of that construct carries the host
/// address and looks the device one up itself, so the clause is replaced by
/// the address of the object as any other data entry clause is. Read as a
/// value, it is the runtime that has to be asked for the address.
struct GetDevicePtrOpLowering
: public ConvertOpToLLVMPattern<acc::GetDevicePtrOp> {
GetDevicePtrOpLowering(const LLVMTypeConverter &converter,
OpenACCSupport &accSupport, Region &globalSymbolRegion,
SymbolTable &symbolTable,
const ACCRuntimeCallConfig &config,
PatternBenefit benefit = 1)
: ConvertOpToLLVMPattern<acc::GetDevicePtrOp>(converter, benefit),
accSupport(accSupport), globalSymbolRegion(globalSymbolRegion),
symbolTable(symbolTable), config(config) {}
/// Returns whether \p user only states the mapping the clause describes,
/// which is what a data exit operation and the construct holding the clause
/// do.
static bool isMappingUse(Operation *user) {
return isa<ACC_DATA_EXIT_OPS, ACC_COMPUTE_AND_DATA_CONSTRUCT_OPS,
acc::KernelEnvironmentOp>(user);
}
LogicalResult
matchAndRewrite(acc::GetDevicePtrOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
bool readAsValue = llvm::any_of(
op->getUsers(), [](Operation *user) { return !isMappingUse(user); });
if (!readAsValue) {
rewriter.replaceOp(op, adaptor.getVar());
return success();
}
// The mapping hands the host address of the object to the runtime call of
// the construct, so the clause cannot stand for the device address at the
// same time.
if (llvm::any_of(op->getUsers(), isMappingUse)) {
(void)accSupport.emitNYI(
op.getLoc(), "device address read from a clause that also states a "
"mapping of the object");
return failure();
}
FailureOr<Value> devicePtr = acc::emitGetDevicePtrCall(
op, adaptor.getVar(), /*ifPresent=*/false, rewriter, globalSymbolRegion,
symbolTable, config);
if (failed(devicePtr))
return failure();
rewriter.replaceOp(op, *devicePtr);
return success();
}
OpenACCSupport &accSupport;
Region &globalSymbolRegion;
SymbolTable &symbolTable;
ACCRuntimeCallConfig config;
};
/// A data exit operation and the bounds of a mapping have no result to stand
/// in for, so they are simply removed with the construct that used them.
template <typename OpTy>
struct EraseDataClauseOp : public ConvertOpToLLVMPattern<OpTy> {
using ConvertOpToLLVMPattern<OpTy>::ConvertOpToLLVMPattern;
using OpAdaptor = typename OpTy::Adaptor;
LogicalResult
matchAndRewrite(OpTy op, OpAdaptor,
ConversionPatternRewriter &rewriter) const override {
rewriter.eraseOp(op);
return success();
}
};
} // namespace
void mlir::configureACCDataDirectiveConversionLegality(
ConversionTarget &target) {
// The map entries themselves stay legal: a construct removes the ones it
// consumes, and the ones describing a construct that is lowered elsewhere
// have to survive this conversion. An `acc.getdeviceptr` read as a value is
// the exception, as the address it stands for is only known to the runtime,
// so leaving it behind would state a mapping that nothing carries out.
target.addIllegalOp<acc::DataOp, acc::EnterDataOp, acc::ExitDataOp,
acc::UpdateOp, acc::DeclareEnterOp, acc::DeclareExitOp,
acc::GetDevicePtrOp>();
}
void mlir::populateACCDataClauseOpPatterns(
LLVMTypeConverter &converter, RewritePatternSet &patterns,
acc::OpenACCSupport &accSupport, Region &globalSymbolRegion,
SymbolTable &symbolTable, const acc::ACCRuntimeCallConfig &config) {
// Bounds describe a mapping rather than data, and they are removed together
// with the clause operations holding them, so their type only has to survive
// this conversion.
converter.addConversion(
[](acc::DataBoundsType type) -> Type { return type; });
patterns.add<
DataEntryOpLowering<acc::MapInfoOp>, DataEntryOpLowering<acc::CopyinOp>,
DataEntryOpLowering<acc::CreateOp>, DataEntryOpLowering<acc::PresentOp>,
DataEntryOpLowering<acc::NoCreateOp>, DataEntryOpLowering<acc::AttachOp>,
DataEntryOpLowering<acc::DevicePtrOp>,
DataEntryOpLowering<acc::UpdateDeviceOp>,
DataEntryOpLowering<acc::PrivateOp>,
DataEntryOpLowering<acc::FirstprivateOp>,
DataEntryOpLowering<acc::FirstprivateMapInitialOp>,
DataEntryOpLowering<acc::DeclareDeviceResidentOp>,
DataEntryOpLowering<acc::DeclareLinkOp>,
EraseDataClauseOp<acc::CopyoutOp>, EraseDataClauseOp<acc::DeleteOp>,
EraseDataClauseOp<acc::DetachOp>, EraseDataClauseOp<acc::UpdateHostOp>,
EraseDataClauseOp<acc::DataBoundsOp>>(converter);
patterns.add<GetDevicePtrOpLowering>(converter, accSupport,
globalSymbolRegion, symbolTable, config);
}
void mlir::populateACCDataDirectivePatterns(
LLVMTypeConverter &converter, RewritePatternSet &patterns,
acc::OpenACCSupport &accSupport, Region &globalSymbolRegion,
SymbolTable &symbolTable, const acc::ACCRuntimeCallConfig &config,
acc::DeviceType clauseDeviceType) {
converter.addConversion([](acc::DeclareTokenType type) -> Type {
return LLVM::LLVMPointerType::get(type.getContext());
});
patterns.add<DataOpLowering, EnterDataOpLowering, ExitDataOpLowering,
UpdateOpLowering>(converter, accSupport, globalSymbolRegion,
symbolTable, config, clauseDeviceType);
patterns.add<DeclareEnterOpLowering, DeclareExitOpLowering>(
converter, accSupport, globalSymbolRegion, symbolTable, config);
}