mirror-llvm-project/clang/lib/CIR/CodeGen/CIRGenCUDANV.cpp
Erich Keane 06361fb2ef
[CIR] Implement most of 'musttail' statement attr (#213154)
This implements the basics, and leaves NYI in a few places (particularly
those that require cleanup calculations). There IS some additional work
that needs to be done when the ABI work is put in place as well, and the
thunk 'tail' hint isn't implemented either, and is left as a
missing-feature.

This is necessary for compiling the bytecode interpreter in Clang in a
few places during self-build.
2026-07-30 16:18:06 -07:00

531 lines
21 KiB
C++

//========- CIRGenCUDANV.cpp - Interface to NVIDIA CUDA Runtime -----=========//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// This provides a class for CUDA code generation targeting the NVIDIA CUDA
// runtime library.
//
//===----------------------------------------------------------------------===//
#include "CIRGenCUDARuntime.h"
#include "CIRGenCXXABI.h"
#include "CIRGenFunction.h"
#include "CIRGenModule.h"
#include "mlir/IR/Operation.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Attrs.inc"
#include "clang/AST/Decl.h"
#include "clang/AST/GlobalDecl.h"
#include "clang/Basic/AddressSpaces.h"
#include "clang/Basic/Cuda.h"
#include "clang/CIR/Dialect/IR/CIRDialect.h"
#include "clang/CIR/Dialect/IR/CIRTypes.h"
#include "llvm/Support/Casting.h"
using namespace clang;
using namespace clang::CIRGen;
namespace {
class CIRGenNVCUDARuntime : public CIRGenCUDARuntime {
protected:
StringRef prefix;
// Map a device stub function to a symbol for identifying kernel in host
// code. For CUDA, the symbol for identifying the kernel is the same as the
// device stub function. For HIP, they are different.
llvm::StringMap<mlir::Operation *> kernelHandles;
// Map a kernel handle to the kernel stub.
llvm::DenseMap<mlir::Operation *, mlir::Operation *> kernelStubs;
struct VarInfo {
cir::GlobalOp var;
const VarDecl *d;
cir::CUDADeviceVarKind flags;
};
llvm::SmallVector<VarInfo, 16> deviceVars;
// Mangle context for device.
std::unique_ptr<MangleContext> deviceMC;
private:
void emitDeviceStubBodyNew(CIRGenFunction &cgf, cir::FuncOp fn,
FunctionArgList &args);
mlir::Value prepareKernelArgs(CIRGenFunction &cgf, mlir::Location loc,
FunctionArgList &args);
mlir::Operation *getKernelHandle(cir::FuncOp fn, GlobalDecl gd) override;
mlir::Operation *getKernelStub(mlir::Operation *handle) override {
auto it = kernelStubs.find(handle);
assert(it != kernelStubs.end());
return it->second;
}
std::string addPrefixToName(StringRef funcName) const;
std::string addUnderscoredPrefixToName(StringRef funcName) const;
public:
CIRGenNVCUDARuntime(CIRGenModule &cgm);
~CIRGenNVCUDARuntime();
void emitDeviceStub(CIRGenFunction &cgf, cir::FuncOp fn,
FunctionArgList &args) override;
void handleVarRegistration(const VarDecl *vd, cir::GlobalOp var) override;
void finalizeModule() override;
void handleGlobalReplace(cir::GlobalOp oldGV, cir::GlobalOp newGV) override;
void internalizeDeviceSideVar(const VarDecl *d,
cir::GlobalLinkageKind &linkage) override;
std::string getDeviceSideName(const NamedDecl *nd) override;
void registerDeviceVar(const VarDecl *vd, cir::GlobalOp &var, bool isExtern,
bool isConstant) {
// Attach the device var attribute to the GlobalOp
auto &builder = cgm.getBuilder();
var->setAttr(cir::CUDAVarRegistrationInfoAttr::getMnemonic(),
cir::CUDAVarRegistrationInfoAttr::get(
builder.getContext(),
getDeviceSideName(cast<NamedDecl>(vd)),
cir::CUDADeviceVarKind::Variable, isExtern, isConstant,
vd->hasAttr<HIPManagedAttr>()));
deviceVars.push_back({
var,
vd,
cir::CUDADeviceVarKind::Variable,
});
}
void registerDeviceSurf(const VarDecl *vd, cir::GlobalOp &var,
bool isExtern) {
auto &builder = cgm.getBuilder();
var->setAttr(cir::CUDAVarRegistrationInfoAttr::getMnemonic(),
cir::CUDAVarRegistrationInfoAttr::get(
builder.getContext(),
getDeviceSideName(cast<NamedDecl>(vd)),
cir::CUDADeviceVarKind::Surface, isExtern,
/*isConstant=*/false,
/*isManaged=*/false));
deviceVars.push_back({
var,
vd,
cir::CUDADeviceVarKind::Surface,
});
}
};
} // namespace
std::string CIRGenNVCUDARuntime::addPrefixToName(StringRef funcName) const {
return (prefix + funcName).str();
}
std::string
CIRGenNVCUDARuntime::addUnderscoredPrefixToName(StringRef funcName) const {
return ("__" + prefix + funcName).str();
}
CIRGenNVCUDARuntime::CIRGenNVCUDARuntime(CIRGenModule &cgm)
: CIRGenCUDARuntime(cgm),
deviceMC(cgm.getASTContext().cudaNVInitDeviceMC()) {
if (cgm.getLangOpts().OffloadViaLLVM)
cgm.errorNYI("CIRGenNVCUDARuntime: Offload via LLVM");
else if (cgm.getLangOpts().HIP)
prefix = "hip";
else
prefix = "cuda";
}
mlir::Value CIRGenNVCUDARuntime::prepareKernelArgs(CIRGenFunction &cgf,
mlir::Location loc,
FunctionArgList &args) {
CIRGenBuilderTy &builder = cgm.getBuilder();
// Build void *args[] and populate with the addresses of kernel arguments.
auto voidPtrArrayTy = cir::ArrayType::get(cgm.voidPtrTy, args.size());
mlir::Value kernelArgs =
builder.createAlloca(loc, cir::PointerType::get(voidPtrArrayTy),
"kernel_args", CharUnits::fromQuantity(16));
mlir::Value kernelArgsDecayed =
builder.createCast(cir::CastKind::array_to_ptrdecay, kernelArgs,
cir::PointerType::get(cgm.voidPtrTy));
for (const auto &[i, arg] : llvm::enumerate(args)) {
mlir::Value index =
builder.getConstInt(loc, llvm::APInt(/*numBits=*/32, i));
mlir::Value storePos =
builder.createPtrStride(loc, kernelArgsDecayed, index);
mlir::Value argAddr = cgf.getAddrOfLocalVar(arg).getPointer();
mlir::Value argAsVoid = builder.createBitcast(argAddr, cgm.voidPtrTy);
builder.CIRBaseBuilderTy::createStore(loc, argAsVoid, storePos);
}
return kernelArgsDecayed;
}
// CUDA 9.0+ uses new way to launch kernels. Parameters are packed in a local
// array and kernels are launched using cudaLaunchKernel().
void CIRGenNVCUDARuntime::emitDeviceStubBodyNew(CIRGenFunction &cgf,
cir::FuncOp fn,
FunctionArgList &args) {
// This requires arguments to be sent to kernels in a different way.
if (cgm.getLangOpts().OffloadViaLLVM)
cgm.errorNYI("CIRGenNVCUDARuntime: Offload via LLVM");
CIRGenBuilderTy &builder = cgm.getBuilder();
mlir::Location loc = fn.getLoc();
// For [cuda|hip]LaunchKernel, we must add another layer of indirection
// to arguments. For example, for function `add(int a, float b)`,
// we need to pass it as `void *args[2] = { &a, &b }`.
mlir::Value kernelArgs = prepareKernelArgs(cgf, loc, args);
// Lookup cudaLaunchKernel/hipLaunchKernel function.
// HIP kernel launching API name depends on -fgpu-default-stream option. For
// the default value 'legacy', it is hipLaunchKernel. For 'per-thread',
// it is hipLaunchKernel_spt.
// cudaError_t cudaLaunchKernel(const void *func, dim3 gridDim, dim3 blockDim,
// void **args, size_t sharedMem,
// cudaStream_t stream);
// hipError_t hipLaunchKernel[_spt](const void *func, dim3 gridDim,
// dim3 blockDim, void **args,
// size_t sharedMem, hipStream_t stream);
TranslationUnitDecl *tuDecl = cgm.getASTContext().getTranslationUnitDecl();
DeclContext *dc = TranslationUnitDecl::castToDeclContext(tuDecl);
// The default stream is usually stream 0 (the legacy default stream).
// For per-thread default stream, we need a different LaunchKernel function.
std::string kernelLaunchAPI = "LaunchKernel";
if (cgm.getLangOpts().GPUDefaultStream ==
LangOptions::GPUDefaultStreamKind::PerThread) {
if (cgm.getLangOpts().HIP)
kernelLaunchAPI += "_spt";
else if (cgm.getLangOpts().CUDA)
kernelLaunchAPI += "_ptsz";
}
std::string launchKernelName = addPrefixToName(kernelLaunchAPI);
const IdentifierInfo &launchII =
cgm.getASTContext().Idents.get(launchKernelName);
FunctionDecl *cudaLaunchKernelFD = nullptr;
for (NamedDecl *result : dc->lookup(&launchII)) {
if (FunctionDecl *fd = dyn_cast<FunctionDecl>(result))
cudaLaunchKernelFD = fd;
}
if (cudaLaunchKernelFD == nullptr) {
cgm.error(cgf.curFuncDecl->getLocation(),
"Can't find declaration for " + launchKernelName);
return;
}
// Use this function to retrieve arguments for cudaLaunchKernel:
// int __[cuda|hip]PopCallConfiguration(dim3 *gridDim, dim3 *blockDim, size_t
// *sharedMem, cudaStream_t *stream)
//
// Here [cuda|hip]Stream_t, while also being the 6th argument of
// [cuda|hip]LaunchKernel, is a pointer to some opaque struct.
mlir::Type dim3Ty = cgf.getTypes().convertType(
cudaLaunchKernelFD->getParamDecl(1)->getType());
mlir::Type streamTy = cgf.getTypes().convertType(
cudaLaunchKernelFD->getParamDecl(5)->getType());
mlir::Value gridDim =
builder.createAlloca(loc, cir::PointerType::get(dim3Ty), "grid_dim",
CharUnits::fromQuantity(8));
mlir::Value blockDim =
builder.createAlloca(loc, cir::PointerType::get(dim3Ty), "block_dim",
CharUnits::fromQuantity(8));
mlir::Value sharedMem = builder.createAlloca(
loc, cir::PointerType::get(cgm.sizeTy), "shared_mem", cgm.getSizeAlign());
mlir::Value stream = builder.createAlloca(
loc, cir::PointerType::get(streamTy), "stream", cgm.getPointerAlign());
cir::FuncOp popConfig = cgm.createRuntimeFunction(
cir::FuncType::get({gridDim.getType(), blockDim.getType(),
sharedMem.getType(), stream.getType()},
cgm.sInt32Ty),
addUnderscoredPrefixToName("PopCallConfiguration"));
cgf.emitRuntimeCall(loc, popConfig, {gridDim, blockDim, sharedMem, stream});
// Now emit the call to cudaLaunchKernel
// [cuda|hip]Error_t [cuda|hip]LaunchKernel(const void *func, dim3 gridDim,
// dim3 blockDim,
// void **args, size_t sharedMem,
// [cuda|hip]Stream_t stream);
// We now either pick the function or the stub global for cuda, hip
// respectively.
mlir::Value kernel = [&]() -> mlir::Value {
if (cir::GlobalOp globalOp = llvm::dyn_cast_or_null<cir::GlobalOp>(
kernelHandles[fn.getSymName()])) {
cir::PointerType kernelTy = cir::PointerType::get(globalOp.getSymType());
mlir::Value kernelVal = cir::GetGlobalOp::create(builder, loc, kernelTy,
globalOp.getSymName());
mlir::Value func = builder.createBitcast(kernelVal, cgm.voidPtrTy);
return func;
}
if (cir::FuncOp funcOp = llvm::dyn_cast_or_null<cir::FuncOp>(
kernelHandles[fn.getSymName()])) {
cir::PointerType kernelTy =
cir::PointerType::get(funcOp.getFunctionType());
mlir::Value kernelVal =
cir::GetGlobalOp::create(builder, loc, kernelTy, funcOp.getSymName());
mlir::Value func = builder.createBitcast(kernelVal, cgm.voidPtrTy);
return func;
}
llvm_unreachable("Expected stub handle to be cir::GlobalOp or FuncOp");
}();
CallArgList launchArgs;
launchArgs.add(RValue::get(kernel),
cudaLaunchKernelFD->getParamDecl(0)->getType());
launchArgs.add(
RValue::getAggregate(Address(gridDim, CharUnits::fromQuantity(8))),
cudaLaunchKernelFD->getParamDecl(1)->getType());
launchArgs.add(
RValue::getAggregate(Address(blockDim, CharUnits::fromQuantity(8))),
cudaLaunchKernelFD->getParamDecl(2)->getType());
launchArgs.add(RValue::get(kernelArgs),
cudaLaunchKernelFD->getParamDecl(3)->getType());
launchArgs.add(
RValue::get(builder.CIRBaseBuilderTy::createLoad(loc, sharedMem)),
cudaLaunchKernelFD->getParamDecl(4)->getType());
launchArgs.add(RValue::get(builder.CIRBaseBuilderTy::createLoad(loc, stream)),
cudaLaunchKernelFD->getParamDecl(5)->getType());
mlir::Type launchTy =
cgm.getTypes().convertType(cudaLaunchKernelFD->getType());
mlir::Operation *cudaKernelLauncherFn = cgm.createRuntimeFunction(
cast<cir::FuncType>(launchTy), launchKernelName);
const CIRGenFunctionInfo &callInfo =
cgm.getTypes().arrangeFunctionDeclaration(cudaLaunchKernelFD);
cgf.emitCall(callInfo, CIRGenCallee::forDirect(cudaKernelLauncherFn),
ReturnValueSlot(), launchArgs, /*isMustTail=*/false);
if (cgm.getASTContext().getTargetInfo().getCXXABI().isMicrosoft() &&
!cgf.getLangOpts().HIP)
cgm.errorNYI("MSVC CUDA stub handling");
}
void CIRGenNVCUDARuntime::emitDeviceStub(CIRGenFunction &cgf, cir::FuncOp fn,
FunctionArgList &args) {
if (auto globalOp =
llvm::dyn_cast<cir::GlobalOp>(kernelHandles[fn.getSymName()])) {
CIRGenBuilderTy &builder = cgm.getBuilder();
mlir::Type fnPtrTy = globalOp.getSymType();
auto sym = mlir::FlatSymbolRefAttr::get(fn.getSymNameAttr());
auto gv = cir::GlobalViewAttr::get(fnPtrTy, sym);
globalOp->setAttr("initial_value", gv);
globalOp->removeAttr("sym_visibility");
globalOp->setAttr("alignment", builder.getI64IntegerAttr(
cgm.getPointerAlign().getQuantity()));
}
// CUDA 9.0 changed the way to launch kernels.
if (CudaFeatureEnabled(cgm.getTarget().getSDKVersion(),
CudaFeature::CUDA_USES_NEW_LAUNCH) ||
(cgm.getLangOpts().HIP && cgm.getLangOpts().HIPUseNewLaunchAPI) ||
cgm.getLangOpts().OffloadViaLLVM)
emitDeviceStubBodyNew(cgf, fn, args);
else
cgm.errorNYI("Emit Stub Body Legacy");
}
CIRGenCUDARuntime *clang::CIRGen::createNVCUDARuntime(CIRGenModule &cgm) {
return new CIRGenNVCUDARuntime(cgm);
}
CIRGenNVCUDARuntime::~CIRGenNVCUDARuntime() {}
mlir::Operation *CIRGenNVCUDARuntime::getKernelHandle(cir::FuncOp fn,
GlobalDecl gd) {
// Check if we already have a kernel handle for this function
auto it = kernelHandles.find(fn.getSymName());
if (it != kernelHandles.end()) {
mlir::Operation *oldHandle = it->second;
// Here we know that the fn did not change. Return it
if (kernelStubs[oldHandle] == fn)
return oldHandle;
// We've found the function name, but F itself has changed, so we need to
// update the references.
if (cgm.getLangOpts().HIP) {
// For HIP compilation the handle itself does not change, so we only need
// to update the Stub value.
kernelStubs[oldHandle] = fn;
return oldHandle;
}
// For non-HIP compilation, erase the old Stub and fall-through to creating
// new entries.
kernelStubs.erase(oldHandle);
}
// If not targeting HIP, store the function itself
if (!cgm.getLangOpts().HIP) {
kernelHandles[fn.getSymName()] = fn;
kernelStubs[fn] = fn;
return fn;
}
// Create a new CIR global variable to represent the kernel handle
CIRGenBuilderTy &builder = cgm.getBuilder();
StringRef globalName = cgm.getMangledName(
gd.getWithKernelReferenceKind(KernelReferenceKind::Kernel));
cir::PointerType fnPtrTy = builder.getPointerTo(fn.getFunctionType());
cir::GlobalOp globalOp =
cgm.createGlobalOp(fn.getLoc(), globalName, fnPtrTy, /*isConstant=*/true);
globalOp->setAttr("alignment", builder.getI64IntegerAttr(
cgm.getPointerAlign().getQuantity()));
// Store references
kernelHandles[fn.getSymName()] = globalOp;
kernelStubs[globalOp] = fn;
return globalOp;
}
void CIRGenNVCUDARuntime::internalizeDeviceSideVar(
const VarDecl *d, cir::GlobalLinkageKind &linkage) {
if (cgm.getLangOpts().GPURelocatableDeviceCode)
cgm.errorNYI(d->getSourceRange(),
"internalizeDeviceSideVar: GPU Relocatable Device Code (RDC)");
// __shared__ variables are odd. Shadows do get created, but
// they are not registered with the CUDA runtime, so they
// can't really be used to access their device-side
// counterparts. It's not clear yet whether it's nvcc's bug or
// a feature, but we've got to do the same for compatibility.
if (d->hasAttr<CUDADeviceAttr>() || d->hasAttr<CUDAConstantAttr>() ||
d->hasAttr<CUDASharedAttr>()) {
linkage = cir::GlobalLinkageKind::InternalLinkage;
}
if (d->getType()->isCUDADeviceBuiltinSurfaceType() ||
d->getType()->isCUDADeviceBuiltinTextureType())
cgm.errorNYI(d->getSourceRange(),
"internalizeDeviceSideVar: CUDA Surface/Texture support");
}
std::string CIRGenNVCUDARuntime::getDeviceSideName(const NamedDecl *nd) {
GlobalDecl gd;
// nd could be either a kernel or a variable.
if (auto *fd = dyn_cast<FunctionDecl>(nd))
gd = GlobalDecl(fd, KernelReferenceKind::Kernel);
else
gd = GlobalDecl(nd);
std::string deviceSideName;
MangleContext *mc;
if (cgm.getLangOpts().CUDAIsDevice)
mc = &cgm.getCXXABI().getMangleContext();
else
mc = deviceMC.get();
if (mc->shouldMangleDeclName(nd)) {
SmallString<256> buffer;
llvm::raw_svector_ostream out(buffer);
mc->mangleName(gd, out);
deviceSideName = std::string(out.str());
} else
deviceSideName = std::string(nd->getIdentifier()->getName());
// Make unique name for device side static file-scope variable for HIP.
if (cgm.getASTContext().shouldExternalize(nd) &&
cgm.getLangOpts().GPURelocatableDeviceCode) {
SmallString<256> buffer;
llvm::raw_svector_ostream out(buffer);
out << deviceSideName;
cgm.printPostfixForExternalizedDecl(out, nd);
deviceSideName = std::string(out.str());
}
return deviceSideName;
}
void CIRGenNVCUDARuntime::handleVarRegistration(const VarDecl *vd,
cir::GlobalOp var) {
if (vd->hasAttr<CUDADeviceAttr>() || vd->hasAttr<CUDAConstantAttr>()) {
// Shadow variables and their properties must be registered with CUDA
// runtime. Skip Extern global variables, which will be registered in
// the TU where they are defined.
//
// Don't register a C++17 inline variable. The local symbol can be
// discarded and referencing a discarded local symbol from outside the
// comdat (__cuda_register_globals) is disallowed by the ELF spec.
//
// HIP managed variables need to be always recorded in device and host
// compilations for transformation.
//
// HIP managed variables and variables in CUDADeviceVarODRUsedByHost are
// added to llvm.compiler-used, therefore they are safe to be registered.
if ((!vd->hasExternalStorage() && !vd->isInline()) ||
cgm.getASTContext().CUDADeviceVarODRUsedByHost.contains(vd) ||
vd->hasAttr<HIPManagedAttr>()) {
registerDeviceVar(vd, var, !vd->hasDefinition(),
vd->hasAttr<CUDAConstantAttr>());
}
} else if (vd->getType()->isCUDADeviceBuiltinSurfaceType()) {
// Builtin surfaces and their template arguments are also registered
// with CUDA runtime.
if (!vd->hasExternalStorage())
registerDeviceSurf(vd, var, !vd->hasDefinition());
} else if (vd->getType()->isCUDADeviceBuiltinTextureType()) {
cgm.errorNYI(vd->getSourceRange(),
"handleVarRegistration: Texture registration");
}
}
void CIRGenNVCUDARuntime::handleGlobalReplace(cir::GlobalOp oldGV,
cir::GlobalOp newGV) {
for (auto &info : deviceVars) {
if (info.var == oldGV)
info.var = newGV;
}
}
void CIRGenNVCUDARuntime::finalizeModule() {
if (!cgm.getLangOpts().CUDAIsDevice)
return;
// Mark ODR-used device variables as compiler used to prevent them from being
// eliminated by optimization. This is necessary for device variables
// ODR-used by host functions. Sema correctly marks them as ODR-used no
// matter whether they are ODR-used by device or host functions.
//
// We do not need to do this if the variable has used attribute since it
// has already been added.
//
// Static device variables have been externalized at this point, therefore
// variables with private or internal linkage need not be added.
for (auto &&info : deviceVars) {
auto kind = info.flags;
bool isDecl = info.var.isDeclaration();
bool isLocalLinkage = cir::isLocalLinkage(info.var.getLinkage());
bool isVarOrSurfaceOrTexture = (kind == cir::CUDADeviceVarKind::Variable ||
kind == cir::CUDADeviceVarKind::Surface ||
kind == cir::CUDADeviceVarKind::Texture);
bool isUsed = info.d->isUsed();
bool hasUsedAttr = info.d->hasAttr<UsedAttr>();
if (!isDecl && !isLocalLinkage && isVarOrSurfaceOrTexture && isUsed &&
!hasUsedAttr) {
if (auto globalValue = mlir::dyn_cast<cir::CIRGlobalValueInterface>(
info.var.getOperation())) {
cgm.addCompilerUsedGlobal(globalValue);
}
}
}
}