Skip to content
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

Merged

Conversation

lbushi25
Copy link
Contributor

@lbushi25 lbushi25 commented Nov 6, 2024

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.

Comment on lines 53 to 59
// 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
Copy link
Contributor

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.

Copy link
Contributor Author

@lbushi25 lbushi25 Nov 7, 2024

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)

Copy link
Contributor

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.

Copy link
Contributor Author

@lbushi25 lbushi25 Nov 8, 2024

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.

Copy link
Contributor Author

@lbushi25 lbushi25 Nov 8, 2024

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 .

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

@steffenlarsen steffenlarsen Nov 14, 2024

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

@Pennycook Pennycook left a 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);
Copy link
Contributor

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?

Copy link
Contributor Author

@lbushi25 lbushi25 Nov 12, 2024

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

@lbushi25 lbushi25 Nov 12, 2024

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@lbushi25
Copy link
Contributor Author

lbushi25 commented Nov 12, 2024

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.

@Pennycook @gmlueck
Based on @tahonermann's feedback, I've reverted back to following the precedent set by sycl::stream, that is, making the default constructor public. I've also opened an issue about this and put a link in a comment. I realize this is deviating from the spec but based on the discussion here, this seems to be the consensus solution at least in the short term. We either have the option to accept this until the frontend handling changes or we can revert the substitution of the default constructor with the one taking an indeterminate as an argument.

@lbushi25
Copy link
Contributor Author

@intel/llvm-gatekeepers This is ready to merge.

@martygrant martygrant merged commit d76b552 into intel:sycl Nov 15, 2024
13 checks passed
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.