Skip to content

[SYCL] Add has_known_identity/known_identity #2528

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

Merged
merged 12 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions sycl/doc/extensions/Reduction/Reduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ unspecified reduction(span<T, Extent> var, const T& identity, BinaryOperation co

The exact behavior of a reduction is specific to an implementation; the only interface exposed to the user is the set of functions above, which construct an unspecified `reduction` object encapsulating the reduction variable, an optional operator identity and the reduction operator. For user-defined binary operations, an implementation should issue a compile-time warning if an identity is not specified and this is known to negatively impact performance (e.g. as a result of the implementation choosing a different reduction algorithm). For standard binary operations (e.g. `std::plus`) on arithmetic types, the implementation must determine the correct identity automatically in order to avoid performance penalties.

Whether an implementation can identify the identity value for a given combination of accumulator type `AccumulatorT` and function object type `BinaryOperation` can be determined using the `has_known_identity` trait class:
```c++
template <typename BinaryOperation, typename AccumulatorT>
struct has_known_identity {
static constexpr bool value;
};

// Available if C++17
template <typename BinaryOperation, typename AccumulatorT>
inline constexpr bool has_known_identity_v = has_known_identity<BinaryOperation, AccumulatorT>::value;
```

If `has_known_identity` returns `true` for a given combination of accumulator type and function object type, the value of the identity can be extracted using the `known_identity` trait class:
```c++
template <typename BinaryOperation, typename AccumulatorT>
struct known_identity {
static constexpr T value;
};

// Available if C++17
template <typename BinaryOperation, typename AccumulatorT>
inline constexpr T known_identity_v = known_identity<BinaryOperation, AccumulatorT>::value;
```

The dimensionality of the `accessor` passed to the `reduction` function specifies the dimensionality of the reduction variable: a 0-dimensional `accessor` represents a scalar reduction, and any other dimensionality represents an array reduction. Specifying an array reduction of size N is functionally equivalent to specifying N independent scalar reductions. The access mode of the accessor determines whether the reduction variable's original value is included in the reduction (i.e. for `access::mode::read_write` it is included, and for `access::mode::discard_write` it is not). Multiple reductions aliasing the same output results in undefined behavior.

`T` must be trivially copyable, permitting an implementation to (optionally) use atomic operations to implement the reduction. This restriction is aligned with `std::atomic<T>` and `std::atomic_ref<T>`.
Expand Down
113 changes: 78 additions & 35 deletions sycl/include/CL/sycl/ONEAPI/reduction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,62 @@ using IsKnownIdentityOp =
IsMinimumIdentityOp<T, BinaryOperation>::value ||
IsMaximumIdentityOp<T, BinaryOperation>::value>;

template <typename BinaryOperation, typename AccumulatorT>
struct has_known_identity_impl {
static constexpr bool value =
IsKnownIdentityOp<AccumulatorT, BinaryOperation>::value;
};

template <typename BinaryOperation, typename AccumulatorT, typename = void>
struct known_identity_impl {};

/// Returns zero as identity for ADD, OR, XOR operations.
template <typename BinaryOperation, typename AccumulatorT>
struct known_identity_impl<BinaryOperation, AccumulatorT,
typename std::enable_if<IsZeroIdentityOp<
AccumulatorT, BinaryOperation>::value>::type> {
static constexpr AccumulatorT value = 0;
};

/// Returns one as identify for MULTIPLY operations.
template <typename BinaryOperation, typename AccumulatorT>
struct known_identity_impl<BinaryOperation, AccumulatorT,
typename std::enable_if<IsOneIdentityOp<
AccumulatorT, BinaryOperation>::value>::type> {
static constexpr AccumulatorT value = 1;
};

/// Returns bit image consisting of all ones as identity for AND operations.
template <typename BinaryOperation, typename AccumulatorT>
struct known_identity_impl<BinaryOperation, AccumulatorT,
typename std::enable_if<IsOnesIdentityOp<
AccumulatorT, BinaryOperation>::value>::type> {
static constexpr AccumulatorT value = ~static_cast<AccumulatorT>(0);
};

/// Returns maximal possible value as identity for MIN operations.
template <typename BinaryOperation, typename AccumulatorT>
struct known_identity_impl<BinaryOperation, AccumulatorT,
typename std::enable_if<IsMinimumIdentityOp<
AccumulatorT, BinaryOperation>::value>::type> {
Comment on lines +206 to +207
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor comment, definitely not a request to change.
This could be a little bit shorter (but same number of lines though):

Suggested change
typename std::enable_if<IsMinimumIdentityOp<
AccumulatorT, BinaryOperation>::value>::type> {
enable_if_t<IsMinimumIdentityOp<
AccumulatorT, BinaryOperation>::value>> {

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 tried this during development and couldn't get it to work. I chalked it up to me not understanding enough about how our implementation of enable_if_t differs from std::enable_if. Can you compile locally if you make this change? Maybe I'm doing something wrong.

Copy link
Contributor

Choose a reason for hiding this comment

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

I checked our enable_if_t implementation. It is the same as in C++ standard. So, there should not be any problem. If you need help we may have a call or IM

static constexpr AccumulatorT value =
std::numeric_limits<AccumulatorT>::has_infinity
? std::numeric_limits<AccumulatorT>::infinity()
: (std::numeric_limits<AccumulatorT>::max)();
};

/// Returns minimal possible value as identity for MAX operations.
template <typename BinaryOperation, typename AccumulatorT>
struct known_identity_impl<BinaryOperation, AccumulatorT,
typename std::enable_if<IsMaximumIdentityOp<
AccumulatorT, BinaryOperation>::value>::type> {
static constexpr AccumulatorT value =
std::numeric_limits<AccumulatorT>::has_infinity
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like AccumulatorT is the the fundamental C++ type only (because it's used in std::numeric_limits).

Two questions basing on that:

  • What about custom types?
  • If only fundamental types should be supported, can we introduce the integral_constant semantics for known_identity API as I suggested for has_known_identity?

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 a short-term fix. Our reduction implementation currently only detects identity values automatically for fundamental C++ types (and half), so that's all we've decided to cover with the traits for now.

Eventually we want developers to be able to declare the identity value for their own types and function object types. The current thinking is that for combinations of fundamental types and known function object types (e.g. std::plus<>) the traits will continue to work as implemented here, but for anything else they'll check the function object for something like an identity member.

There are still some open questions about exactly what this should look like for transparent functors and what additional checks are needed (e.g. if we can determine an identity for T1 and T1 is convertible to T2, is it safe to assume that the identity for T1 can be used?). These are important questions, but I don't think the first implementation of the traits should be blocked by them.

? static_cast<AccumulatorT>(
-std::numeric_limits<AccumulatorT>::infinity())
: std::numeric_limits<AccumulatorT>::lowest();
};

/// Class that is used to represent objects that are passed to user's lambda
/// functions and representing users' reduction variable.
/// The generic version of the class represents those reductions of those
Expand Down Expand Up @@ -191,43 +247,10 @@ class reducer<T, BinaryOperation,
MValue = BOp(MValue, Partial);
}

/// Returns zero as identity for ADD, OR, XOR operations.
template <typename _T = T, class _BinaryOperation = BinaryOperation>
static enable_if_t<IsZeroIdentityOp<_T, _BinaryOperation>::value, _T>
getIdentity() {
return 0;
}

/// Returns one as identify for MULTIPLY operations.
template <typename _T = T, class _BinaryOperation = BinaryOperation>
static enable_if_t<IsOneIdentityOp<_T, _BinaryOperation>::value, _T>
static enable_if_t<has_known_identity_impl<_BinaryOperation, _T>::value, _T>
getIdentity() {
return 1;
}

/// Returns bit image consisting of all ones as identity for AND operations.
template <typename _T = T, class _BinaryOperation = BinaryOperation>
static enable_if_t<IsOnesIdentityOp<_T, _BinaryOperation>::value, _T>
getIdentity() {
return ~static_cast<_T>(0);
}

/// Returns maximal possible value as identity for MIN operations.
template <typename _T = T, class _BinaryOperation = BinaryOperation>
static enable_if_t<IsMinimumIdentityOp<_T, _BinaryOperation>::value, _T>
getIdentity() {
return std::numeric_limits<_T>::has_infinity
? std::numeric_limits<_T>::infinity()
: (std::numeric_limits<_T>::max)();
}

/// Returns minimal possible value as identity for MAX operations.
template <typename _T = T, class _BinaryOperation = BinaryOperation>
static enable_if_t<IsMaximumIdentityOp<_T, _BinaryOperation>::value, _T>
getIdentity() {
return std::numeric_limits<_T>::has_infinity
? static_cast<_T>(-std::numeric_limits<_T>::infinity())
: std::numeric_limits<_T>::lowest();
return known_identity_impl<_BinaryOperation, _T>::value;
}

template <typename _T = T>
Expand Down Expand Up @@ -1076,6 +1099,26 @@ reduction(T *VarPtr, BinaryOperation) {
access::mode::read_write>(VarPtr);
}

template <typename BinaryOperation, typename AccumulatorT>
struct has_known_identity : detail::has_known_identity_impl<
typename std::decay<BinaryOperation>::type,
typename std::decay<AccumulatorT>::type> {};
#if __cplusplus >= 201703L
template <typename BinaryOperation, typename AccumulatorT>
inline constexpr bool has_known_identity_v =
has_known_identity<BinaryOperation, AccumulatorT>::value;
#endif

template <typename BinaryOperation, typename AccumulatorT>
struct known_identity
: detail::known_identity_impl<typename std::decay<BinaryOperation>::type,
typename std::decay<AccumulatorT>::type> {};
#if __cplusplus >= 201703L
template <typename BinaryOperation, typename AccumulatorT>
inline constexpr AccumulatorT known_identity_v =
known_identity<BinaryOperation, AccumulatorT>::value;
#endif

} // namespace ONEAPI
} // namespace sycl
} // __SYCL_INLINE_NAMESPACE(cl)