Author: Konstantinos Parasyris Date: 2026-08-15T18:15:24-07:00 New Revision: 59d202c7711dabafafed4e408a50f46399322479
URL: https://github.com/llvm/llvm-project/commit/59d202c7711dabafafed4e408a50f46399322479 DIFF: https://github.com/llvm/llvm-project/commit/59d202c7711dabafafed4e408a50f46399322479.diff LOG: [CIR][SYCL] Device kernel caller (#213771) During device compilation, emit a SYCL kernel caller offload entry point in place of each function declared with the sycl_kernel_entry_point attribute, mirroring classic CodeGen's `CodeGenModule::EmitSYCLKernelCaller`. Depends on #213728 Added: Modified: clang/lib/CIR/CodeGen/CIRGenFunction.cpp clang/lib/CIR/CodeGen/CIRGenFunction.h clang/lib/CIR/CodeGen/CIRGenModule.cpp clang/lib/CIR/CodeGen/CIRGenModule.h clang/lib/CIR/CodeGen/CIRGenSYCL.cpp clang/lib/CIR/CodeGen/CIRGenTypes.cpp clang/lib/CIR/CodeGen/CIRGenTypes.h clang/lib/CIR/CodeGen/TargetInfo.h clang/lib/CIR/CodeGen/Targets/SPIRV.cpp clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp Removed: ################################################################################ diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp index 66e7b6d5061df..39cf3ee20d52b 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp @@ -369,14 +369,17 @@ void CIRGenFunction::LexicalScope::emitImplicitReturn() { CIRGenBuilderTy &builder = cgf.getBuilder(); LexicalScope *localScope = cgf.curLexScope; - const auto *fd = cast<clang::FunctionDecl>(cgf.curGD.getDecl()); + // Synthesized functions (e.g. SYCL kernel caller entry points) have no + // FunctionDecl; the non-void flow-off-the-end handling below is guarded on + // fd. + const auto *fd = dyn_cast_or_null<clang::FunctionDecl>(cgf.curGD.getDecl()); // In C++, flowing off the end of a non-void function is always undefined // behavior. In C, flowing off the end of a non-void function is undefined // behavior only if the non-existent return value is used by the caller. // That influences whether the terminating op is trap, unreachable, or // return. - if (cgf.getLangOpts().CPlusPlus && !fd->hasImplicitReturnZero() && + if (fd && cgf.getLangOpts().CPlusPlus && !fd->hasImplicitReturnZero() && !cgf.sawAsmBlock && !fd->getReturnType()->isVoidType() && builder.getInsertionBlock() && !previousOpIsNonYieldingCleanup(builder.getInsertionBlock())) { @@ -651,7 +654,7 @@ mlir::LogicalResult CIRGenFunction::emitFunctionBody(const clang::Stmt *body) { return emitStmt(body, /*useCurrentScope=*/true); } -static void eraseEmptyAndUnusedBlocks(cir::FuncOp func) { +void CIRGenFunction::eraseEmptyAndUnusedBlocks(cir::FuncOp func) { // Remove any leftover blocks that are unreachable and empty, since they do // not represent unreachable code useful for warnings nor anything deemed // useful in general. diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h index 0117945993ade..20e2d13c9652f 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.h +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h @@ -49,6 +49,7 @@ class LoopOp; } // namespace mlir namespace clang { +class OutlinedFunctionDecl; class SYCLKernelCallStmt; } // namespace clang @@ -2306,6 +2307,13 @@ class CIRGenFunction : public CIRGenTypeCache { mlir::LogicalResult emitSYCLKernelCallStmt(const SYCLKernelCallStmt &s); + void emitSYCLKernelCaller(const clang::OutlinedFunctionDecl *outlinedFnDecl, + cir::FuncOp funcOp, cir::FuncType funcType, + FunctionArgList &args); + + /// Remove leftover empty and unreachable blocks from an emitted function. + static void eraseEmptyAndUnusedBlocks(cir::FuncOp func); + std::optional<mlir::Value> emitTargetBuiltinExpr(unsigned builtinID, const clang::CallExpr *e, ReturnValueSlot &returnValue); diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.cpp b/clang/lib/CIR/CodeGen/CIRGenModule.cpp index e4650b775bc3c..16254d1bdba91 100644 --- a/clang/lib/CIR/CodeGen/CIRGenModule.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenModule.cpp @@ -451,9 +451,13 @@ void CIRGenModule::emitDeferred() { fd->isDefined()) { // Functions with an invalid sycl_kernel_entry_point attribute are // ignored during device compilation. - if (!fd->getAttr<SYCLKernelEntryPointAttr>()->isInvalidAttr()) - errorNYI(fd->getSourceRange(), - "SYCL kernel caller offload entry point"); + if (!fd->getAttr<SYCLKernelEntryPointAttr>()->isInvalidAttr()) { + // Generate and emit the SYCL kernel caller function. + emitSYCLKernelCaller(fd, getASTContext()); + // Recurse to emit any symbols directly or indirectly referenced + // by the SYCL kernel caller function. + emitDeferred(); + } // Do not emit the sycl_kernel_entry_point attributed function. continue; } diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.h b/clang/lib/CIR/CodeGen/CIRGenModule.h index 9d19a58a05b4d..78612dd2e9de8 100644 --- a/clang/lib/CIR/CodeGen/CIRGenModule.h +++ b/clang/lib/CIR/CodeGen/CIRGenModule.h @@ -654,6 +654,11 @@ class CIRGenModule : public CIRGenTypeCache { void emitGlobalDefinition(clang::GlobalDecl gd, mlir::Operation *op = nullptr); void emitGlobalFunctionDefinition(clang::GlobalDecl gd, mlir::Operation *op); + + /// Emit the SYCL kernel caller offload entry point function generated for a + /// function declared with the sycl_kernel_entry_point attribute. + void emitSYCLKernelCaller(const clang::FunctionDecl *kernelEntryPointFn, + clang::ASTContext &ctx); void emitGlobalVarDefinition(const clang::VarDecl *vd, bool isTentative = false); diff --git a/clang/lib/CIR/CodeGen/CIRGenSYCL.cpp b/clang/lib/CIR/CodeGen/CIRGenSYCL.cpp index 9308b1fe4189f..b873c2d6f24b7 100644 --- a/clang/lib/CIR/CodeGen/CIRGenSYCL.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenSYCL.cpp @@ -11,9 +11,16 @@ //===----------------------------------------------------------------------===// #include "CIRGenFunction.h" +#include "CIRGenModule.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/Attr.h" +#include "clang/AST/Decl.h" +#include "clang/AST/SYCLKernelInfo.h" #include "clang/AST/StmtSYCL.h" +#include "llvm/Support/SaveAndRestore.h" + using namespace clang; using namespace clang::CIRGen; @@ -33,3 +40,109 @@ CIRGenFunction::emitSYCLKernelCallStmt(const SYCLKernelCallStmt &s) { // of the original function body. return emitStmt(s.getKernelLaunchStmt(), /*useCurrentScope=*/true); } + +// Emit the body of a SYCL kernel caller offload entry point. Mirrors the tail +// of generateCode, but is driven by an OutlinedFunctionDecl and an explicit +// argument list rather than a FunctionDecl. +void CIRGenFunction::emitSYCLKernelCaller( + const OutlinedFunctionDecl *outlinedFnDecl, cir::FuncOp funcOp, + cir::FuncType funcType, FunctionArgList &args) { + const Stmt *body = outlinedFnDecl->getBody(); + SourceLocation loc = outlinedFnDecl->getLocation(); + SourceRange bodyRange = body->getSourceRange(); + + // Synthesized entry point: no FunctionDecl, emitted with an empty GlobalDecl. + curGD = GlobalDecl(); + + SourceLocRAIIObject fnLoc{*this, loc.isValid() ? getLoc(loc) + : builder.getUnknownLoc()}; + + mlir::Location fusedLoc = getLoc(bodyRange); + mlir::Block *entryBB = funcOp.addEntryBlock(); + + SymTableScopeTy varScope(symbolTable); + { + LexicalScope lexScope(*this, fusedLoc, entryBB); + startFunction(GlobalDecl(), getContext().VoidTy, funcOp, funcType, args, + loc, bodyRange.getBegin()); + if (mlir::failed(emitFunctionBody(body))) + return; + if (mlir::failed(funcOp.verifyBody())) + return; + finishFunction(body->getEndLoc()); + } + + eraseEmptyAndUnusedBlocks(funcOp); +} + +void CIRGenModule::emitSYCLKernelCaller(const FunctionDecl *kernelEntryPointFn, + ASTContext &ctx) { + assert(ctx.getLangOpts().SYCLIsDevice && + "SYCL kernel caller offload entry point functions can only be emitted" + " during device compilation"); + + const auto *kernelEntryPointAttr = + kernelEntryPointFn->getAttr<SYCLKernelEntryPointAttr>(); + assert(kernelEntryPointAttr && "Missing sycl_kernel_entry_point attribute"); + assert(!kernelEntryPointAttr->isInvalidAttr() && + "sycl_kernel_entry_point attribute is invalid"); + + // Find the SYCLKernelCallStmt. + SYCLKernelCallStmt *kernelCallStmt = + cast<SYCLKernelCallStmt>(kernelEntryPointFn->getBody()); + + // Retrieve the SYCL kernel caller parameters from the OutlinedFunctionDecl. + FunctionArgList args; + const OutlinedFunctionDecl *outlinedFnDecl = + kernelCallStmt->getOutlinedFunctionDecl(); + args.append(outlinedFnDecl->param_begin(), outlinedFnDecl->param_end()); + + // Compute the function info and CIR function type. + const CIRGenFunctionInfo &fnInfo = + getTypes().arrangeDeviceKernelCallerDeclaration(ctx.VoidTy, args); + cir::FuncType funcType = getTypes().getFunctionType(fnInfo); + + // Retrieve the generated name for the SYCL kernel caller function. + CanQualType kernelNameType = + ctx.getCanonicalType(kernelEntryPointAttr->getKernelName()); + const SYCLKernelInfo &kernelInfo = ctx.getSYCLKernelInfo(kernelNameType); + + // Synthesized from the OutlinedFunctionDecl, not a FunctionDecl, so create + // the function directly with a null FunctionDecl (mirrors classic CodeGen's + // llvm::Function::Create). + cir::FuncOp funcOp = createCIRFunction( + getLoc(kernelEntryPointFn->getSourceRange()), kernelInfo.GetKernelName(), + funcType, /*funcDecl=*/nullptr); + funcOp.setLinkage(cir::GlobalLinkageKind::ExternalLinkage); + + // Emit as a device kernel (e.g. spir_kernel). Classic CodeGen derives this + // from CC_DeviceKernel via SetLLVMFunctionAttributes; CIR does not yet route + // opFuncCallingConv onto the FuncOp, so set it from the target hook. + funcOp.setCallingConv(getTargetCIRGenInfo().getDeviceKernelCallingConv()); + + // Route through the shared attribute path so generic function attributes + // (e.g. convergent) are applied, matching classic CodeGen's + // SetLLVMFunctionAttributes. There is no FunctionDecl, so pass an empty + // GlobalDecl. + setCIRFunctionAttributes(GlobalDecl(), fnInfo, funcOp, /*isThunk=*/false); + + // TODO: attributes applied by classic CodeGen not yet handled in CIR: + // SetSYCLKernelAttributes (norecurse, mustprogress), addSYCLModuleIdAttr. + assert(!cir::MissingFeatures::setLLVMFunctionFEnvAttributes()); + + // Emit the SYCL kernel caller function. + CIRGenFunction cgf(*this, builder); + llvm::SaveAndRestore<CIRGenFunction *> savedCGF(curCGF, &cgf); + { + mlir::OpBuilder::InsertionGuard guard(builder); + cgf.emitSYCLKernelCaller(outlinedFnDecl, funcOp, funcType, args); + } + + setDSOLocal(static_cast<mlir::Operation *>(funcOp)); + + setNonAliasAttributes(GlobalDecl(), funcOp); + // CIR's setter takes a FunctionDecl; nullptr skips OutlinedFunctionDecl- + // derived attributes (e.g. inline hints), not yet handled. + assert(!cir::MissingFeatures::opFuncExtraAttrs()); + setCIRFunctionAttributesForDefinition(/*fd=*/nullptr, funcOp); +} diff --git a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp index b2824d6f64700..346542134470a 100644 --- a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp @@ -1,6 +1,7 @@ #include "CIRGenTypes.h" #include "CIRGenCXXABI.h" +#include "CIRGenCall.h" #include "CIRGenFunctionInfo.h" #include "CIRGenModule.h" #include "mlir/IR/BuiltinTypes.h" @@ -785,6 +786,20 @@ const CIRGenFunctionInfo &CIRGenTypes::arrangeCIRFunctionInfo( return *fi; } +const CIRGenFunctionInfo & +CIRGenTypes::arrangeDeviceKernelCallerDeclaration(QualType resultType, + const FunctionArgList &args) { + SmallVector<CanQualType, 16> argTypes; + for (const VarDecl *arg : args) + argTypes.push_back(astContext.getCanonicalParamType(arg->getType())); + + // Classic CodeGen passes FnInfoOpts::None here; that is the no-op case, so + // nothing is needed even once CIR models FnInfoOpts. + return arrangeCIRFunctionInfo( + resultType->getCanonicalTypeUnqualified(), /*isInstanceMethod=*/false, + argTypes, FunctionType::ExtInfo(CC_DeviceKernel), RequiredArgs::All); +} + const CIRGenFunctionInfo &CIRGenTypes::arrangeGlobalDeclaration(GlobalDecl gd) { assert(!dyn_cast<ObjCMethodDecl>(gd.getDecl()) && "This is reported as a FIXME in LLVM codegen"); diff --git a/clang/lib/CIR/CodeGen/CIRGenTypes.h b/clang/lib/CIR/CodeGen/CIRGenTypes.h index a7827f76bd5f2..c5f8b521ed888 100644 --- a/clang/lib/CIR/CodeGen/CIRGenTypes.h +++ b/clang/lib/CIR/CodeGen/CIRGenTypes.h @@ -44,6 +44,7 @@ class CallArgList; class CIRGenBuilderTy; class CIRGenCXXABI; class CIRGenModule; +class FunctionArgList; /// This class organizes the cross-module state that is used while lowering /// AST types to CIR types. @@ -186,6 +187,12 @@ class CIRGenTypes { const CIRGenFunctionInfo & arrangeFunctionDeclaration(const clang::FunctionDecl *fd); + /// Arrange the function info for a device kernel caller entry point (e.g. a + /// SYCL kernel caller). + const CIRGenFunctionInfo & + arrangeDeviceKernelCallerDeclaration(clang::QualType resultType, + const FunctionArgList &args); + /// A builtin function is a freestanding function using the default /// C conventions. const CIRGenFunctionInfo &arrangeBuiltinFunctionCall(QualType resultType, diff --git a/clang/lib/CIR/CodeGen/TargetInfo.h b/clang/lib/CIR/CodeGen/TargetInfo.h index 84680f82f4e4b..290027dad074f 100644 --- a/clang/lib/CIR/CodeGen/TargetInfo.h +++ b/clang/lib/CIR/CodeGen/TargetInfo.h @@ -153,6 +153,12 @@ class TargetCIRGenInfo { mlir::Operation *global, CIRGenModule &module) const {} + /// Get the CIR calling convention to use for a device kernel entry point + /// (e.g. an OpenCL/SYCL or CUDA/HIP kernel) on this target. + virtual cir::CallingConv getDeviceKernelCallingConv() const { + return cir::CallingConv::C; + } + virtual bool isScalarizableAsmOperand(CIRGenFunction &cgf, mlir::Type ty) const { return false; diff --git a/clang/lib/CIR/CodeGen/Targets/SPIRV.cpp b/clang/lib/CIR/CodeGen/Targets/SPIRV.cpp index 643c635128d09..f2d9810b36061 100644 --- a/clang/lib/CIR/CodeGen/Targets/SPIRV.cpp +++ b/clang/lib/CIR/CodeGen/Targets/SPIRV.cpp @@ -48,6 +48,10 @@ class SPIRVTargetCIRGenInfo : public TargetCIRGenInfo { func.setCallingConv(cir::CallingConv::SpirKernel); } } + + cir::CallingConv getDeviceKernelCallingConv() const override { + return cir::CallingConv::SpirKernel; + } }; } // namespace diff --git a/clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp b/clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp index c6522064f2fdc..4d7ffe01fdebe 100644 --- a/clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp +++ b/clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp @@ -1,10 +1,25 @@ -// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spir64-unknown-unknown \ -// RUN: -fclangir -emit-cir -verify %s +// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spirv64-unknown-unknown -fclangir -emit-cir %s -o %t.cir +// RUN: FileCheck --input-file=%t.cir %s -check-prefix=CIR +// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spirv64-unknown-unknown -fclangir -emit-llvm %s -o %t-cir.ll +// RUN: FileCheck --input-file=%t-cir.ll %s -check-prefixes=LLVM,LLVM-OGCG +// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spirv64-unknown-unknown -emit-llvm %s -o %t.ll +// RUN: FileCheck --input-file=%t.ll %s -check-prefixes=OGCG,LLVM-OGCG -// During device compilation, a SYCL kernel caller offload entry point is -// emitted in place of each sycl_kernel_entry_point attributed function. That -// lowering is not yet implemented in CIR, so it must be reported as a clean -// "Not Yet Implemented" diagnostic rather than crashing. +// On an ELF target such as spir64, the kernel caller entry point definition is +// dso_local. dso_local is only attached to a definition, so this also verifies +// that setDSOLocal() runs after body emission. +// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spir64-unknown-unknown -fclangir -emit-cir %s -o %t-elf.cir +// RUN: FileCheck --input-file=%t-elf.cir %s -check-prefix=CIR-ELF +// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spir64-unknown-unknown -fclangir -emit-llvm %s -o %t-elf-cir.ll +// RUN: FileCheck --input-file=%t-elf-cir.ll %s -check-prefix=LLVM-OGCG-ELF +// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spir64-unknown-unknown -emit-llvm %s -o %t-elf.ll +// RUN: FileCheck --input-file=%t-elf.ll %s -check-prefix=LLVM-OGCG-ELF + +// During device compilation, an offload kernel caller entry point is emitted +// in place of each sycl_kernel_entry_point attributed function. The entry +// point is named after the kernel name type and its body is the transformed +// body held by the OutlinedFunctionDecl (which invokes the kernel functor). +// The sycl_kernel_entry_point attributed function itself is not emitted. // Required by sycl_kernel_entry_point semantics. template <typename KernelName, typename... Ts> @@ -12,12 +27,52 @@ void sycl_kernel_launch(const char *, Ts...) {} template <typename KernelName, typename KernelType> [[clang::sycl_kernel_entry_point(KernelName)]] -// expected-error@+1 {{ClangIR code gen Not Yet Implemented: SYCL kernel caller offload entry point}} void kernel_single_task(KernelType kf) { kf(); } struct KN; +struct MemberKN; struct K { void operator()() const {} }; -void test() { kernel_single_task<KN>(K{}); } +// A sycl_kernel_entry_point function may also be a non-static member function +// (Sema only rejects explicit-object members, ctors and dtors). The offload +// entry point is still a free function and must not run an instance-function +// prologue. +struct Invoker { + template <typename KernelName, typename KernelType> + [[clang::sycl_kernel_entry_point(KernelName)]] + void kernel_single_task(KernelType kf) { kf(); } +}; + +void test() { + kernel_single_task<KN>(K{}); + Invoker{}.kernel_single_task<MemberKN>(K{}); +} + +// The kernel caller entry point is named after the kernel name type (KN), is +// emitted with the spir_kernel calling convention, and its body calls the +// kernel functor's operator(). The sycl_kernel_entry_point function and its +// launch call are not emitted during device compilation. +// CIR-LABEL: cir.func {{.*}}@_ZTS2KN{{.*}}cc(spir_kernel) +// CIR: cir.call @_ZNK1KclEv +// CIR: cir.return +// CIR-NOT: cir.func {{.*}}@_Z18kernel_single_task +// CIR-NOT: cir.call {{.*}}@_Z17sycl_kernel_launch + +// The member-function entry point is emitted the same way, as a free function +// (no implicit `this` parameter). +// CIR-LABEL: cir.func {{.*}}@_ZTS8MemberKN{{.*}}cc(spir_kernel) +// CIR: cir.call @_ZNK1KclEv +// CIR: cir.return + +// LLVM-OGCG-LABEL: define {{.*}}spir_kernel void @_ZTS2KN +// LLVM: call {{.*}}void @_ZNK1KclEv +// OGCG: call {{.*}}spir_func void @_ZNK1KclEv +// LLVM-OGCG: ret void +// LLVM-OGCG-NOT: define {{.*}}@_Z18kernel_single_task + +// On ELF, the kernel caller entry point definition is dso_local in CIR, +// CIR-lowered LLVM IR, and classic CodeGen alike. +// CIR-ELF: cir.func {{.*}}dso_local {{.*}}@_ZTS2KN +// LLVM-OGCG-ELF: define {{.*}}dso_local {{.*}}void @_ZTS2KN _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
