-
Notifications
You must be signed in to change notification settings - Fork 740
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
[SYCL] Add indeterminate constructor to work group memory interface #16003
[SYCL] Add indeterminate constructor to work group memory interface #16003
Conversation
// Frontend requires special types to have a default constructor in device | ||
// compilation mode in order to have a uniform way of initializing an object of | ||
// special type to then call the __init method on it. This is purely an | ||
// implementation detail and not part of the spec. | ||
#ifdef __SYCL_DEVICE_ONLY__ | ||
work_group_memory() = default; | ||
#endif |
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 legal. Doesn't it violate ODR?
This is observable to the user, so isn't just an implementation detail. Exposing a public default constructor allows a user to call work_group_memory()
in device code, and also causes std::is_default_constructible_v<work_group_memory>
to produce different values on host and device (which could break all sorts of things).
One fix would be to make this a protected function instead, and then create a factory function in the detail::
namespace that creates an uninitialized work_group_memory
and calls __init
on it.
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.
This compiles fine. Exposing it to the user in device code is a problem, but unfortunately the way our frontend is set-up it requires special types to have a default constructor so that it will initialize them using that constructor in device code and then call __init
on it. I did explore other avenues but the frontend team, @elizabethandrews , really don't like to have different handlings for different special types. Presumably, this sets a bad precedent for the future where other special types will also have their own hacks like this and the code will be non-uniform and harder to reason about and in my opinion its a totally valid concern.
Surprisingly, another class which does not have a default constructor as per the spec but seems to add one in device compilation mode is sycl::stream
.( see line 862 in stream.hpp
)
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 didn't mean to imply that the code wouldn't compile. I think it's invalid because it results in multiple definitions of the same class (one for the host, one for the device), because it's not what the specification says, and because it could break user code in surprising ways.
I didn't realize we were already doing this for sycl::stream
. I don't think we should be doing that, either.
Since the function call is only required by the front-end, can we just mark these default constructors private, and then ask the front-end to ignore the access specifier and call it anyway?
Tagging @gmlueck for awareness.
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.
We could probably use some knowledge input from @intel/dpcpp-cfe-reviewers.
Can we do what @Pennycook suggested and have the Semantic analyzer ignore access specifiers just for the duration of this default initialization for special types?
This would allow us to mark these default constructors private for classes that do not have them in the spec so that users dont accidentally call them. This would be something that would apply to all special types, so we would preserve uniformity.
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 didn't mean to imply that the code wouldn't compile. I think it's invalid because it results in multiple definitions of the same class (one for the host, one for the device), because it's not what the specification says, and because it could break user code in surprising ways.
I didn't realize we were already doing this for
sycl::stream
. I don't think we should be doing that, either.Since the function call is only required by the front-end, can we just mark these default constructors private, and then ask the front-end to ignore the access specifier and call it anyway?
Tagging @gmlueck for awareness.
I made the requested changes.
Let's see if this looks ok to @intel/dpcpp-cfe-reviewers .
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.
Sorry, our comments crossed. I'm not opposed to your option (2) if that seems less hacky. If we do that, can we put the factory function in the
detail
namespace?
I think so? I'm just not sure the effort required for this off the top of my head. As Tom mentioned, it may just not be worth it at this point. We could just go with option 1 adding a comment along the lines of "This is a temporary hack" and focus on designing whatever we upstream.
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.
Either (1) or (2) is fine with me. I'm happy as long as we don't expose public APIs to the user which are not official public APIs.
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.
Personally, I don't see (1) as any more of a hack than the use of the __init
function as a whole. It is the compiler making the decision to use some member function(s) of the type, by instruction of the directives we give it. Luckily it isn't a user-facing feature, so we only have ourselves to blame if we mess it up.
Note also that the __init()
member function can be - and in most classes is - declared private. @lbushi25 - Could we also move that to private while we are at it?
@tahonermann & @elizabethandrews - SYCLSpecialClassDocs in clang/include/clang/Basic/AttrDocs.td does not mention the default ctor requirement, though it has been a requirement for as long as I can remember. Should we amend it and, if we go with (1) here, note the access specifier relaxation?
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 we need to update the documentation. Comments in the code should suffice since it is an incorrect implementation assumption which we need to change eventually.
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 agree with @elizabethandrews. We'll need to revisit the docs with whatever design we publish upstream anyway.
…tor access specifier
…tor access specifier
…tor access specifier
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.
The changes in e531b05 address my concerns about the implementation of work_group_memory
following the specification. I defer to @intel/dpcpp-cfe-reviewers for an assessment of whether a factory function or ignoring the access specifiers is an acceptable solution.
addFieldInit(FD, Ty, std::nullopt, | ||
InitializationKind::CreateDefault(KernelCallerSrcLoc)); | ||
|
||
DefaultConstructor->setAccess(DefaultConstructorAccess); |
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.
Does it actually complain during kernel construction if you don't set access?
If yes, does it complain when it should after this change?
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.
Does it actually complain during kernel construction if you don't set access? If yes, does it complain when it should after this change?
During the construction of the SYCL Kernel Body, it complains that the constructor has an incorrect number of arguments because it doesn't see a public default constructor. After this change, it doesn't complain anymore during kernel construction but it does complain when using the default constructor in any other place for example if the user uses the default constructor, they will get a compilation error because its not public to them. Its also an approach that works for any special type so we don't have to change our handling based on the special type.
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.
Does it actually complain during kernel construction if you don't set access? If yes, does it complain when it should after this change?
This line in particular that you have reviewed sets the access back to its original value so that we don't accidentally leave the default constructor visible when it shouldn't be. Frankly, even without this line it seemed like the user could still not call the default constructor if it was defined private, but I chose to err on the safe side.
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 see. I suppose I would also prefer the safe side.
Shouldn't you be doing the same for each variant of handleSpecialType
?
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 see. I suppose I would also prefer the safe side. Shouldn't you be doing the same for each variant of
handleSpecialType
?
Good question. This change is only relevant for the semantic action of the SyclKernelBodyCreator
class which has 3 members named handleSpecialType
. One is for kernel handler which has its own thing going on so I don't think it needs these changes, the other is the one I've changed here and the remaining one is the one taking a CXXBaseSpecifier
. I suppose I should add it to this one too but I forgot, is that what you were asking about?
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 suppose I should add it to this one too but I forgot, is that what you were asking about?
Right. Passing precommit also suggests lack of corresponding tests. I suppose a test should contain work group memory as a base to exercise the code.
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.
Ok. I will add such a test.
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 do not have another solution but this just feels very hacky and I really doubt community clang will accept this. This is out of the scope of the PR but the design of how we handle initialization of special types probably needs to change if we cannot use a default constructor. @tahonermann please weigh in here.
@Pennycook @gmlueck |
@intel/llvm-gatekeepers This is ready to merge. |
Implement the changes made to the work group memory spec in this PR: #15933.
Essentially, the default constructor is removed from the spec and instead a constructor that takes an indeterminate argument is made available in order to emphasize that any operation on such a work group memory object is undefined behavior except for assigning to it another work group memory object. Because the frontend needs special types to have a default constructor, we make it private instead and when its time for the frontend to call it, simply override the access specifier temporarily.