blob: 47f4acd4dd1dba80af2aa1fd5cc8bf46772c0452 [file]
//===-- CUFAddConstructor.cpp ---------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
#include "flang/Optimizer/Builder/BoxValue.h"
#include "flang/Optimizer/Builder/CUFCommon.h"
#include "flang/Optimizer/Builder/FIRBuilder.h"
#include "flang/Optimizer/Builder/Runtime/RTBuilder.h"
#include "flang/Optimizer/CodeGen/Target.h"
#include "flang/Optimizer/CodeGen/TypeConverter.h"
#include "flang/Optimizer/Dialect/CUF/CUFOps.h"
#include "flang/Optimizer/Dialect/FIRAttr.h"
#include "flang/Optimizer/Dialect/FIROps.h"
#include "flang/Optimizer/Dialect/FIRType.h"
#include "flang/Optimizer/Support/DataLayout.h"
#include "flang/Optimizer/Transforms/Passes.h"
#include "flang/Runtime/CUDA/registration.h"
#include "flang/Runtime/entry-names.h"
#include "mlir/Dialect/DLTI/DLTI.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.h"
#include "mlir/Dialect/LLVMIR/LLVMAttrs.h"
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
#include "mlir/IR/Value.h"
#include "mlir/Pass/Pass.h"
#include "llvm/ADT/SmallVector.h"
namespace fir {
#define GEN_PASS_DEF_CUFADDCONSTRUCTOR
#include "flang/Optimizer/Transforms/Passes.h.inc"
} // namespace fir
using namespace Fortran::runtime::cuda;
namespace {
static constexpr llvm::StringRef cudaFortranCtorName{
"__cudaFortranConstructor"};
static constexpr llvm::StringRef managedPtrSuffix{".managed.ptr"};
/// Create an 8-byte pointer global in the __nv_managed_data__ section.
/// The CUDA runtime populates this pointer with the unified memory address
/// when the module is initialized via __cudaInitModule.
static fir::GlobalOp createManagedPointerGlobal(fir::FirOpBuilder &builder,
mlir::ModuleOp mod,
fir::GlobalOp globalOp) {
mlir::MLIRContext *ctx = mod.getContext();
std::string ptrGlobalName = (globalOp.getSymName() + managedPtrSuffix).str();
auto ptrTy = fir::LLVMPointerType::get(ctx, mlir::IntegerType::get(ctx, 8));
mlir::OpBuilder::InsertionGuard guard(builder);
builder.setInsertionPointAfter(globalOp);
llvm::SmallVector<mlir::NamedAttribute> attrs;
attrs.push_back(
mlir::NamedAttribute(mlir::StringAttr::get(ctx, "section"),
mlir::StringAttr::get(ctx, "__nv_managed_data__")));
mlir::DenseElementsAttr initAttr = {};
auto ptrGlobal = fir::GlobalOp::create(
builder, globalOp.getLoc(), ptrGlobalName, /*isConstant=*/false,
/*isTarget=*/false, ptrTy, initAttr,
/*linkName=*/builder.createInternalLinkage(), attrs);
mlir::Region &region = ptrGlobal.getRegion();
mlir::Block *block = builder.createBlock(&region);
builder.setInsertionPointToStart(block);
mlir::Value zero = fir::ZeroOp::create(builder, globalOp.getLoc(), ptrTy);
fir::HasValueOp::create(builder, globalOp.getLoc(), zero);
return ptrGlobal;
}
/// Return true if \p hostGlobal is a host module-scope global that has been
/// mirrored in the GPU module as an external (no-body) declaration by the
/// CUFDeviceGlobal pass under -gpu=mem:unified. Such globals must be
/// registered with the CUDA driver via CUFRegisterExternalVariable so the
/// device-side `.extern` symbol resolves to the host pointer at module-load
/// time and HMM/ATS handles migration.
static bool isCudaUnifiedExternalGlobal(fir::GlobalOp hostGlobal,
mlir::SymbolTable &gpuSymTable) {
if (hostGlobal.getDataAttrAttr())
return false;
if (hostGlobal.getConstant())
return false;
auto gpuGlobal = gpuSymTable.lookup<fir::GlobalOp>(hostGlobal.getSymName());
if (!gpuGlobal)
return false;
return !gpuGlobal.isInitialized();
}
/// Build a C-style name literal (`<symname>\0`) for use as the deviceName
/// argument of a CUF registration runtime call.
static mlir::Value buildGlobalNameLiteral(fir::FirOpBuilder &builder,
mlir::Location loc,
fir::GlobalOp globalOp) {
std::string nameStr = globalOp.getSymbol().getValue().str();
nameStr += '\0';
return fir::getBase(fir::factory::createStringLiteral(builder, loc, nameStr));
}
/// Compute the storage size in bytes of \p globalOp. For a box-typed
/// allocatable global the size is the descriptor size (after type
/// conversion); otherwise it's the size of the global's declared type.
static mlir::Value computeGlobalSize(fir::FirOpBuilder &builder,
mlir::Location loc, mlir::Type idxTy,
const mlir::DataLayout &dl,
const fir::KindMapping &kindMap,
fir::LLVMTypeConverter &typeConverter,
fir::GlobalOp globalOp) {
std::optional<uint64_t> size;
if (auto boxTy = mlir::dyn_cast<fir::BaseBoxType>(globalOp.getType())) {
mlir::Type structTy = typeConverter.convertBoxTypeAsStruct(boxTy);
size = dl.getTypeSizeInBits(structTy) / 8;
}
if (!size) {
if (auto s =
fir::getTypeSizeAndAlignment(loc, globalOp.getType(), dl, kindMap))
size = s->first;
}
if (!size) {
// A global embedding descriptor (allocatable/pointer) components has no
// structural size; size it via its LLVM type, which inlines the
// descriptors.
mlir::Type llvmTy = typeConverter.convertType(globalOp.getType());
if (llvmTy && mlir::isa<mlir::DataLayoutTypeInterface>(llvmTy))
size = dl.getTypeSizeInBits(llvmTy) / 8;
}
if (!size) {
size = fir::getTypeSizeAndAlignmentOrCrash(loc, globalOp.getType(), dl,
kindMap)
.first;
}
return builder.createIntegerConstant(loc, idxTy, *size);
}
/// Storage size in bytes of \p globalOp as a raw integer (see computeGlobalSize
/// for the box vs declared-type handling).
static uint64_t getGlobalSizeInBytes(mlir::Location loc,
const mlir::DataLayout &dl,
const fir::KindMapping &kindMap,
fir::LLVMTypeConverter &typeConverter,
fir::GlobalOp globalOp) {
std::optional<uint64_t> size;
if (auto boxTy = mlir::dyn_cast<fir::BaseBoxType>(globalOp.getType())) {
mlir::Type structTy = typeConverter.convertBoxTypeAsStruct(boxTy);
size = dl.getTypeSizeInBits(structTy) / 8;
}
if (!size) {
if (auto s =
fir::getTypeSizeAndAlignment(loc, globalOp.getType(), dl, kindMap))
size = s->first;
}
if (!size) {
// A global embedding descriptor (allocatable/pointer) components has no
// structural size; size it via its LLVM type, which inlines the
// descriptors.
mlir::Type llvmTy = typeConverter.convertType(globalOp.getType());
if (llvmTy && mlir::isa<mlir::DataLayoutTypeInterface>(llvmTy))
size = dl.getTypeSizeInBits(llvmTy) / 8;
}
if (!size) {
size = fir::getTypeSizeAndAlignmentOrCrash(loc, globalOp.getType(), dl,
kindMap)
.first;
}
return *size;
}
/// Emit a call to a CUF registration runtime function with the canonical
/// (module, addr, name, size) signature, where addr is the address of \p
/// addrGlobal taken via fir.address_of and name/size describe \p nameGlobal.
/// Used both for CUFRegisterVariable / CUFRegisterManagedVariable / and
/// CUFRegisterExternalVariable.
static void
emitCUFRegistrationCall(fir::FirOpBuilder &builder, mlir::Location loc,
mlir::Type idxTy, const mlir::DataLayout &dl,
const fir::KindMapping &kindMap,
fir::LLVMTypeConverter &typeConverter,
mlir::Value registeredMod, mlir::func::FuncOp func,
fir::GlobalOp addrGlobal, fir::GlobalOp nameGlobal) {
mlir::Value gblName = buildGlobalNameLiteral(builder, loc, nameGlobal);
mlir::Value sizeVal = computeGlobalSize(builder, loc, idxTy, dl, kindMap,
typeConverter, nameGlobal);
mlir::Value addr = fir::AddrOfOp::create(
builder, loc, addrGlobal.resultType(), addrGlobal.getSymbol());
llvm::SmallVector<mlir::Value> args{
fir::runtime::createArguments(builder, loc, func.getFunctionType(),
registeredMod, addr, gblName, sizeVal)};
fir::CallOp::create(builder, loc, func, args);
}
static bool hasRegisteredGlobals(mlir::ModuleOp mod,
mlir::SymbolTable gpuSymTable,
bool cudaUnified) {
for (fir::GlobalOp globalOp : mod.getOps<fir::GlobalOp>()) {
auto attr = globalOp.getDataAttrAttr();
if (!attr) {
if (cudaUnified && isCudaUnifiedExternalGlobal(globalOp, gpuSymTable))
return true;
continue;
}
if (!gpuSymTable.lookup(globalOp.getSymName()))
continue;
if (attr.getValue() == cuf::DataAttribute::Managed &&
!mlir::isa<fir::BaseBoxType>(globalOp.getType()))
return true;
switch (attr.getValue()) {
case cuf::DataAttribute::Device:
case cuf::DataAttribute::Constant:
case cuf::DataAttribute::Managed: {
return true;
} break;
default:
break;
}
}
return false;
}
static bool hasKernel(mlir::gpu::GPUModuleOp gpuMod) {
for (auto func : gpuMod.getOps<mlir::gpu::GPUFuncOp>())
if (func.isKernel())
return true;
return false;
}
struct CUFAddConstructor
: public fir::impl::CUFAddConstructorBase<CUFAddConstructor> {
using CUFAddConstructorBase::CUFAddConstructorBase;
void runOnOperation() override {
mlir::ModuleOp mod = getOperation();
mlir::SymbolTable symTab(mod);
mlir::OpBuilder opBuilder{mod.getBodyRegion()};
fir::FirOpBuilder builder(opBuilder, mod);
fir::KindMapping kindMap{fir::getKindMapping(mod)};
builder.setInsertionPointToEnd(mod.getBody());
mlir::Location loc = mod.getLoc();
auto *ctx = mod.getContext();
auto voidTy = mlir::LLVM::LLVMVoidType::get(ctx);
auto idxTy = builder.getIndexType();
auto funcTy =
mlir::LLVM::LLVMFunctionType::get(voidTy, {}, /*isVarArg=*/false);
std::optional<mlir::DataLayout> dl =
fir::support::getOrSetMLIRDataLayout(mod, /*allowDefaultLayout=*/false);
if (!dl) {
mlir::emitError(mod.getLoc(),
"data layout attribute is required to perform " +
getName() + "pass");
}
bool needAllocatorRegistration = false;
mod.walk([&](fir::DeclareOp declOp) {
if (declOp.getFortranAttrs() &&
fir::bitEnumContainsAny(*declOp.getFortranAttrs(),
fir::FortranVariableFlagsEnum::allocatable |
fir::FortranVariableFlagsEnum::pointer)) {
needAllocatorRegistration = true;
return mlir::WalkResult::interrupt();
}
return mlir::WalkResult::advance();
});
if (!needAllocatorRegistration) {
mod.walk([&](fir::GlobalOp globalOp) {
if (globalOp.getDataAttrAttr()) {
if (auto baseBoxType =
mlir::dyn_cast<fir::BaseBoxType>(globalOp.getType())) {
if (baseBoxType.isPointerOrAllocatable()) {
needAllocatorRegistration = true;
return mlir::WalkResult::interrupt();
}
}
}
return mlir::WalkResult::advance();
});
}
// Create the constructor function that call CUFRegisterAllocator.
builder.setInsertionPointToEnd(mod.getBody());
auto func = mlir::LLVM::LLVMFuncOp::create(builder, loc,
cudaFortranCtorName, funcTy);
func.setLinkage(mlir::LLVM::Linkage::Internal);
auto entryBlock = func.addEntryBlock(builder);
builder.setInsertionPointToStart(entryBlock);
if (needAllocatorRegistration) {
// Symbol reference to CUFRegisterAllocator.
builder.setInsertionPointToEnd(mod.getBody());
auto registerFuncOp = mlir::LLVM::LLVMFuncOp::create(
builder, loc, RTNAME_STRING(CUFRegisterAllocator), funcTy);
registerFuncOp.setVisibility(mlir::SymbolTable::Visibility::Private);
auto cufRegisterAllocatorRef = mlir::SymbolRefAttr::get(
mod.getContext(), RTNAME_STRING(CUFRegisterAllocator));
builder.setInsertionPointToStart(entryBlock);
mlir::LLVM::CallOp::create(builder, loc, funcTy, cufRegisterAllocatorRef);
}
auto gpuMod = symTab.lookup<mlir::gpu::GPUModuleOp>(cudaDeviceModuleName);
if (gpuMod) {
mlir::SymbolTable gpuSymTable(gpuMod);
bool needsModuleRegistration =
hasKernel(gpuMod) ||
hasRegisteredGlobals(mod, gpuSymTable, cudaUnified);
if (needsModuleRegistration) {
auto llvmPtrTy = mlir::LLVM::LLVMPointerType::get(ctx);
auto registeredMod = cuf::RegisterModuleOp::create(
builder, loc, llvmPtrTy,
mlir::SymbolRefAttr::get(ctx, gpuMod.getName()));
fir::LLVMTypeConverter typeConverter(
mod, /*applyTBAA=*/false, /*forceUnifiedTBAATree=*/false, *dl);
// Register kernels
for (auto func : gpuMod.getOps<mlir::gpu::GPUFuncOp>()) {
if (func.isKernel()) {
auto kernelName = mlir::SymbolRefAttr::get(
builder.getStringAttr(cudaDeviceModuleName),
{mlir::SymbolRefAttr::get(builder.getContext(),
func.getName())});
cuf::RegisterKernelOp::create(builder, loc, kernelName,
registeredMod);
}
}
// Register variables
bool hasNonAllocManagedGlobal = false;
for (fir::GlobalOp globalOp : mod.getOps<fir::GlobalOp>()) {
auto attr = globalOp.getDataAttrAttr();
if (!attr)
continue;
if (!gpuSymTable.lookup(globalOp.getSymName()))
continue;
bool isNonAllocManagedGlobal =
attr.getValue() == cuf::DataAttribute::Managed &&
!mlir::isa<fir::BaseBoxType>(globalOp.getType());
switch (attr.getValue()) {
case cuf::DataAttribute::Device:
case cuf::DataAttribute::Constant:
case cuf::DataAttribute::Managed: {
if (isNonAllocManagedGlobal) {
hasNonAllocManagedGlobal = true;
// Non-allocatable managed globals use pointer indirection:
// a companion pointer in __nv_managed_data__ holds the unified
// memory address, registered via __cudaRegisterManagedVar.
fir::GlobalOp ptrGlobal =
createManagedPointerGlobal(builder, mod, globalOp);
auto func = fir::runtime::getRuntimeFunc<mkRTKey(
CUFRegisterManagedVariable)>(loc, builder);
emitCUFRegistrationCall(builder, loc, idxTy, *dl, kindMap,
typeConverter, registeredMod, func,
/*addrGlobal=*/ptrGlobal,
/*nameGlobal=*/globalOp);
} else {
auto func =
fir::runtime::getRuntimeFunc<mkRTKey(CUFRegisterVariable)>(
loc, builder);
emitCUFRegistrationCall(builder, loc, idxTy, *dl, kindMap,
typeConverter, registeredMod, func,
/*addrGlobal=*/globalOp,
/*nameGlobal=*/globalOp);
// Under -gpu=mem:unified, also register the global as
// device-resident so a matching host symbol from another
// translation unit is not treated as host memory.
if (cudaUnified) {
uint64_t szBytes = getGlobalSizeInBytes(
loc, *dl, kindMap, typeConverter, globalOp);
cuf::RegisterVariableStaticOp::create(
builder, loc,
mlir::SymbolRefAttr::get(ctx, globalOp.getSymName()),
builder.getStringAttr(globalOp.getSymName()),
builder.getI64IntegerAttr(szBytes));
}
}
} break;
default:
break;
}
}
// Register externally-linked module globals under -gpu=mem:unified.
// CUFDeviceGlobal cloned them into the GPU module with external
// linkage so PTX emits .extern; the CUDA driver patches the device
// reference to the host pointer at module-load time after this call.
// Works uniformly for fixed-shape (e.g. fir.array<5xi32>) and
// allocatable (fir.box<fir.heap<...>>) module globals.
if (cudaUnified) {
for (fir::GlobalOp globalOp : mod.getOps<fir::GlobalOp>()) {
if (!isCudaUnifiedExternalGlobal(globalOp, gpuSymTable))
continue;
auto func = fir::runtime::getRuntimeFunc<mkRTKey(
CUFRegisterExternalVariable)>(loc, builder);
emitCUFRegistrationCall(builder, loc, idxTy, *dl, kindMap,
typeConverter, registeredMod, func,
/*addrGlobal=*/globalOp,
/*nameGlobal=*/globalOp);
}
}
if (hasNonAllocManagedGlobal) {
// Initialize the module after all variables are registered so the
// runtime populates managed variable unified memory pointers.
mlir::func::FuncOp initFunc =
fir::runtime::getRuntimeFunc<mkRTKey(CUFInitModule)>(loc,
builder);
mlir::FunctionType initFTy = initFunc.getFunctionType();
llvm::SmallVector<mlir::Value> initArgs{fir::runtime::createArguments(
builder, loc, initFTy, registeredMod)};
fir::CallOp::create(builder, loc, initFunc, initArgs);
}
}
}
mlir::LLVM::ReturnOp::create(builder, loc, mlir::ValueRange{});
// Create the llvm.global_ctor with the function.
// TODO: We might want to have a utility that retrieve it if already
// created and adds new functions.
builder.setInsertionPointToEnd(mod.getBody());
llvm::SmallVector<mlir::Attribute> funcs;
funcs.push_back(
mlir::FlatSymbolRefAttr::get(mod.getContext(), func.getSymName()));
llvm::SmallVector<int> priorities;
llvm::SmallVector<mlir::Attribute> data;
priorities.push_back(0);
data.push_back(mlir::LLVM::ZeroAttr::get(mod.getContext()));
mlir::LLVM::GlobalCtorsOp::create(
builder, mod.getLoc(), builder.getArrayAttr(funcs),
builder.getI32ArrayAttr(priorities), builder.getArrayAttr(data));
}
};
} // end anonymous namespace