Skip to content

[SYCL][2/2] Add nd_launch overloads taking kernel arguments as a span - #22971

Open
mieshkiwrk wants to merge 9 commits into
intel:syclfrom
mieshkiwrk:kernel-fastpath-span
Open

[SYCL][2/2] Add nd_launch overloads taking kernel arguments as a span#22971
mieshkiwrk wants to merge 9 commits into
intel:syclfrom
mieshkiwrk:kernel-fastpath-span

Conversation

@mieshkiwrk

@mieshkiwrk mieshkiwrk commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

*Depends on #22970

nd_launch(queue|handler, nd_range, const kernel &, args...) needs one instantiation per argument count, so a language runtime that only learns its argument list at run time has to carry a variadic dispatch table with a cap. Add an overload taking the arguments as a sycl::span<const raw_kernel_arg>: flat build cost, no cap, same direct submission path as the parameter pack overload.

A raw_kernel_arg carried only bytes, so every element of such a sequence bound as a value argument, and a pointer bound that way only reaches the kernel on Level Zero: OpenCL routes value arguments to clSetKernelArg, which rejects a USM pointer, and Native CPU stores the address of its own copy of the bytes. The argument list these overloads exist for is a pointer plus scalars, so the shape they are meant to serve was the one that did not work anywhere else. Hence the pointer form of raw_kernel_arg, which takes the address of the pointer the way the byte form takes the address of the bytes; SYCL_EXT_ONEAPI_RAW_KERNEL_ARG becomes 2.

A container of raw_kernel_arg converts to sycl::span, but a parameter pack is an exact match and wins overload resolution, so both parameter pack overloads now pass such an argument on to the span overload rather than binding the container object as a single argument, which is what the handler overload used to do silently for a std::array.

The review fixes from #22970 are carried here too, so the two stay aligned: cl_mem keeps the kind the handler gives it, and an array of scalars is bound as bytes on the fast path. A raw_kernel_arg stays outside that classification, since it says at run time which of the two kinds it carries.


More context: intel/intel-xpu-backend-for-triton#7737

nd_launch(queue, nd_range, const kernel &, args...) expands to submit() plus a
handler, so a kernel object never reaches the direct submission path that a
kernel function object already takes. For a language runtime that loads kernels
from a binary, the handler-less enqueue functions therefore save nothing.

Bind the arguments straight from the call and reuse
queue_impl::submit_kernel_scheduler_bypass with a real kernel_impl *, which that
function already accepts, whenever every argument can be bound as plain bytes.
Accessors, local accessors, streams and work group memory keep the command group
path, the same line HasSpecialCaptures draws in the runtime, and a dependency the
scheduler has to track still falls back to a command group.

KernelArgView is passed across the ABI boundary, so it gets its own header in an
inline versioned namespace with a layout test, following nd_range_view.
@slawekptak

Copy link
Copy Markdown
Contributor

@mieshkiwrk thanks for implementing this! Adding @uditagarwal97 as we talked about having handler-less sycl::kernel submission path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to specify if this is sycl::span or std::span. The same applies to some other places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is sycl::span, qualified now here and in the other places.

sycl::span is what SYCL uses as its own vocabulary type today - queue.hpp takes sycl::span<const event> all over, and the other extensions spell it that way too - so these overloads follow suit.

If SYCL ever moves to std::span, these overloads get aligned along with the rest of the API.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to use std::span here instead of sycl::span, and require applications to compile in C++20 mode in order to use these APIs. This is the direction we've been going for other extensions such as:

We have a similar direction also in the KHR extensions. For example, we rely on C++23 std::dextents here:

https://registry.khronos.org/SYCL/specs/sycl-2020/html/sycl-2020.html#sec:khr-group-interface

I believe it was a mistake for SYCL to "pre adopt" sycl::span because implementations will never be able to change this to be an alias to std::span without making an ABI break. TBH, I'm not sure how we are going to resolve this, but I'd like to stop relying on it in order to minimize the number of places that will be impacted if we do switch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed, both overloads take std::span<const raw_kernel_arg> behind __cpp_lib_span now, the spec marks them as C++20, and the exported entry points take a pointer and a count so sycl::span is out of the ABI as well. In C++17 a container would otherwise bind as a single kernel argument silently, so that is diagnosed.


One question: the feature-test macro still reports 2 in C++17, where version 2 of the extension does not exist - should it report 1 without __cpp_lib_span?

