Skip to content

[SYCL] Consistent is_device_copyable for sycl::accessor - #23250

Open
inteltdelisle wants to merge 22 commits into
intel:syclfrom
inteltdelisle:tdelisle/runtime/accessor_is_device_copyable
Open

inteltdelisle wants to merge 22 commits into
intel:syclfrom
inteltdelisle:tdelisle/runtime/accessor_is_device_copyable

Conversation

@inteltdelisle

@inteltdelisle inteltdelisle commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

sycl::is_device_copyable_v (and std::is_trivially_copyable_v) evaluated differently in host and device compilation, because the accessor special members are implemented differently under SYCL_DEVICE_ONLY.

For example, this compiled for the device but not for the host:

    static_assert(sycl::is_device_copyable_v<sycl::accessor<int, 1, sycl::access_mode::read_write>>);
    static_assert(sycl::is_device_copyable_v<sycl::local_accessor<int, 1>>);

Traits that differ between compilation passes can make constexpr/SFINAE results diverge between host and device.

Fix (behind -fpreview-breaking-changes, since it changes trivial copyability and is ABI-breaking):

Accessor types (accessor, local_accessor/local_accessor_base, host_accessor, and the image accessors) get a user-provided empty destructor, so they are consistently not trivially copyable and, per SYCL 2020, not implicitly device copyable. The copy/move constructors and assignment operators are explicitly defaulted to avoid issues with implicit functions.

Kernel parameter legality is now separate from device copyability (Not ABI-breaking):

New sycl::detail::is_legal_kernel_parameter trait that follows the SYCL 2020 "legal kernel parameter" rules: device copyable types, device-target accessors, arrays of legal types, and structs whose fields and bases are all legal and that contain a special SYCL type. Host-target accessors are rejected.

CheckDeviceCopyable is replaced by legal_kernel_parameters::validate_functor, which checks kernel captures against this trait. handler::set_arg has also been updated for consistency, reusing the trait were possible.
The integration header now specializes detail::is_legal_kernel_parameter instead of is_device_copyable for free-function structs that contain special types.

Compiler changes needed because accessors now have a non-trivial destructor:

SemaSYCL: finalize the destructor of special-object clones (FinalizeVarWithDestructor). Without this change, no definition is created for the dtor, since this is an inlined template, and calls to the destructor may fail to link.

CGDeclCXX: don't register a destructor for SYCL work-group-scope variables (e.g. the parallel_for_work_group kernel object). SYCL requires these destructors to have no effect and platforms are not required to declared/implement __cxx_atexit.

ESIMDVerifier: allow the local_accessor and local_accessor_base destructors.

Other: the accessor property-list conversion constructor is now constrained with enable_if instead of a static_assert, so CTAD and same-type copies pick the copy constructor.

Known limitation:

A struct that contains only legal members but has an illegal user-defined copy constructor or destructor is not detected.

Follow-up on the work-group scope attribute handling: #23461.

Tests:

New/updated tests for accessor special members, implicitly device copyable SYCL types, is_device_copyable (positive and negative), and set_arg. Each has RUN lines with and without -fpreview-breaking-changes.

Previously, is_device_copyable_v evaluated to different values for
device and host code. Added explicit instantiation to consistently
return false.
Also added tests for other basic sycl types.

Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
@inteltdelisle
inteltdelisle requested a review from a team as a code owner September 23, 2026 18:21

@koparasy koparasy left a comment

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.

A high level comment. What makes accessors change their behavior depending on host|device compilation?

@inteltdelisle

Copy link
Copy Markdown
Contributor Author

A high level comment. What makes accessors change their behavior depending on host|device compilation?

By default is_device_copyable simply falls back on std::is_trivially_copyable, which in parts look at what the ctors are doing. sycl::accessor ctors are implemented differently depending on whether ___SYCL_DEVICE_ONLY__ is defined and that is what was causing the difference.

@slawekptak

Copy link
Copy Markdown
Contributor

@inteltdelisle thanks for the fix. Could you please describe a bit of a background for this change - was some specific test failing? Thanks!
Adding @gmlueck

