-
Notifications
You must be signed in to change notification settings - Fork 850
[SYCL][FFK] Add SYCL_EXT_ONEAPI_KERNEL_FUNCTION bare-name kernel launch #22865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: sycl
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5917,6 +5917,106 @@ void SemaSYCL::processFreeFunctionDeclaration(const FunctionDecl *FD) { | |
| FreeFunctionDeclarations.insert(FD->getCanonicalDecl()); | ||
| } | ||
|
|
||
| // Handle __builtin_sycl_launch_kernel(name, launch-args...). The | ||
| // SYCL_EXT_ONEAPI_KERNEL_FUNCTION launch macro expands to | ||
| // | ||
| // kernel_function<__builtin_sycl_launch_kernel(name, args...)>, args... | ||
| // | ||
| // so this builtin sits in the `kernel_function<Func>` non-type template | ||
| // parameter slot of an enqueue function. Its job is purely to resolve `name` | ||
| // (a bare, possibly overloaded / templated free-function-kernel name) to a | ||
| // concrete specialization using the launch argument types, and to evaluate to | ||
| // a pointer to it — a constant expression usable as the `auto *Func` NTTP. The | ||
| // same launch arguments are passed positionally to the enqueue function to | ||
| // perform the actual launch; the builtin never evaluates them. Mirrors the | ||
| // CUDA `<<<>>>` front-end approach (build a synthetic call, read the resolved | ||
| // callee), and reuses the real overload-resolution machinery so a bad launch | ||
| // produces ordinary diagnostics at the call site. | ||
| ExprResult SemaSYCL::BuildSYCLLaunchKernelCall(CallExpr *TheCall) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise with respect to my comment about the name of the builtin function, this name doesn't reflect what the function really does. It is misleadingly similar to other functions in this same file that actually are involved in launching a kernel (e.g., I find the comparison with CUDA misleading as well. The CUDA kernel call expression does actually launch a kernel (by implicitly calling |
||
| if (TheCall->getNumArgs() < 1) { | ||
| // The builtin needs at least the kernel-name argument. This is unreachable | ||
| // through the SYCL_EXT_ONEAPI_KERNEL_FUNCTION macro (NAME is mandatory); | ||
| // the guard only protects the getArg(0) below for a direct zero-argument | ||
| // call. Reuse the generic builtin arg-count diagnostic (as the sibling | ||
| // __builtin_sycl_is_kernel family does) rather than a bespoke one. | ||
| Diag(TheCall->getBeginLoc(), diag::err_builtin_invalid_argument_count) << 1; | ||
| return ExprError(); | ||
| } | ||
|
|
||
| Expr *KernelNameExpr = TheCall->getArg(0); | ||
| SmallVector<Expr *, 8> LaunchArgs(TheCall->arguments().begin() + 1, | ||
| TheCall->arguments().end()); | ||
|
|
||
| // Depend on template parameters: defer until instantiation. When any launch | ||
| // argument (or the kernel name) is dependent, Sema::BuildCallExpr has already | ||
| // created this builtin call with a dependent result type and skipped custom | ||
| // type checking, so we simply return it; this handler re-runs on the | ||
| // instantiated, concrete-typed call. The deferred call keeps its BuiltinFn | ||
| // placeholder callee; the SYCL_EXT_ONEAPI_KERNEL_FUNCTION macro wraps the | ||
| // builtin in a unary plus so the enclosing kernel_function<...> NTTP argument | ||
| // is a UnaryOperator (a plain prvalue that does not recurse into this | ||
| // dependent CallExpr) rather than the CallExpr itself, which is what keeps | ||
| // classification off CallExpr::getCallReturnType and avoids a front-end | ||
| // crash. | ||
| if (KernelNameExpr->isTypeDependent() || KernelNameExpr->isValueDependent() || | ||
| Expr::hasAnyTypeDependentArguments(LaunchArgs)) | ||
| return TheCall; | ||
|
|
||
| // Build the synthetic call and let overload resolution + deduction run. This | ||
| // reuses the real C++ machinery so diagnostics (no viable overload, | ||
| // ambiguity, non-deducible template parameter) are emitted at the launch | ||
| // site for free. | ||
| ExprResult Call = SemaRef.BuildCallExpr(/*Scope=*/nullptr, KernelNameExpr, | ||
| KernelNameExpr->getBeginLoc(), | ||
| LaunchArgs, TheCall->getRParenLoc()); | ||
| if (Call.isInvalid()) | ||
| return ExprError(); | ||
|
|
||
| auto *ResolvedCall = dyn_cast<CallExpr>(Call.get()->IgnoreImpCasts()); | ||
| FunctionDecl *ResolvedFn = | ||
| ResolvedCall ? ResolvedCall->getDirectCallee() : nullptr; | ||
| if (!ResolvedFn) | ||
| // Overload resolution / deduction failed and already emitted diagnostics | ||
| // (BuildCallExpr can return a recovery expression rather than an invalid | ||
| // one), or the name did not resolve to a direct callee. Nothing to add. | ||
| return ExprError(); | ||
|
|
||
| // Free-function-kernel verification is only meaningful during device | ||
| // compilation. On the host pass the SYCL driver includes the integration | ||
| // footer, whose attribute-less redeclaration of the kernel would make | ||
| // isFreeFunction() spuriously fail; the host pass only needs the resolved | ||
| // function pointer for the runtime launch. The device pass performs the real | ||
| // validation. | ||
| if (getLangOpts().SYCLIsDevice) { | ||
| if (!isFreeFunction(ResolvedFn)) { | ||
| Diag(KernelNameExpr->getBeginLoc(), | ||
| diag::err_sycl_launch_kernel_not_free_function); | ||
| Diag(ResolvedFn->getLocation(), diag::note_previous_declaration); | ||
| return ExprError(); | ||
| } | ||
|
|
||
| // Force instantiation of the specialization so its definition is emitted | ||
| // and routed into the free-function-kernel collection/emission path exactly | ||
| // as the explicit kernel_function<Func> form does today. The synthetic Call | ||
| // above is discarded; no host call to the kernel is emitted. | ||
| SemaRef.MarkFunctionReferenced(KernelNameExpr->getBeginLoc(), ResolvedFn); | ||
| } | ||
|
|
||
| // Evaluate to &ResolvedFn: an ordinary function-pointer constant expression | ||
| // (the builtin CallExpr itself is not constant-foldable), usable directly as | ||
| // the `auto *Func` non-type template parameter of kernel_function<Func>. The | ||
| // emitted SPIR-V kernel is therefore the real user function (no wrapper) and | ||
| // the runtime launch path is unchanged. | ||
| QualType FnPtrTy = getASTContext().getPointerType(ResolvedFn->getType()); | ||
| ExprResult FnRef = | ||
| SemaRef.BuildDeclRefExpr(ResolvedFn, ResolvedFn->getType(), VK_LValue, | ||
| KernelNameExpr->getBeginLoc()); | ||
| if (FnRef.isInvalid()) | ||
| return ExprError(); | ||
| return SemaRef.ImpCastExprToType(FnRef.get(), FnPtrTy, | ||
| CK_FunctionToPointerDecay); | ||
| } | ||
|
|
||
| void SemaSYCL::ProcessFreeFunction(FunctionDecl *FD) { | ||
| if (isFreeFunction(FD)) { | ||
| if (CheckFreeFunctionDiagnostics(SemaRef, FD)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // RUN: %clang_cc1 -internal-isystem %S/Inputs -fsycl-is-device %s -verify | ||
|
|
||
| // Tests __builtin_sycl_launch_kernel: a bare (overloaded / templated) SYCL free | ||
| // function kernel name is resolved against the launch argument types via | ||
| // ordinary C++ overload resolution / template argument deduction, and the | ||
| // builtin evaluates to a pointer to the resolved specialization — usable as the | ||
| // `auto *Func` non-type template parameter of the enqueue-function launch path. | ||
| // This is the front-end seam the SYCL_EXT_ONEAPI_KERNEL_FUNCTION launch macro is built on. | ||
|
|
||
| #include "sycl.hpp" | ||
|
|
||
| template <typename T> | ||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-nd-range-kernel", 1)]] | ||
| void axpy(T *y, const T *x, T a, int n) { | ||
| for (int i = 0; i < n; ++i) | ||
| y[i] = a * x[i] + y[i]; | ||
| } | ||
|
|
||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-nd-range-kernel", 1)]] | ||
| void ovl(int *p) {} // expected-note {{candidate function not viable}} | ||
|
|
||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-nd-range-kernel", 1)]] | ||
| void ovl(float *p) {} // expected-note {{candidate function not viable}} | ||
|
|
||
| // A plain function that is NOT a free-function kernel. | ||
| __attribute__((sycl_device)) | ||
| void not_a_kernel(int *p) {} // expected-note {{previous declaration is here}} | ||
|
|
||
| // The builtin evaluates to a pointer to the resolved specialization, usable as | ||
| // an `auto *Func` non-type template parameter. | ||
| template <auto *Func> struct kernel_function_s {}; | ||
|
|
||
| void test() { | ||
| float *y = nullptr; | ||
| const float *x = nullptr; | ||
| int *ip = nullptr; | ||
| float *fp = nullptr; | ||
|
|
||
| // Deduce T = float from the pointer arguments; result folds to '&axpy<float>' | ||
| // and is usable as a non-type template argument. | ||
| kernel_function_s<__builtin_sycl_launch_kernel(axpy, y, x, 1.0f, 8)> k1; | ||
| (void)k1; | ||
|
|
||
| // Pick the right overload by argument type. | ||
| kernel_function_s<__builtin_sycl_launch_kernel(ovl, ip)> k2; | ||
| (void)k2; | ||
| kernel_function_s<__builtin_sycl_launch_kernel(ovl, fp)> k3; | ||
| (void)k3; | ||
|
|
||
| // No viable overload: no ovl(double*). | ||
| double *dp = nullptr; | ||
| // expected-error@+1 {{no matching function for call to 'ovl'}} | ||
| (void)__builtin_sycl_launch_kernel(ovl, dp); | ||
|
|
||
| // Resolves to a real function that is not a free-function kernel. | ||
| // expected-error@+1 {{is not a SYCL free function kernel}} | ||
| (void)__builtin_sycl_launch_kernel(not_a_kernel, ip); | ||
|
|
||
| // Missing kernel-name argument. | ||
| // expected-error@+1 {{builtin takes one argument}} | ||
| (void)__builtin_sycl_launch_kernel(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // RUN: %clang_cc1 -internal-isystem %S/Inputs -fsycl-is-device %s -verify | ||
|
|
||
| // Adversarial coverage for __builtin_sycl_launch_kernel name resolution: the | ||
| // kernel-name operand must survive the full range of C++ ways a function name | ||
| // can be spelled, both non-dependent and (via the '+' form the SYCL_EXT_ONEAPI_KERNEL_FUNCTION | ||
| // macro emits) inside templates with dependent arguments. | ||
| // | ||
| // These lock in the behaviors that were only checked manually before: | ||
| // - qualified names (ns::ft) | ||
| // - explicit template-ids (ns::ft<float>) | ||
| // - overloaded names resolved by argument type, including in a dependent | ||
| // context | ||
| // - mixed template parameters: a leading non-deducible NTTP given explicitly | ||
| // (Dim) plus a trailing deducible type parameter (T) | ||
| // - zero launch arguments | ||
|
|
||
| #include "sycl.hpp" | ||
|
|
||
| // expected-no-diagnostics | ||
|
|
||
| template <auto *Func> struct kernel_function_s {}; | ||
| template <auto *Func> constexpr kernel_function_s<Func> kernel_function{}; | ||
|
|
||
| namespace ns { | ||
| template <typename T> | ||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-nd-range-kernel", 1)]] | ||
| void ft(T *p, int n) {} | ||
| } // namespace ns | ||
|
|
||
| // Overload set, distinguished by argument type. | ||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-nd-range-kernel", 1)]] | ||
| void ovl(int *p) {} | ||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-nd-range-kernel", 1)]] | ||
| void ovl(float *p) {} | ||
|
|
||
| // Mixed: Dim is a non-deducible NTTP (appears in no parameter), T is deducible. | ||
| template <int Dim, typename T> | ||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-nd-range-kernel", 1)]] | ||
| void mix(T *p, int n) {} | ||
|
|
||
| // Zero-argument kernel. | ||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-single-task-kernel", "")]] | ||
| void noop() {} | ||
|
|
||
| // Non-dependent uses. | ||
| void non_dependent() { | ||
| float *p = nullptr; | ||
| int *ip = nullptr; | ||
|
|
||
| // Qualified name. | ||
| (void)kernel_function<+__builtin_sycl_launch_kernel(ns::ft, p, 8)>; | ||
| // Qualified explicit template-id. | ||
| (void)kernel_function<+__builtin_sycl_launch_kernel(ns::ft<float>, p, 8)>; | ||
| // Overload resolved by argument type. | ||
| (void)kernel_function<+__builtin_sycl_launch_kernel(ovl, ip)>; | ||
| (void)kernel_function<+__builtin_sycl_launch_kernel(ovl, p)>; | ||
| // Mixed: Dim=2 explicit (leading, non-deducible), T deduced from p. | ||
| (void)kernel_function<+__builtin_sycl_launch_kernel(mix<2>, p, 8)>; | ||
| // Zero launch arguments. | ||
| (void)kernel_function<+__builtin_sycl_launch_kernel(noop)>; | ||
| } | ||
|
|
||
| // Dependent uses (the '+' form the macro emits): resolution deferred to | ||
| // instantiation. These previously risked the front-end classification crash. | ||
| template <typename T> | ||
| auto dep_qualified(T *p) { | ||
| return kernel_function<+__builtin_sycl_launch_kernel(ns::ft, p, 8)>; | ||
| } | ||
| template <typename T> | ||
| auto dep_overload(T *p) { | ||
| return kernel_function<+__builtin_sycl_launch_kernel(ovl, p)>; | ||
| } | ||
| template <int Dim, typename T> | ||
| auto dep_mixed(T *p) { | ||
| return kernel_function<+__builtin_sycl_launch_kernel(mix<Dim>, p, 8)>; | ||
| } | ||
|
|
||
| void dependent() { | ||
| int *ip = nullptr; | ||
| float *fp = nullptr; | ||
| (void)dep_qualified(fp); // ns::ft<float> | ||
| (void)dep_overload(ip); // ovl(int*) | ||
| (void)dep_overload(fp); // ovl(float*) | ||
| (void)dep_mixed<3>(fp); // mix<3, float> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // RUN: %clang_cc1 -internal-isystem %S/Inputs -fsycl-is-device %s -verify | ||
|
|
||
| // Regression test: __builtin_sycl_launch_kernel used inside a template, with | ||
| // dependent launch arguments, must be deferred to instantiation rather than | ||
| // resolved eagerly. Using the raw builtin call as the `auto *Func` non-type | ||
| // template argument of kernel_function<...> would crash the front end while | ||
| // dependent: deducing the auto* parameter runs Expr::Classify on the builtin | ||
| // CallExpr, whose callee has the BuiltinFn placeholder type, and | ||
| // CallExpr::getCallReturnType asserts trying to castAs<FunctionType>. | ||
| // | ||
| // The SYCL_EXT_ONEAPI_KERNEL_FUNCTION macro wraps the builtin in a unary plus, so the non-type | ||
| // template argument is a UnaryOperator (whose classification is a plain | ||
| // prvalue and does not recurse into the dependent CallExpr) instead of the | ||
| // CallExpr itself. That is what makes the dependent case well-formed with no | ||
| // front-end change; this test spells the same `+` form the macro emits. | ||
| // (Unary plus on a function pointer is the identity, so the deduced Func value | ||
| // is unchanged.) | ||
|
|
||
| #include "sycl.hpp" | ||
|
|
||
| template <auto *Func> struct kernel_function_s {}; | ||
| template <auto *Func> constexpr kernel_function_s<Func> kernel_function{}; | ||
|
|
||
| template <typename T> | ||
| __attribute__((sycl_device)) | ||
| [[__sycl_detail__::add_ir_attributes_function("sycl-nd-range-kernel", 1)]] | ||
| void axpy(T *y, const T *x, T a, int n) {} | ||
|
|
||
| // Dependent-argument use inside a function template: deferred to instantiation. | ||
| template <typename T> | ||
| auto select(T *y, const T *x, T a, int n) { | ||
| return kernel_function<+__builtin_sycl_launch_kernel(axpy, y, x, a, n)>; | ||
| } | ||
|
|
||
| void ok() { | ||
| // expected-no-diagnostics | ||
| float *y = nullptr; | ||
| const float *x = nullptr; | ||
| (void)select(y, x, 1.0f, 8); // instantiates select<float>, resolves axpy<float> | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is a good name for what this builtin does. It doesn't actually launch a kernel; all it does is select an overload from an overload set based on a set of arguments. Other name suggestions:
__builtin_sycl_declcall(name stolen from https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2825r1.html).__builtin_sycl_kernel_selector__builtin_sycl_overload_resolver