-
Notifications
You must be signed in to change notification settings - Fork 69
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
Clarify group function inputs must be initialized #516
Clarify group function inputs must be initialized #516
Conversation
The group_broadcast and select_from_group functions are unlike the others, in that they conditionally exchange values between work-items in the group. Some developers may assume that it is safe for input arguments to be uninitialized as long as conditions are such that those same values are not exchanged with another work-item. However, using an uninitialized value in this way results in undefined behavior (according to C++), which may result in compilers performing optimizations that are incompatible with the semantics of group functions. This commit clarifies that the arguments passed to group_broadcast and select_from_group must be initialized on all work-items. Although this clarification is arguably unnecessary (since it is already covered by C++ rules), this is a common enough mistake that stating the precondition explicitly may help developers who are new to SYCL.
This commit adds a non-normative note to explain why certain group functions now explicitly require the values from all work-items to be initialized. The non-normative note appears at the beginning of the group functions section because this seemed preferable to repeating the note for each overload.
I don't think this a strong argument against this possible solution. It would be up to the implementation to make sure it is sufficient. An implementation could always implement
A high quality implementation might want to do something smarter or make sure that the extra branch gets optimized out. But that is purely an implementation problem. |
Hm. I think that might work, but I need to think about it some more. If we did adopt this as the solution, would you recommend that we change all of the group functions to accept Another of my concerns was that it might be difficult to identify cases where SYCL functions should accept |
We should think hard about what API breaking ramifications there are if we change these parameters from Here's one to get us started:
|
Not because of their collective nature. A user could write something like:
This would also be UB. The special thing about the collective SYCL function is that they take values they might not use. And therefore it is unintuitive if one doesn't know that pass-by-value counts as use. Also call-by-value and call-by-reference is indistinguishable looking at just the caller. Making the problem invisible without knowing the callee signature. Of course a user might write a What is unique about the a some of the collective functions among the existing SYCL functions is that they are the only ones which don't always use their pass-by-value parameter and therefore have this unintuitive "pass-by-value counts as use" problem. I don't like the current solution. If we were to clarify C++ we shouldn't do that for the group function but make this a general note because it also impacts user functions. But I don't think the SYCL spec is a good place to teach C++ and even if we wanted to it seems unclear why this particular C++ quirk needs to be called out. In particular because isn't particularly dangerous one, because if users follow best practice (compile with One could argue that it is bad practice for any function (both SYCL collective and user functions like If we were to change the collective functions we should either only do it for some or clarify that they never short-circuit. Because it wouldn't be obvious anymore that
This can be generalized: Any code that uses the function type in way that that it matters that the argument is a reference. Instead of decltype you can get the function type in other ways (e.g. template deduction). Not all uses of the function type would be a problem. E.g. it wouldn't matter if it's only used to get the return type. |
Clarifying for myself: If we change it to passing by reference, it is a semantic change. If someone is passing a global or static and they change its value somewhere else (worse, now as a race condition :-(), the change is noticed. That being said, IDK if this affects anything but contrived code. @Pennycook's solution adds default constructibility as a requirement on And all that being said, it really is a C++ gotcha. :-(. It is likely to be addressed in C++26 by P2795 Erroneous behaviour for uninitialized reads, as |
This is proving to be much more complicated than I expected. It sounds to me like the
If we leave things as they are and wait for P2795, what should the warning note look like? How would people feel about encouraging implementations to issue a warning in this case? Or should that be left entirely to quality of implementation? |
I think the change is more trouble than it is worth. IMO finding workarounds for C++ gotchas is a losing battle. Other than large things like buffers and arrays, users should be initializing all their variables. Even without optimizations, such initialization is typically in the noise. As for encouraging implementations to warn, I'm pretty sure gcc and clang already do with -Wuninitialized or -Wmaybe-uninitialized (both part of -Wall). |
You're right, they do. But you have to opt in to those. I was imagining that it might be a good idea for SYCL compilers to go beyond that, and issue a warning in this case unconditionally... But the more I think about it, the more I think it would be weird to talk about this in the specification. I still think there's a good chance that developers migrating to SYCL from other languages (e.g. CUDA) will encounter this and be confused. But perhaps the right fix there is to point this out during training or as part of dedicated migration resources. |
These clarifications were motivated by code that looks like this:
Some SYCL developers (including me!) might expect that this code to be safe, because:
x
before its value is broadcast to other work-itemsgroup_broadcast
call always returns the value from the leader, and values from other work-items are always discardedgroup_broadcast
is always assigned tox
in all work-items before it is usedHowever, because
group_broadcast
acceptsx
by value, C++ rules say thatgroup_broadcast
reads the uninitialized value(s) on all work-items. This is undefined behavior, and so compilers are free to optimize code like the above in surprising ways. For example, a compiler may decide that since the call togroup_broadcast
is only legal ifx
is initialized, andx
is only initialized ifsg.leader()
is true,sg.leader()
must be unconditionally true and thus the branch can be eliminated.The fix suggested by this commit is to clarify that the code above is illegal, and must be rewritten as:
Changing the declarations of the group functions to accept
T&
instead ofT
was considered as an alternative fix. Although it seems promising, there's no way for us (the writers of the SYCL specification) to change the definitions of other backend-specific APIs (e.g., in SPIR-V or CUDA) that may be used to implement these functions. If the backend-specific APIs ultimately take the arguments by value, changing the SYCL interface may not be sufficient to prevent unsafe optimizations.