@inteltdelisle

Copy link
Copy Markdown
Contributor Author

This fix was of result of a bug report submitted by @koparasy, who noted that the following code was inconsistent between device and host compilation:

#include <sycl/sycl.hpp>

using AccT = sycl::accessor<int, 1, sycl::access_mode::read_write>;
using LAccT = sycl::local_accessor<int, 1>;

static_assert(
   sycl::is_device_copyable_v<AccT>,
   "DEVICE pass: accessor IS device-copyable (trivially copyable here)");
static_assert(sycl::is_device_copyable_v<LAccT>,
             "DEVICE pass: local_accessor IS device-copyable here");
void f() {}

This code compiled for the device but not the host. The additional tests are simply there to verify the behavior of other basic sycl types.

Worth noting that I did not add a test to verify the behavior of is_device_copyable<buffer<...>>, I am not sure what the desired behavior is for that type.

Comment thread sycl/include/sycl/accessor.hpp Outdated
template <typename DataT, int Dimensions, access_mode AccessMode>
struct is_device_copyable<host_accessor<DataT, Dimensions, AccessMode>>
: std::false_type {};

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 wonder if we should change the implementation of the accessor instead of specializing is_device_copyable. For example, adding an empty user-provided destructor will cause the types to be non-trivially copyable.

This seems better than specializing the trait because it also makes is_trivially_copyable consistent between host and device.

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.

I'll confirm locally whether that works. I was hesitant do change the accessor type itself, since that will have a less targeted impact. On the other hand the spec does state "Any type that is trivially copyable (as defined by the C++ core language) is implicitly device copyable." so that solution match the spec more closely.

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.

@gmlueck Adding the dtors instead works locally. I can push that change if it's a preferred solution.

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.

Spec-wise, I think it's better. Maybe @intel/llvm-reviewers-runtime can weigh in from the implementation side.

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.

@inteltdelisle Changing a type's trivial-copyability is an ABI breaking change. I am on board with @gmlueck's suggestion, but I think we should add the user-provided destructor under PREVIEW_BREAKING_CHANGES flag.

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.

@gmlueck Does SYCL require std::is_trivially_copyable_v<accessor> itself to evaluate consistently with sycl::is_device_copyable_v<accessor>? If not, wouldn't an explicit specialization of is_device_copyable be preferable to deliberately making the destructor non-trivial, since the latter changes additional C++ type properties unrelated to device copyability?

I am also wondering whether we actually want to break code that may rely on std::is_trivially_copyable_v<accessor>, even if the current behavior is somewhat obscure and differs between host and device compilation.

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.

In general, I think it's bad for traits to evaluate differently in the host vs. the device compilation passes because this can lead to constexpr expressions having different values in the two passes. I think this could lead to quite confusing behavior (e.g. SFINAE constraints evaluating different in the two passes). Therefore, I think std::is_trivially_copyable_v<accessor> should evaluate the same in both passes.

TBH, I think the SYCL spec should be changed to guarantee that all standard traits and all SYCL traits evaluate the same in all compiler passes. This is a battle for another day, though.

