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

[RFC][POC] Suggest re-design of compile-time properties extension #15752

Closed
wants to merge 33 commits into from

Conversation

aelovikov-intel
Copy link
Contributor

@aelovikov-intel aelovikov-intel commented Oct 18, 2024

This is based on @rolandschulz 's #13776 and #13669, with lots of help from both @rolandschulz and @tahonermann in offline chats.

This is a proof of concept on how the extension can be changed. The purpose of this PR is to start discussion on

  1. If the proposed redesign is acceptable/desirable
  2. If so, how should we implement it? Should we have a single PR that updates the extension, implementation and uses all at once? If yes, can we do that outside of the major release?

I plan on drafting the changes to the extension later on once the implementation prototype is closer to finalization.

int x = 42;
properties pl{ct_prop<42>{}, rt_prop{x}};
constexpr auto p = pl.get_property<struct ct_prop_key>();
static_assert(std::is_same_v<decltype(p), const ct_prop<42>>);
Copy link
Contributor Author

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 const ct_prop<42> instead of prop<42> is an issue, and if it is then I don't know how to address it.

Comment on lines 77 to 80
foo(+prop<42>);
// More than one property in a property list looks very natural.
// Alternatively, that can be `operator|` but it has no unary version.
foo(prop<42> + other_prop);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We want to provide a simplified user interface like this, but it's unclear what's the best way to implement this. This file sketches few possible approaches starting from here.

Comment on lines 950 to 958
template <typename property_list_ty>
inline constexpr bool are_properties_valid_for_create_bundle_from_source =
new_properties::all_properties_in_v<property_list_ty, include_files>;

template <typename property_list_ty>
inline constexpr bool are_properties_valid_for_build_source_bundle =
new_properties::all_properties_in_v<property_list_ty, build_options,
save_log, registered_kernel_names>;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is subjective, but I think this is better than having each property specialize is_property_key_of as all the supported properties of something are listed in one place.

Comment on lines 41 to 42
foo(properties{prop<42>{}});
foo(properties{prop<42>{}, other_prop{}});
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the simplicity of this. I think the old approach required us to define too many things with the same/similar names, and so if we can avoid having to define the shortcuts that's probably better for everybody.

Another benefit of exposing that these property values are objects is that developers will naturally have a better understanding of the difference between properties with compile-time and run-time values:

  • prop<42>{} is clearly an instance of a class storing 42 as a compile-time value
  • prop(42) is clearly an instance of a class storing 42 as a run-time value

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 agree. I think it's still clear that prop<42> is a compile-time property (without the {}), and I like that it's two characters shorter.

Also, I think the inline constexpr values are a verbosity saver for compile-time properties that have an enumeration. Consider the host_acess property from sycl_ext_oneapi_device_global:

properties props{host_access_read};                       // with inline constexpr variable
properties props{host_access<host_access_enum::read>{}};  // without

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 agree. I think it's still clear that prop<42> is a compile-time property (without the {}), and I like that it's two characters shorter.

It's shorter, but it's inconsistent. In the situation where prop supports both compile-time and run-time values, the compile-time value would be used as prop<42> and the run-time value would be used as prop_property(42). Pairing prop<42>{} and prop(42) is more consistent, and we don't require developers to learn any additional naming conventions.