`is_plain_kernel_arg_v` classified through `std::decay_t`, which turns an array
into a pointer, so `nd_launch(queue, range, kernel, array, ...)` bound the array
as UR_EXP_KERNEL_ARG_TYPE_POINTER and the runtime read its first bytes as an
address. That binds neither the bytes nor the array: wrong results on Level Zero
and an abort inside the OpenCL driver, where the handler path binds the array as
plain bytes. Classify without decaying, so an array keeps using the command
group path, and cover both paths with a test.

The E2E test also passed a USM pointer through `raw_kernel_arg`, which binds as
a value argument and therefore only reaches the kernel on Level Zero. The
pointer is now passed typed, and the all-raw case moved to a Level Zero gated
test, the same restriction the RawKernelArg tests carry.

Pass the kernel bundle to the direct submission the way the handler path passes
it, so the device globals a bundle keeps to itself can be initialized.
The number of arguments is part of the type of a parameter pack, so a caller
that only learns its argument list at run time has to instantiate the pack
overload once for every count it may encounter. Add queue and handler overloads
taking span<const raw_kernel_arg>, where the count is data instead.

raw_kernel_arg is already type erased, so the two forms bind the same bytes and
measure the same; only the caller's build differs.

A container, or a non-const span, converts to span<const raw_kernel_arg>, but a
parameter pack is an exact match and wins overload resolution, which would bind
the container object itself as a single kernel argument. Diagnose that spelling
rather than let it produce wrong results at run time.

Bumps SYCL_EXT_ONEAPI_ENQUEUE_FUNCTIONS to 2 and documents the overloads.
A raw_kernel_arg carried only bytes, so the span overloads bound every element
as a value argument. A pointer bound that way only reaches the kernel on Level
Zero: the OpenCL adapter passes a value argument to clSetKernelArg, which
rejects a USM pointer with CL_INVALID_MEM_OBJECT, and the Native CPU adapter
puts the address of its own copy of the bytes into the argument slot. The
argument list these overloads exist for is a pointer plus scalars, so the shape
they are meant to serve was the one that did not work anywhere else.

Add a pointer form of raw_kernel_arg. It takes the address of the pointer, the
way the byte form takes the address of the bytes, so that passing the pointer
itself does not compile. handler::setArgHelper and both makeKernelArgView
overloads bind such an argument as a pointer. The graph extension needs no
change: a dynamic parameter stores the raw_kernel_arg object itself and an
update rewrites the bytes the node holds, which for a pointer argument are the
pointer. SYCL_EXT_ONEAPI_RAW_KERNEL_ARG becomes 2.

The E2E test of the span overloads no longer needs a Level Zero requirement,
and neither does the one that passes every argument through raw_kernel_arg. New
tests cover the handler path on OpenCL, a graph dynamic parameter that rebinds
a pointer, the layout of raw_kernel_arg, and that passing a pointer by value
does not compile.
The overloads taking the kernel arguments as a sequence take a sycl::span, which
a std::vector, a std::array or a std::span of raw_kernel_arg all convert to, but
a parameter pack is an exact match and wins overload resolution, so a container
reached the parameter pack overload instead. On the queue that was diagnosed. On
the handler it was not: a container that is trivially copyable, std::array among
them, compiled and bound the container object as a single kernel argument, and
one that is not produced an error from inside handler.hpp.

Have both parameter pack overloads pass such an argument on to the overload
taking a span, so that passing the container itself binds the arguments it
holds. The specification loses the clause that made such a call ill-formed,
along with a note claiming that sycl::span has no implicit conversion from a
container, which it does have.

Spell out sycl::span rather than span in the specification, since std::span is a
different type, and replace the test of the diagnostic with one that checks that
every spelling of an argument list compiles.
@mieshkiwrk
mieshkiwrk force-pushed the kernel-fastpath-span branch from 6423856 to 62fa8b3 Compare August 19, 2026 15:39
@mieshkiwrk
mieshkiwrk marked this pull request as ready for review August 31, 2026 14:55
@mieshkiwrk
mieshkiwrk requested a review from a team as a code owner August 31, 2026 14:55
@mieshkiwrk
mieshkiwrk requested a review from adamfidel August 31, 2026 14:55
`cl_mem` is a pointer typedef, so it was bound as a pointer argument and the
handle reached clSetKernelArgMemPointerINTEL as if it were an address, which
faults on the OpenCL backend. Bind it as the bytes of the handle, the exception
`handler::setArgHelper` makes for `OpenCLMemT`. A `raw_kernel_arg` keeps saying
at run time which of the two kinds it carries.