I agree that changing the trait in the device compiler could potentially break an application (thought I think it's pretty unlikely). Therefore, I agree that it's better to make this change at the next major release.

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.

@gmlueck I'm not super familiar with the release procedure yet, what does making this change at the next major release entail precisely? Do we merge this change in behind the PREVIEW_BREAKING_CHANGES?

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.

Yes, that's right. You would merge the change as it is now in this PR behind the PREVIEW_BREAKING_CHANGES macro. At the next major release, we search for all these macros and enable the code they protect by default. Merging the changes now under the macro provides two benefits:

  • It allows user to see what potential breaking changes might be coming in the next major release by compiling with -fpreview-breaking-changes.
  • It lets us address issues like this early, rather than waiting until right before the next major release.

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.

Thnx!

Accessors are no longer trivially copyable to match the sycl spec more
closely.
Change is behind PREVIEW_BREAKING_CHANGES.

Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
Previous definition of CheckDeviceCopyable was relying on
is_device_copyable as a shortcut for checking kernel parameter legality.
Renamed to CheckKernelParametersAreLegal and re-wrote implementation to
match the sycl 2020 spec more closely.

Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
Since sycl::accessor is not trivially copyable, special types require
the normal destructor machinery.

Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
@inteltdelisle
inteltdelisle requested a review from a team as a code owner October 5, 2026 18:57
Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
@inteltdelisle
inteltdelisle requested a review from a team as a code owner October 7, 2026 13:50

Copilot AI left a comment

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.

🟡 Changes recommended

Kernel legality predicates currently admit invalid host accessors, host-target accessors, and arbitrary non-device-copyable arguments.

4 open findings
What changed in this PR

Makes accessor device-copyability consistent while preserving legal kernel captures.

Changes:

  • Adds non-trivial accessor destructors and explicit copy/move operations.
  • Reworks kernel-parameter legality checks.
  • Updates compiler lowering, ESIMD verification, and type-trait tests.
File Description
sycl/​test/​basic_tests/​implicit_device_copyable_types.cpp Adds device-copyability assertions.
sycl/​include/​sycl/​handler.hpp Broadens set_arg eligibility.
sycl/​include/​sycl/​detail/​kernel_launch_helper.hpp Invokes the renamed legality check.
sycl/​include/​sycl/​detail/​is_device_copyable.hpp Implements recursive kernel-parameter legality checks.
sycl/​include/​sycl/​accessor.hpp Adds accessor destructors, special members, and legality traits.
llvm/​lib/​SYCLLowerIR/​ESIMD/​ESIMDVerifier.cpp Allows local-accessor destructors in ESIMD.
clang/​lib/​Sema/​SemaSYCL.cpp Finalizes generated variables with destructors.
clang/​lib/​CodeGen/​CGDeclCXX.cpp Skips work-group-scope destructor registration.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread sycl/include/sycl/accessor.hpp Outdated
Comment on lines +2891 to +2896
template <typename DataT, int Dimensions, access::mode AccessMode,
access::target AccessTarget, access::placeholder IsPlaceholder,
typename PropertyListT>
struct is_sycl_accessor<accessor<DataT, Dimensions, AccessMode, AccessTarget,
IsPlaceholder, PropertyListT>>
: std::true_type {};
Comment thread sycl/include/sycl/accessor.hpp Outdated
Comment on lines +2901 to +2903
template <typename DataT, int Dimensions, access_mode AccessMode>
struct is_sycl_accessor<host_accessor<DataT, Dimensions, AccessMode>>
: std::true_type {};
Comment thread sycl/include/sycl/handler.hpp Outdated
Comment on lines +1187 to +1190
|| (!std::is_abstract_v<remove_cv_ref_t<T>> &&
!std::is_polymorphic_v<remove_cv_ref_t<T>> &&
std::is_destructible_v<remove_cv_ref_t<T>> &&
std::is_copy_constructible_v<remove_cv_ref_t<T>>);
Comment on lines +123 to +128
#ifdef __INTEL_PREVIEW_BREAKING_CHANGES
static_assert(!sycl::is_device_copyable_v<
sycl::accessor<int, 1, sycl::access_mode::read_write>>);
static_assert(!sycl::is_device_copyable_v<sycl::local_accessor<int, 1>>);
static_assert(!sycl::is_device_copyable_v<sycl::host_accessor<int, 1>>);
#endif // __INTEL_PREVIEW_BREAKING_CHANGES

@elizabethandrews elizabethandrews left a comment

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.

Please add a FE test.

Comment thread clang/lib/CodeGen/CGDeclCXX.cpp Resolved
Comment thread clang/lib/CodeGen/CGDeclCXX.cpp Resolved
Updated is_legal_kernel_parameter to no longer accept any types composed
of legal types.
Changes to is device copyable extended to accessors.
Added tests to cover new changes.

Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
@inteltdelisle

Copy link
Copy Markdown
Contributor Author

@gmlueck @koparasy Now that the tests are passing, I believe this is the set of changes that are needed to get is_device_copyable to be consistent for accessors across host and device. I made accessors no-longer trivially copyable by making the destructors non-trivial. I did not investigate the alternative solution of making the copy/move constructors non trivial. (Claude Code assumes that alternative isn't simpler).

Any thoughts on this approach?

@koparasy

Copy link
Copy Markdown
Contributor

@gmlueck @koparasy Now that the tests are passing, I believe this is the set of changes that are needed to get is_device_copyable to be consistent for accessors across host and device. I made accessors no-longer trivially copyable by making the destructors non-trivial. I did not investigate the alternative solution of making the copy/move constructors non trivial. (Claude Code assumes that alternative isn't simpler).

Any thoughts on this approach?

I will come back monday about this. Initially I imagined this will be a trivial direction. I got that wrong. I need to reason about this PR a little deeper.

@koparasy koparasy left a comment

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.

With is_legal_kernel_parameter_v in ShouldEnableSetArg, the generic set_arg(int, T&&) is now enabled on host for every type is_sycl_accessor matches. Accessors with the default property list still reach the accessor-specific setArgHelper, but an accessor with a non-empty accessor_property_list, and unsampled_image_accessor / sampled_image_accessor, have no matching overload. IIUC they fall into the generic setArgHelper(T&&), which copies the host object bytes as kind_std_layout. Previously these failed to compile; now they compile and pass a wrong argument. Could you verify how those are resolved with some tests, and act accordingly?

Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
@inteltdelisle

Copy link
Copy Markdown
Contributor Author

With is_legal_kernel_parameter_v in ShouldEnableSetArg, the generic set_arg(int, T&&) is now enabled on host for every type is_sycl_accessor matches. Accessors with the default property list still reach the accessor-specific setArgHelper, but an accessor with a non-empty accessor_property_list, and unsampled_image_accessor / sampled_image_accessor, have no matching overload. IIUC they fall into the generic setArgHelper(T&&), which copies the host object bytes as kind_std_layout. Previously these failed to compile; now they compile and pass a wrong argument. Could you verify how those are resolved with some tests, and act accordingly?

I refined the condition so that it bahaviour is unchanged and added tests to confirm this. I also added a static assert to make sure the std layour is not used incorrectly.

@koparasy

Copy link
Copy Markdown
Contributor

@inteltdelisle thanks. This is way more complex, invasive than what I had initially imagined. Saying this does not mean you are doing anything wrong. Friday I will try to spend some time to think if we can simplify this. But overall this looks correct to me now.

@koparasy

koparasy commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

@inteltdelisle a type is trivially copyable only if all of its copy/move operations are trivial, assignment included. An = default written outside the class counts as user-provided, so the operator is non-trivial but behaves exactly like the default. So if you have something like this:

// in class
accessor(const accessor &) = default;
accessor(accessor &&) = default;
accessor &operator=(const accessor &);
accessor &operator=(accessor &&);
// out of line
template <...> accessor<...> &accessor<...>::operator=(const accessor &) noexcept  = default;
template <...> accessor<...> &accessor<...>::operator=(accessor &&) noexcept  = default;

It should mark the accessors non copyable on both sides. By doing this, you no longer need the compiler changes (I guess).

Signed-off-by: Delisle, Thierry <thierry.delisle@intel.com>
@inteltdelisle

inteltdelisle commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

@inteltdelisle a type is trivially copyable only if all of its copy/move operations are trivial, assignment included. An = default written outside the class counts as user-provided, so the operator is non-trivial but behaves exactly like the default. So if you have something like this:

// in class
accessor(const accessor &) = default;
accessor(accessor &&) = default;
accessor &operator=(const accessor &);
accessor &operator=(accessor &&);
// out of line
template <...> accessor<...> &accessor<...>::operator=(const accessor &) noexcept  = default;
template <...> accessor<...> &accessor<...>::operator=(accessor &&) noexcept  = default;

It should mark the accessors non copyable on both sides. By doing this, you no longer need the compiler changes (I guess).

I pushed a commit to switch to this approach. One side effect to note here is that std::is_trivially_destructible will now return different results for accessors on the host and device side.

/// static storage duration.
static void EmitDeclDestroy(CodeGenFunction &CGF, const VarDecl &D,
ConstantAddress Addr) {

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.

Remove this.

@koparasy

koparasy commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

@inteltdelisle a type is trivially copyable only if all of its copy/move operations are trivial, assignment included. An = default written outside the class counts as user-provided, so the operator is non-trivial but behaves exactly like the default. So if you have something like this:

// in class
accessor(const accessor &) = default;
accessor(accessor &&) = default;
accessor &operator=(const accessor &);
accessor &operator=(accessor &&);
// out of line
template <...> accessor<...> &accessor<...>::operator=(const accessor &) noexcept  = default;
template <...> accessor<...> &accessor<...>::operator=(accessor &&) noexcept  = default;

It should mark the accessors non copyable on both sides. By doing this, you no longer need the compiler changes (I guess).

I pushed a commit to switch to this approach. One side effect to note here is that std::is_trivially_destructible will no return different results for accessors on the host and device side.

Do you mean "will NOT return .." or "will now return". If it is the later, we probably don't want that. Your previous solution in that case would be better.

@inteltdelisle

Copy link
Copy Markdown
Contributor Author

Do you mean "will NOT return .." or "will now return". If it is the later, we probably don't want that. Your previous solution in that case would be better.

"Will now return". Although it's more accurate to say, will revert to destructors only being trivially destructible on the device, which is the current status before this PR.

@elizabethandrews elizabethandrews left a comment

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.

@tahonermann can you also take a look please. The code changes look ok to me but I am not sure if the functional change has consequences I am not thinking off.

// work-group local memory. There is no program exit on the device to run
// their destructor at, and SYCL 2020 requires the destructors of device
// copyable types to have no effect on the device, so don't register one.
if (CGF.getLangOpts().SYCLIsDevice) {

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.

The comment does not fully cover the actual code changes. All locals are affected IIUC. The SYCL 2020 rule refers to only kernel captures but this change affects a variable declared in the kernel bocy as well right?.

In any case is this code even hit after after #23702 ?

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.

I believe you are correct that the changes to this file can be reverted once #23702 is in. But I'm not sure whether #23702 should go in until #23700 is fixed. So I'm not entirely sure how to proceed here.

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 you can just update the comment to describe the status as well. Maybe add a FIXME so we can keep track of its removal.

// Special type with a non-trivial destructor, like the device accessors. The
// destructor of the object created in the kernel to pass to the free function
// must be instantiated and defined, not just declared.
template <typename DataT>

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'm not sure if we should update the mock headers to include the destructor under some macro for now? @tahonermann can you weigh in?

@@ -0,0 +1,29 @@
// RUN: %clang_cc1 -fsycl-is-device -triple spir64-unknown-unknown -disable-llvm-passes -Wno-sycl-work-group-dtor -emit-llvm %s -o - | FileCheck %s

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 see you passed -Wno-sycl-work-group-dtor. This PR adds a destructor to accessors, so wouldn't that mean that this warning would fire for every parallel_for_work_group kernel capturing an accessor ? Is this expected behavior?

q.submit([&](handler &h) {
NonTrivial Captured;
h.parallel_for_work_group<class kernel>(
range<1>{1}, range<1>{1}, [=](group<1> G) { (void)Captured; });

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.

Could you also add a local declared inside the parallel_for_work_group body with a non-trivial destructor (not captured).

// CHECK-NEXT: %0 = load ptr addrspace(4), ptr addrspace(4) %__arg__obj.addr.ascast, align 8
// CHECK-NEXT: call spir_func void @{{.*}}annotated_ptr{{.*}}__init{{.*}}

// Special type with a non-trivial destructor, like the device accessors. The

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.

Nit: Comment says "the device accessors". Test is for __SYCL_TYPE(work_group_memory)

This branch has not been deployed

No deployments
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.

7 participants