Also, I think the inline constexpr values are a verbosity saver for compile-time properties that have an enumeration. Consider the host_acess property from [sycl_ext_oneapi_device_global]> (https://github.com/intel/llvm/blob/sycl/sycl/doc/extensions/experimental/sycl_ext_oneapi_device_global.asciidoc)

I'm not opposed to creating shorthand aliases for compile-time properties with an enumeration. But we can have a shorthand and still expose everything as a type -- we'd just have to use a type alias instead of a variable:

using host_access_read = host_access<host_access_enum::read>;
...
properties props{host_access_read{}};                    // with shorthand
properties props{host_access<host_access_enum::read>{}}; // without shorthand

Copy link
Contributor

Choose a reason for hiding this comment

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

It's shorter, but it's inconsistent. In the situation where prop supports both compile-time and run-time values, the compile-time value would be used as prop<42> and the run-time value would be used as prop_property(42). Pairing prop<42>{} and prop(42) is more consistent, and we don't require developers to learn any additional naming conventions.

We had this discussion before. There are no properties today that mix compile-time and runtime values, and we don't have any immediate plans to add any like this. I do not think it makes sense to optimize for such a weird case, which will probably never happen.

Copy link
Contributor

@Pennycook Pennycook Oct 24, 2024

Choose a reason for hiding this comment

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

We had this discussion before.

I have a vague recollection of this, but I thought it was with an older design of properties, where the shorthands were more essential. Andrei's proposal here is much simpler, to the point that I don't think we need the aliases anymore, and that's why I think we should discuss it again.

Consider the current definition of sub_group_size:

struct sub_group_size_key {
  template <uint32_t Size>
  using value_t = property_value<sub_group_size_key, std::integral_constant<uint32_t, Size>>;
};

template <uint32_t Size>
inline constexpr sub_group_size_key::value_t<Size> sub_group_size;

Typing sub_group_size<32> is clearly much better than typing sub_group_size_key::value_t<32>{}, and defining an alias hides all the implementation complexity from the user.

I don't think typing sub_group_size<32> instead of sub_group_size<32>{} is enough of a gain to justify introducing an otherwise unnecessary sub_group_size_property class.

There are no properties today that mix compile-time and runtime values, and we don't have any immediate plans to add any like this.

I hadn't realized at the time, but @rolandschulz and I were actually discussing a case like this late last week.

If we add a property to control the size of the work-group scratchpad memory (like the one proposed in #15061), then having a single work_group_scratchpad_size property that can be used as either work_group_scratchpad_size<1024>{} or work_group_scratchpad_size(N) would be nicer than splitting it into two properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd envision something like this instead: https://godbolt.org/z/b97a39zrv

Copy link
Contributor

Choose a reason for hiding this comment

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

I was trying to avoid stealing a value from the representable range of value. But yes, that works too.

You can avoid the <> by employing a deduction guide. https://godbolt.org/z/zK53Kvvnv.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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 that's clever, but it's still not clear to me that there's a real use case here. Why would we want a compile-time-constant version of work_group_scratchpad_size? Is there some meaningful optimization that we expect the compiler to do in this case?

In any case, this would be a very disruptive change in the common use case for properties. There's a lot of code out there using the inline constexpr variables. We can't ask all that code to change now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't ask all that code to change now

I don't agree. The properties are experimental and I think changing them by deprecating variables in some 2025.x minor release and removing in 2026.0 is acceptable. Also, we can consider this in scope of eventually making it a KHR extension (or next SYCL revision).

Comment on lines 115 to 122
void bar() {
// "Duck-typing" here.
foo(prop<42>);

// Now nothing prevents us from using `operator|` here that someone might
// found more natural. Not doing for simplicity for now.
foo(prop<42> + other_prop);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Another way that we could achieve this syntax without the duck-typing would be to define two overloads of every function that is supposed to take properties:

template <typename Property> auto foo(Property prop);
template <typename PropertyList> auto foo(PropList props = {});

...but there would be drawbacks to that, like the fact that foo<prop<42>> would be a different function to foo<properties<prop<42>>>. That said, I think duck-typing and @rolandschulz's suggestion that we handle this via adjusted constraints might both have this same issue.

Would this be good enough? People designing property interfaces would have to be aware of this potential issue, but we might just be able to disable the single-property overload on a case-by-case basis. I think it would only matter if somebody wanted to use a static variable inside of a function accepting a property list, and I can't immediately think of a use-case for that. And there's a straightforward workaround (as @rolandschulz pointed out offline): if you want to use this pattern, foo(property) should just call foo(properties(property)).

Providing the extra overload seems nicer to me than supporting unary +.

Copy link
Contributor

Choose a reason for hiding this comment

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

Providing the extra overload seems nicer to me than supporting unary +.

I agree. Would callers be able to pass an initializer list to specify a list of properties like:

foo({prop1, prop2});

I think that would be nicer than:

foo(properties{prop1, prop2});

Copy link
Contributor

Choose a reason for hiding this comment

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

Would callers be able to pass an initializer list to specify a list of properties like:

I might be wrong, but I think we'll always hit problems with this sort of thing because the property list type can't be deduced.

We can make foo((prop1, prop2)) work by overloading the ,(property) operator, but I don't know if that's a good idea in general. I've never seen , overloaded in the wild.

@aelovikov-intel
Copy link
Contributor Author

aelovikov-intel commented Oct 28, 2024

Latest revision drops property_key from user interfaces (i.e. has_property/get_property). The change is based on the @Pennycook 's offline suggestion.

This comes with a limitation that we won't be able to support properties that have mixed type/non-type template parameters until something like https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2989r2.pdf (universal template parameters) appears in C++, but I'm not aware of such properties (we do have type-templated properties for virtual functions though).

Note that some extensions implement has/get_property and they'd need to provide the additional version as well, e.g.

template <typename propertyT> static constexpr bool has_property() {
return property_list_t::template has_property<propertyT>();
}
template <typename propertyT> static constexpr auto get_property() {
return property_list_t::template get_property<propertyT>();
}
};

Update: https://github.com/intel/llvm/blob/sycl/sycl/doc/extensions/experimental/sycl_ext_oneapi_prefetch.asciidoc is a "mixed" non-type/type property, but it can easily be changed - the type is used as a simple tag.

@Pennycook
Copy link
Contributor

Latest revision drops property_key from user interfaces (i.e. has_property/get_property). The change is based on the @Pennycook 's offline suggestion.

Looks great to me.👍

Update: https://github.com/intel/llvm/blob/sycl/sycl/doc/extensions/experimental/sycl_ext_oneapi_prefetch.asciidoc is a "mixed" non-type/type property, but it can easily be changed - the type is used as a simple tag.

How do you recommend that we fix this case?

If I understand the issue correctly, we could either: 1) replace the cache_level enum values with a bunch of classes like struct l1_cache{}, so everything is a type; or 2) replace the nontemporal struct with something like a memory_mode enum, so everything is a value. Is there a reason to prefer one over the other from an implementation perspective, or is this purely an interface design question?