`makeKernelArgView` already bound an array as the bytes it is, but no trait let
one reach the fast path, so an array argument still went through the handler.
Class types, and arrays of them, keep the command group path: those may be
structs with special types inside.

Neither which arguments take the fast path nor the kind they are bound with is
observable end to end, hence the new test.

Extract the `KData` fields before the `CGExecKernel` construction that moves it,
the way `submit_kernel_direct_impl` does, and say why the argument pointer can be
cast.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do other people on @intel/dpcpp-specification-reviewers think about this? It seems a little weird to exclude certain types like this from the template overload above taking an Args pack. On the other hand, I do see that it makes this API taking span more convenient.

On the other other hand, the span overload seems like a ninja feature, so we could just ask those users to use a cast.

If we did want to add this exclusion, it should be expressed in the spec as a Constraint on the overload taking the Args pack:

Constraints: sizeof(Args...) is not one, or the single Arg is not convertible to std::span<const raw_kernel_arg>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case it helps the decision, a few measurements from trying the options locally:

  • With neither rule, a container is bound as one argument: numArgs = 1, one value argument holding the whole array. It compiles, then faults on Level Zero and errors in the OpenCL adapter at launch.
  • Constraining the pack overload alone sends a container to the kernel function object overload, not to the span one - it resolves as intended once that overload enforces the constraint it already has, its pack being sycl::reduction objects.
  • Requiring a cast need not leave that trap open - the container spelling can be diagnosed instead of bound, as it already is in C++17 - but the call site then spells std::span<const raw_kernel_arg>{args}, since std::span{args} deduces a non-const span.
  • Unaffected in all three: a single raw_kernel_arg, typed arguments, kernel function objects with and without reductions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I think this note is not needed in the specification. If you want to show the usage (like the automatic conversion from containers like std::array), add an Examples section to the bottom of the spec and add an example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the whole note. The usage it described moved to an Examples section at the end of the spec,
with the motivation kept as one line in the Overview.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like as good a place as any to add this comment ... Do we really need to call a special API on OpenCL to pass a kernel argument that is a pointer? What happens if you call the API that just sets raw bytes?

It seems weird that it wouldn't work because you can wrap the pointer in a struct, and then pass the struct by value. In that case, would wouldn't call the special "pointer" API to set the kernel argument. Why would this case be different from the case when the pointer is not wrapped in a struct?

CC: @bashbaug

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We ran into this passing a USM pointer through raw_kernel_arg, so I tested the spellings side by side with released headers, i.e. before this PR, on a Data Center GPU Max 1100:

backend      typed pointer   pointer as its bytes   pointer in a struct, by value
Level Zero   correct         correct                correct
OpenCL       correct         CL_INVALID_MEM_OBJECT  correct

So the struct case does work, as you say, and on Level Zero even the byte form does. What fails is the plain pointer argument on OpenCL - a struct by value is a value argument, so nothing is bound as a pointer there, whereas setting a pointer parameter goes through a different entry point than setting bytes. Native CPU differs again, and there the byte form would not even report an error.

The fix in this PR is to let an element say it is a pointer, since a raw_kernel_arg otherwise carries only an address and a length, which a pointer and a same-sized scalar share. The spelling is open - arg_kind on the byte constructor would work just as well - if you would prefer it that way.

The note carried two things a specification does not need in that place: why
the overloads taking a sequence of arguments exist, and how a container of
raw_kernel_arg reaches them. The first is motivation and moves to the Overview,
the second is usage and moves to an Examples section at the end of the
specification, which is where this extension shows how its APIs are called.
The overloads that take the arguments of a sycl::kernel as a sequence now take
a std::span, which makes them available only with C++20. That is the direction
the other extensions take: an implementation cannot turn sycl::span into an
alias of std::span without breaking the ABI, so a new API should not add another
place that would have to change if SYCL ever switches.

The exported entry points take a pointer and a count instead of a view, so the
spelling the public overloads use is not part of the ABI either and can be
changed again without breaking it.

Before C++20 there is no overload taking a sequence, so a container of
raw_kernel_arg handed to a parameter pack overload would be bound as a single
kernel argument, which compiles for any trivially copyable container and gives
wrong results. It is diagnosed instead, with a test that pins both the
diagnostic and that the same spellings compile with C++20.
Line wrapping only, as git-clang-format produces it for the previous commit.

The two test comments also said that a container bound as a single kernel
argument produces wrong results at run time. It does not get that far: the
launch faults on Level Zero and the OpenCL adapter rejects it, so the comments
now say that it only fails once the kernel is launched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants