blob: 659f72922e05e7f07265bacf5865ec56826d1f3e [file] [edit]
//===-- NVPTXLowerArgs.cpp - Lower arguments ------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Arguments to kernel functions are passed via param space, which imposes
// certain restrictions:
// http://docs.nvidia.com/cuda/parallel-thread-execution/#state-spaces
//
// Kernel parameters are read-only and accessible only via ld.param
// instruction, directly or via a pointer.
//
// Copying a byval struct into local memory in IR allows us to enforce
// the param space restrictions, gives the rest of IR a pointer w/o
// param space restrictions, and gives us an opportunity to eliminate
// the copy.
//
// This pass lowers byval parameters of kernel functions. It rewrites the
// kernel's signature so that each byval argument is declared directly as a
// pointer in the param address space (`ptr addrspace(101)`), then adjusts the
// body to match. The parameter symbols occupy this space when lowered during
// ISel, so making the IR type honest avoids the need for a cast or intrinsic to
// reinterpret a generic pointer as a param-space pointer.
//
// This pass uses 1 of 3 possible strategies to lower byval parameters:
//
// 1. Direct readonly nocapture uses: If we can trace through all the uses and
// we can convert them all to param AS, then we'll do this. This is useful
// for pre-SM70 targets where cvta.param is not available.
//
// 2. Grid constant: If the argument is a grid constant (and the target supports
// cvta.param), we can cast back to generic address space to use the pointer
// directly.
//
// 3. Local copy: If we can't trace through all the uses and we can't convert
// them all to param AS, then we'll create a local copy of the argument in
// local memory. This is useful for arguments that are mutated.
//
//===----------------------------------------------------------------------===//
#include "NVPTX.h"
#include "NVPTXTargetMachine.h"
#include "NVPTXUtilities.h"
#include "NVVMProperties.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVectorExtras.h"
#include "llvm/Analysis/PtrUseVisitor.h"
#include "llvm/CodeGen/TargetPassConfig.h"
#include "llvm/IR/Attributes.h"
#include "llvm/IR/DebugInfo.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Type.h"
#include "llvm/InitializePasses.h"
#include "llvm/Pass.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/NVPTXAddrSpace.h"
#define DEBUG_TYPE "nvptx-lower-args"
using namespace llvm;
using namespace NVPTXAS;
namespace {
class NVPTXLowerArgsLegacyPass : public ModulePass {
bool runOnModule(Module &M) override;
public:
static char ID; // Pass identification, replacement for typeid
NVPTXLowerArgsLegacyPass() : ModulePass(ID) {}
StringRef getPassName() const override {
return "Lower pointer arguments of CUDA kernels";
}
void getAnalysisUsage(AnalysisUsage &AU) const override {
AU.addRequired<TargetPassConfig>();
}
};
} // namespace
char NVPTXLowerArgsLegacyPass::ID = 0;
INITIALIZE_PASS_BEGIN(NVPTXLowerArgsLegacyPass, "nvptx-lower-args",
"Lower arguments (NVPTX)", false, false)
INITIALIZE_PASS_DEPENDENCY(TargetPassConfig)
INITIALIZE_PASS_END(NVPTXLowerArgsLegacyPass, "nvptx-lower-args",
"Lower arguments (NVPTX)", false, false)
/// Recursively convert the users of a param to the param address space.
static void convertToParamAS(ArrayRef<Use *> OldUses, Value *Param) {
struct IP {
Use *OldUse;
Value *NewParam;
};
const auto CloneInstInParamAS = [](const IP &I) -> Value * {
auto *OldInst = cast<Instruction>(I.OldUse->getUser());
if (auto *LI = dyn_cast<LoadInst>(OldInst)) {
LI->setOperand(0, I.NewParam);
return LI;
}
if (auto *GEP = dyn_cast<GetElementPtrInst>(OldInst)) {
SmallVector<Value *, 4> Indices(GEP->indices());
auto *NewGEP = GetElementPtrInst::Create(
GEP->getSourceElementType(), I.NewParam, Indices, GEP->getName(),
GEP->getIterator());
NewGEP->setNoWrapFlags(GEP->getNoWrapFlags());
return NewGEP;
}
if (auto *BC = dyn_cast<BitCastInst>(OldInst)) {
auto *NewBCType =
PointerType::get(BC->getContext(), ADDRESS_SPACE_ENTRY_PARAM);
return BitCastInst::Create(BC->getOpcode(), I.NewParam, NewBCType,
BC->getName(), BC->getIterator());
}
if (auto *ASC = dyn_cast<AddrSpaceCastInst>(OldInst)) {
assert(ASC->getDestAddressSpace() == ADDRESS_SPACE_ENTRY_PARAM);
(void)ASC;
// Just pass through the argument, the old ASC is no longer needed.
return I.NewParam;
}
if (auto *MI = dyn_cast<MemTransferInst>(OldInst)) {
if (MI->getRawSource() == I.OldUse->get()) {
// convert to memcpy/memmove from param space.
IRBuilder<> Builder(OldInst);
Intrinsic::ID ID = MI->getIntrinsicID();
CallInst *B = Builder.CreateMemTransferInst(
ID, MI->getRawDest(), MI->getDestAlign(), I.NewParam,
MI->getSourceAlign(), MI->getLength(), MI->isVolatile());
for (unsigned I : {0, 1})
if (uint64_t Bytes = MI->getParamDereferenceableBytes(I))
B->addDereferenceableParamAttr(I, Bytes);
return B;
}
}
llvm_unreachable("Unsupported instruction");
};
auto ItemsToConvert =
map_to_vector(OldUses, [=](Use *U) -> IP { return {U, Param}; });
SmallVector<Instruction *> InstructionsToDelete;
while (!ItemsToConvert.empty()) {
IP I = ItemsToConvert.pop_back_val();
Value *NewInst = CloneInstInParamAS(I);
Instruction *OldInst = cast<Instruction>(I.OldUse->getUser());
if (NewInst && NewInst != OldInst) {
// We've created a new instruction. Queue users of the old instruction to
// be converted and the instruction itself to be deleted. We can't delete
// the old instruction yet, because it's still in use by a load somewhere.
for (Use &U : OldInst->uses())
ItemsToConvert.push_back({&U, NewInst});
InstructionsToDelete.push_back(OldInst);
}
}
// Now we know that all argument loads are using addresses in parameter space
// and we can finally remove the old instructions in generic AS. Instructions
// scheduled for removal should be processed in reverse order so the ones
// closest to the load are deleted first. Otherwise they may still be in use.
// E.g if we have Value = Load(BitCast(GEP(arg))), InstructionsToDelete will
// have {GEP,BitCast}. GEP can't be deleted first, because it's still used by
// the BitCast.
for (Instruction *I : llvm::reverse(InstructionsToDelete))
I->eraseFromParent();
}
namespace {
struct ArgUseChecker : PtrUseVisitor<ArgUseChecker> {
using Base = PtrUseVisitor<ArgUseChecker>;
// Set of phi/select instructions using the Arg
SmallPtrSet<Instruction *, 4> Conditionals;
ArgUseChecker(const DataLayout &DL) : PtrUseVisitor(DL) {}
PtrInfo visitArgPtr(Argument &A) {
assert(A.getType()->isPointerTy());
IntegerType *IntIdxTy = cast<IntegerType>(DL.getIndexType(A.getType()));
IsOffsetKnown = false;
Offset = APInt(IntIdxTy->getBitWidth(), 0);
PI.reset();
LLVM_DEBUG(dbgs() << "Checking Argument " << A << "\n");
// Enqueue the uses of this pointer.
enqueueUsers(A);
// Visit all the uses off the worklist until it is empty.
// Note that unlike PtrUseVisitor we intentionally do not track offsets.
// We're only interested in how we use the pointer.
while (!(Worklist.empty() || PI.isAborted())) {
UseToVisit ToVisit = Worklist.pop_back_val();
U = ToVisit.UseAndIsOffsetKnown.getPointer();
Instruction *I = cast<Instruction>(U->getUser());
LLVM_DEBUG(dbgs() << "Processing " << *I << "\n");
Base::visit(I);
}
if (PI.isEscaped())
LLVM_DEBUG(dbgs() << "Argument pointer escaped: " << *PI.getEscapingInst()
<< "\n");
else if (PI.isAborted())
LLVM_DEBUG(dbgs() << "Pointer use needs a copy: " << *PI.getAbortingInst()
<< "\n");
LLVM_DEBUG(dbgs() << "Traversed " << Conditionals.size()
<< " conditionals\n");
return PI;
}
void visitStoreInst(StoreInst &SI) {
// Storing the pointer escapes it.
if (U->get() == SI.getValueOperand())
return PI.setEscapedAndAborted(&SI);
PI.setAborted(&SI);
}
void visitAddrSpaceCastInst(AddrSpaceCastInst &ASC) {
// ASC to param space are no-ops and do not need a copy
if (ASC.getDestAddressSpace() != ADDRESS_SPACE_ENTRY_PARAM)
return PI.setEscapedAndAborted(&ASC);
Base::visitAddrSpaceCastInst(ASC);
}
void visitPtrToIntInst(PtrToIntInst &I) { Base::visitPtrToIntInst(I); }
void visitPHINodeOrSelectInst(Instruction &I) {
assert(isa<PHINode>(I) || isa<SelectInst>(I));
enqueueUsers(I);
Conditionals.insert(&I);
}
// PHI and select just pass through the pointers.
void visitPHINode(PHINode &PN) { visitPHINodeOrSelectInst(PN); }
void visitSelectInst(SelectInst &SI) { visitPHINodeOrSelectInst(SI); }
// memcpy/memmove are OK when the pointer is source. We can convert them to
// AS-specific memcpy.
void visitMemTransferInst(MemTransferInst &II) {
if (*U == II.getRawDest())
PI.setAborted(&II);
}
void visitMemSetInst(MemSetInst &II) { PI.setAborted(&II); }
}; // struct ArgUseChecker
// Create a local copy of the byval parameter \p Arg in an alloca, filled by a
// copy from \p ParamPtr (a pointer to the parameter), and replace all uses of
// \p Arg with the alloca. \p ParamPtr is either the natively param-space
// argument (when called from the signature rewrite) or the generic byval
// argument itself (when called early, before the signature has been rewritten).
void copyByValParam(Function &F, Argument &Arg, Value &ParamPtr) {
LLVM_DEBUG(dbgs() << "Creating a local copy of " << Arg << "\n");
Type *ByValType = Arg.getParamByValType();
const DataLayout &DL = F.getDataLayout();
IRBuilder<> IRB(&F.getEntryBlock().front());
AllocaInst *AllocA = IRB.CreateAlloca(ByValType, nullptr, Arg.getName());
// Set the alignment to alignment of the byval parameter. This is because,
// later load/stores assume that alignment, and we are going to replace
// the use of the byval parameter with this alloca instruction.
AllocA->setAlignment(
Arg.getParamAlign().value_or(DL.getPrefTypeAlign(ByValType)));
Arg.replaceAllUsesWith(AllocA);
// If the parameter is never read (writeonly or readnone), there is nothing to
// copy in; the alloca above already provides the writable local storage the
// body needs, and reading the param here would contradict the attribute.
if (Arg.hasAttribute(Attribute::ReadNone) ||
Arg.hasAttribute(Attribute::WriteOnly))
return;
// Be sure to propagate alignment to this copy; LLVM doesn't know that NVPTX
// addrspacecast preserves alignment. Since params are constant, this copy
// is definitely not volatile.
const auto ArgSize = *AllocA->getAllocationSize(DL);
IRB.CreateMemCpy(AllocA, AllocA->getAlign(), &ParamPtr, AllocA->getAlign(),
ArgSize);
}
} // namespace
// Returns true if F has a byval argument not yet in the param address space.
// Such arguments are lowered exactly once, so one already in param space means
// the kernel has already been processed.
static bool kernelNeedsByValLowering(const Function &F) {
return any_of(F.args(), [](const Argument &A) {
return A.hasByValAttr() &&
A.getType()->getPointerAddressSpace() != ADDRESS_SPACE_ENTRY_PARAM;
});
}
// Lower the uses of a single kernel byval argument. \p OldArg is the original
// (generic) argument whose uses are being rewritten; \p NewParamArg is its
// replacement, natively in the param address space.
static void lowerKernelByValParam(Argument &OldArg, Argument &NewParamArg,
Function &F, const bool HasCvtaParam) {
assert(isKernelFunction(F));
const DataLayout &DL = F.getDataLayout();
IRBuilder<> IRB(&F.getEntryBlock().front());
if (OldArg.use_empty())
return;
// (1) First check the easy case, if were able to trace through all the uses
// and we can convert them all to param AS, then we'll do this.
ArgUseChecker AUC(DL);
ArgUseChecker::PtrInfo PI = AUC.visitArgPtr(OldArg);
const bool ArgUseIsReadOnly = !(PI.isEscaped() || PI.isAborted());
if (ArgUseIsReadOnly && AUC.Conditionals.empty()) {
// Convert all loads and intermediate operations to use parameter AS and
// skip creation of a local copy of the argument.
SmallVector<Use *, 16> UsesToUpdate(make_pointer_range(OldArg.uses()));
for (Use *U : UsesToUpdate)
convertToParamAS(U, &NewParamArg);
// This path does not replaceAllUsesWith the old argument, so any debug-info
// uses would be left dangling and reset to poison when the old function is
// erased. Point them at the new param-space argument instead.
if (OldArg.isUsedByMetadata()) {
SmallVector<DbgVariableRecord *, 4> DbgUsers;
findDbgUsers(&OldArg, DbgUsers);
for (DbgVariableRecord *DVR : DbgUsers)
DVR->replaceVariableLocationOp(&OldArg, &NewParamArg);
}
return;
}
// (2) If the argument is grid constant, we get to use the pointer directly.
if (HasCvtaParam && (ArgUseIsReadOnly || isParamGridConstant(OldArg))) {
LLVM_DEBUG(dbgs() << "Using non-copy pointer to " << OldArg << "\n");
// Cast the param-space argument to the generic address space. Because the
// argument is natively in param space, this cast only ever goes
// param -> generic and lowers to cvta.param; there is no inverse cast for
// InferAddressSpaces to fold it away with.
Value *GenericArg = IRB.CreateAddrSpaceCast(
&NewParamArg, IRB.getPtrTy(ADDRESS_SPACE_GENERIC),
OldArg.getName() + ".gen");
OldArg.replaceAllUsesWith(GenericArg);
return;
}
// (3) Otherwise we have to create a copy of the argument in local memory.
copyByValParam(F, OldArg, NewParamArg);
}
// Mark a param-space byval argument as non-writable.
static void markArgNonWritable(Argument &Arg) {
if (Arg.onlyReadsMemory())
return;
if (Arg.hasAttribute(Attribute::WriteOnly)) {
Arg.removeAttr(Attribute::WriteOnly);
Arg.addAttr(Attribute::ReadNone);
return;
}
Arg.addAttr(Attribute::ReadOnly);
}
// Rewrite a kernel's signature so that each byval argument is declared directly
// as a pointer in the param address space, then lower the body to match. This
// creates a new function, moves the body across, and erases \p F.
static void rewriteKernelByValSignature(Function &F, const bool HasCvtaParam) {
LLVMContext &Ctx = F.getContext();
FunctionType *FTy = F.getFunctionType();
// Build the new signature: byval pointer arguments move to the param address
// space; all other arguments are unchanged.
SmallVector<Type *> Params(FTy->params());
for (const Argument &Arg : F.args())
if (Arg.hasByValAttr())
Params[Arg.getArgNo()] = PointerType::get(Ctx, ADDRESS_SPACE_ENTRY_PARAM);
Function *NF = Function::Create(
FunctionType::get(FTy->getReturnType(), Params, FTy->isVarArg()),
F.getLinkage(), F.getAddressSpace());
NF->copyAttributesFrom(&F);
NF->setComdat(F.getComdat());
F.getParent()->getFunctionList().insert(F.getIterator(), NF);
// ISel reads the param symbol directly for kernel byval arguments; this is
// valid because the signature rewrite above puts them in the param address
// space. Mark them non-writable: any mutation is redirected to a local copy
// below, so the param itself is never written.
for (Argument &NewArg : NF->args())
if (NewArg.hasByValAttr())
markArgNonWritable(NewArg);
// Take over F's name and uses (e.g. @llvm.used, nvvm.annotations metadata),
// then move the body across.
F.replaceAllUsesWith(NF);
NF->takeName(&F);
NF->splice(NF->begin(), &F);
// Remap arguments. Non-byval arguments keep their type and are replaced
// directly; byval arguments change address space, so their uses are lowered
// to operate on the new param-space argument.
for (auto [OldArg, NewArg] : zip_equal(F.args(), NF->args())) {
if (OldArg.hasByValAttr())
lowerKernelByValParam(OldArg, NewArg, *NF, HasCvtaParam);
else
OldArg.replaceAllUsesWith(&NewArg);
NewArg.takeName(&OldArg);
}
// Move function-level metadata (debug info, etc.) to the new function.
NF->copyMetadata(&F, /*Offset=*/0);
F.clearMetadata();
F.eraseFromParent();
}
// =============================================================================
// Main function for this pass.
// =============================================================================
static bool processFunction(Function &F, NVPTXTargetMachine &TM) {
if (!isKernelFunction(F) || F.isDeclaration())
return false;
// Skip kernels with no byval arguments, and those already lowered (byval
// arguments sitting in the param address space).
if (!kernelNeedsByValLowering(F))
return false;
LLVM_DEBUG(dbgs() << "Lowering kernel args of " << F.getName() << "\n");
const NVPTXSubtarget *ST = TM.getSubtargetImpl(F);
rewriteKernelByValSignature(F, ST->hasCvtaParam());
return true;
}
static bool processModule(Module &M, NVPTXTargetMachine &TM) {
bool Changed = false;
for (Function &F : make_early_inc_range(M))
Changed |= processFunction(F, TM);
return Changed;
}
bool NVPTXLowerArgsLegacyPass::runOnModule(Module &M) {
auto &TM = getAnalysis<TargetPassConfig>().getTM<NVPTXTargetMachine>();
return processModule(M, TM);
}
ModulePass *llvm::createNVPTXLowerArgsPass() {
return new NVPTXLowerArgsLegacyPass();
}
static bool copyFunctionByValArgs(Function &F) {
LLVM_DEBUG(dbgs() << "Creating a copy of byval args of " << F.getName()
<< "\n");
bool Changed = false;
if (isKernelFunction(F)) {
for (Argument &Arg : F.args())
if (Arg.hasByValAttr() && !isParamGridConstant(Arg)) {
copyByValParam(F, Arg, Arg);
Changed = true;
}
}
return Changed;
}
PreservedAnalyses NVPTXCopyByValArgsPass::run(Function &F,
FunctionAnalysisManager &AM) {
return copyFunctionByValArgs(F) ? PreservedAnalyses::none()
: PreservedAnalyses::all();
}
PreservedAnalyses NVPTXLowerArgsPass::run(Module &M,
ModuleAnalysisManager &AM) {
auto &NTM = static_cast<NVPTXTargetMachine &>(TM);
bool Changed = processModule(M, NTM);
return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all();
}