..make previous property_key_tag the key itself (adjusted to the lack of
universal template).
@aelovikov-intel
Copy link
Contributor Author

Is there a reason to prefer one over the other from an implementation perspective, or is this purely an interface design question?

I think the latter. I'd personally prefer memory_mode enum but by a very small margin.

Comment on lines +139 to +142
static_assert(foo<ty>::bar(detail::key<prop>()));
static_assert(foo<ty>::bar(detail::key<prop>(), detail::key<prop2>()));
static_assert(foo<empty_properties_t>::bar(detail::key<prop>()));
static_assert(!foo<ty>::bar(detail::key<prop2>()));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's continue the discussion from #15899 (comment) here. @gmlueck , @Pennycook

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gmlueck 's argument is that we want to be able to extend the set of allowed properties for a given API from multiple extensions. As such, it's preferred to have a trait that each property can specialize, instead of a single type list (as in @Pennycook's and my suggestions).

@aelovikov-intel
Copy link
Contributor Author

Remaining open questions (to the best of my understanding):

  1. Naming convention and variable shortcuts.
template <...> struct foo_property {};
inline constexpr auto foo = ...;

struct bar { bar(...); };

// inconsistent usage: user must remember <> vs {}/()
auto pl = properties{ foo<..>, bar{...} };

// inconsistent naming: user must remember _property
pl.has_property<foo_property> && pl.has_property<bar>;

An alternative might be to name the types without _property suffix and use inline constexpr auto foo_v = ...; for the shortcut, but then _v isn't any better than {}. Another alternative is to drop any property list APIs from the extension and make it an opaque type that implementation will somehow handle magically. My preference is to drop variable shortcuts and turn it into types when used with enums. I believe that's @Pennycook 's position as well.

  1. Duck-typing/shorter syntax to create property list from individual properties. It looks we're all in agreement that unary plus is bad and non-negotiable. I think that duck-typing might work but isn't great and I don't think properties{foo{}, bar{}, baz{}} is that bad. As such, I'd rather drop the shorter syntax (with operator+ or operator|) altogether. That said, providing has/get_property inside individual properties isn't very hard and I won't object against that direction.

  2. Substitute for is_property_of. To be discussed in inlne comments thread at [RFC][POC] Suggest re-design of compile-time properties extension #15752 (comment)

  3. How to stage the changes between preview/minor/major releases.

@Pennycook
Copy link
Contributor

My preference is to drop variable shortcuts and turn it into types when used with enums. I believe that's @Pennycook 's position as well.

I think this is my position, too. But to make it explicit, am I right in thinking that this would mean that your example above becomes the below?

template <...> struct foo {};

struct bar { bar(...); };

// consistent usage: user must always construct a property (same as existing sycl::property_list)
auto pl = properties{ foo<..>{}, bar(...) };

// consistent naming: properties only have one name, used for both declarations and queries
pl.has_property<foo>() && pl.has_property<bar>();

I'd still like us to try and come up with some shorthand syntax, even if it's not one of the approaches that we've discussed above. For comparison, consider a reduction:

// SYCL 2020
sycl::reduction(..., sycl::property::reduction::initialize_to_identity{}); // 51 characters

// Without any shorthands, top-level namespace
sycl::reduction(..., sycl::properties{sycl::initialize_to_identity{}}); // 48 characters

// Without any shorthands, new property namespace (my current expectation)
sycl::reduction(..., sycl::properties{sycl::property::initialize_to_identity{}}); // 58 characters

// With a shorthand, or even just an overload for one property
sycl::reduction(..., sycl::property::initialize_to_identity{}); // 40 characters

@aelovikov-intel
Copy link
Contributor Author

But to make it explicit, am I right in thinking that this would mean that your example above becomes the below?

Yes.

@aelovikov-intel aelovikov-intel deleted the new-properties branch November 21, 2024 16:26
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.

6 participants