group-collectives.hh
Go to the documentation of this file.
1#pragma once
2
3/// @file
4/// @brief `batchlas::portable::` group broadcast, shuffles and sums for any trivially copyable or complex `T`.
5///
6/// Under DPC++ these names are the `sycl::` functions themselves (using-declarations),
7/// so a call compiles to exactly the direct `sycl::` call. Under AdaptiveCpp they are
8/// wrappers: a value not 1, 2, 4 or 8 bytes wide moves word by word and a `std::complex` sum
9/// reduces its real and imaginary parts separately. A separate namespace keeps
10/// unqualified calls on sub-group partitions (sg_partition.hh) resolving as before.
11///
12/// Installed with the rest of `include/batchlas` (the install rule copies the
13/// tree wholesale); the device BLAS headers include it. Not a stable interface.
14/// @ingroup api_internal_helpers
15
16#include <batchlas/backend_config.h>
17
18#include <sycl/sycl.hpp>
19
20#include <complex>
21#include <cstddef>
22#include <cstdint>
23#include <cstring>
24#include <type_traits>
25
27
28#if BATCHLAS_SYCL_IMPL_ACPP || defined(BATCHLAS_DOXYGEN)
29
30namespace detail {
31
32// AdaptiveCpp's SSCP collectives move a T of exactly 1, 2, 4 or 8 bytes (static_assert otherwise);
33// its reductions take arithmetic T only.
34template <typename T>
35inline constexpr bool kSplitWords = sizeof(T) > 8 || (sizeof(T) & (sizeof(T) - 1)) != 0;
36
37template <typename T>
38struct is_std_complex : std::false_type {};
39template <typename R>
40struct is_std_complex<std::complex<R>> : std::true_type {};
41
42template <typename T, typename Op>
43inline constexpr bool kSplitComplexSum =
44 is_std_complex<T>::value && (std::is_same_v<Op, sycl::plus<T>> || std::is_same_v<Op, sycl::plus<>>);
45
46template <typename T, typename F>
47inline T map_words(const T& v, F&& f) {
48 static_assert(std::is_trivially_copyable_v<T>, "group collectives need a trivially copyable T");
49 using W = std::conditional_t<sizeof(T) % 8 == 0, std::uint64_t, std::uint32_t>;
50 constexpr std::size_t kN = (sizeof(T) + sizeof(W) - 1) / sizeof(W);
51 W w[kN] = {};
52 std::memcpy(w, &v, sizeof(T));
53#pragma clang loop unroll(full)
54 for (std::size_t i = 0; i < kN; ++i) w[i] = f(w[i]);
55 T out;
56 std::memcpy(&out, w, sizeof(T));
57 return out;
58}
59
60template <typename G>
61inline constexpr bool kWorkGroup = false;
62template <int D>
63inline constexpr bool kWorkGroup<sycl::group<D>> = true;
64
65template <typename T>
66inline constexpr bool kFloating = std::is_floating_point_v<T> || std::is_same_v<T, sycl::half>;
67
68template <typename T>
69struct sycl_vec : std::false_type {};
70template <typename R, int N>
71struct sycl_vec<sycl::vec<R, N>> : std::true_type {
72 using elem = R;
73 static constexpr int size = N;
74};
75
76// sycl::plus<vec<R, N>> -> sycl::plus<R>, for a component-wise reduce.
77template <typename Op, typename R>
78struct rebind_op;
79template <template <typename> class OpT, typename V, typename R>
80struct rebind_op<OpT<V>, R> {
81 using type = OpT<R>;
82};
83
84template <typename T>
85using local_ptr = __attribute__((address_space(3))) T*;
86
87// sycl_impl::on_metal() (src/sycl/sscp_target.hh), which an installed header cannot include.
88// 4 is compiler_backend::metal; acpp 25.10 has no such enumerator.
89__attribute__((always_inline)) inline bool on_metal() {
90 bool r = false;
91 __acpp_if_target_sscp(
92 r = static_cast<int>(sycl::AdaptiveCpp_jit::reflect<
93 sycl::AdaptiveCpp_jit::reflection_query::compiler_backend>()) == 4;)
94 return r;
95}
96
97// One scratch per T per kernel, shared by every op and call site, as AdaptiveCpp's own is.
98template <typename T>
99__attribute__((always_inline)) inline local_ptr<T> wg_scratch() {
100 local_ptr<T> p = nullptr;
101 __acpp_if_target_sscp(static __attribute__((loader_uninitialized))
102 __attribute__((address_space(3))) T buf[32];
103 p = &buf[0];)
104 return p;
105}
106
107// TRAP (AdaptiveCpp 25.10): its work-group reduce of a floating value is rounded to ~16 (float)
108// or ~42 (double) mantissa bits. wg_reduce bit-casts the result to an integer and broadcasts
109// it through its FLOAT scratch, a value conversion (sscp/builtins/detail/reduction.hpp:101,
110// detail/broadcast.hpp:29). Same algorithm with a typed final broadcast; sub-group reduces are exact.
111template <typename Group, typename T, typename Op>
112inline T wg_reduce_floating(Group g, T x, Op op) {
113 // Host-compiled kernels (omp.library-only) have no wg_scratch.
114 __acpp_if_target_host(return sycl::reduce_over_group(g, x, op);)
115 // Metal emits no static local memory, and its own reduce keeps a typed scratch (no TRAP).
116 if (on_metal()) return sycl::reduce_over_group(g, x, op);
117 const sycl::sub_group sg{};
118 const T s = sycl::reduce_over_group(sg, x, op);
119 const std::uint32_t width = sg.get_max_local_range()[0];
120 const std::uint32_t nsg = (static_cast<std::uint32_t>(g.get_local_linear_range()) + width - 1) / width;
121 if (nsg == 1) return s;
122 const std::uint32_t lid = static_cast<std::uint32_t>(g.get_local_linear_id());
123 const std::uint32_t sgid = lid / width;
124 const bool leader = sg.get_local_linear_id() == 0;
125 local_ptr<T> buf = wg_scratch<T>();
126 if (leader && sgid < 32) buf[sgid] = s;
127 sycl::group_barrier(g);
128 for (std::uint32_t i = 32; i < nsg; i += 32) {
129 if (leader && sgid >= i && sgid < i + 32) buf[sgid % 32] = op(T(buf[sgid % 32]), s);
130 sycl::group_barrier(g);
131 }
132 if (lid == 0) {
133 T acc = T(buf[0]);
134 for (std::uint32_t k = 1; k < (nsg < 32 ? nsg : 32); ++k) acc = op(acc, T(buf[k]));
135 buf[0] = acc;
136 }
137 sycl::group_barrier(g);
138 const T r = T(buf[0]);
139 sycl::group_barrier(g);
140 return r;
141}
142
143} // namespace detail
144
145/// @addtogroup api_internal_helpers
146/// @{
147
148/// @brief `sycl::group_broadcast` for any trivially copyable `T`. Collective.
149template <typename Group, typename T, typename... Id>
150 requires sycl::is_group_v<Group>
151inline T group_broadcast(Group g, T x, Id... id) {
152 if constexpr (detail::kSplitWords<T>) {
153 return detail::map_words(x, [&](auto w) { return sycl::group_broadcast(g, w, id...); });
154 } else {
155 return sycl::group_broadcast(g, x, id...);
156 }
157}
158
159/// @brief `sycl::select_from_group` for any trivially copyable `T`. Collective.
160template <typename Group, typename T, typename Id>
161 requires sycl::is_group_v<Group>
162inline T select_from_group(Group g, T x, Id id) {
163 if constexpr (detail::kSplitWords<T>) {
164 return detail::map_words(x, [&](auto w) { return sycl::select_from_group(g, w, id); });
165 } else {
166 return sycl::select_from_group(g, x, id);
167 }
168}
169
170/// @brief `sycl::shift_group_left` for any trivially copyable `T`. Collective.
171template <typename Group, typename T, typename... Delta>
172 requires sycl::is_group_v<Group>
173inline T shift_group_left(Group g, T x, Delta... delta) {
174 if constexpr (detail::kSplitWords<T>) {
175 return detail::map_words(x, [&](auto w) { return sycl::shift_group_left(g, w, delta...); });
176 } else {
177 return sycl::shift_group_left(g, x, delta...);
178 }
179}
180
181/// @brief `sycl::shift_group_right` for any trivially copyable `T`. Collective.
182template <typename Group, typename T, typename... Delta>
183 requires sycl::is_group_v<Group>
184inline T shift_group_right(Group g, T x, Delta... delta) {
185 if constexpr (detail::kSplitWords<T>) {
186 return detail::map_words(x, [&](auto w) { return sycl::shift_group_right(g, w, delta...); });
187 } else {
188 return sycl::shift_group_right(g, x, delta...);
189 }
190}
191
192/// @brief `sycl::permute_group_by_xor` for any trivially copyable `T`. Collective.
193template <typename Group, typename T, typename Mask>
194 requires sycl::is_group_v<Group>
195inline T permute_group_by_xor(Group g, T x, Mask mask) {
196 if constexpr (detail::kSplitWords<T>) {
197 return detail::map_words(x, [&](auto w) { return sycl::permute_group_by_xor(g, w, mask); });
198 } else {
199 return sycl::permute_group_by_xor(g, x, mask);
200 }
201}
202
203/// @brief `sycl::reduce_over_group`, also for a `std::complex` sum; a work-group floating
204/// result is exact (see detail::wg_reduce_floating). Collective.
205template <typename Group, typename T, typename Op>
206 requires sycl::is_group_v<Group>
207inline T reduce_over_group(Group g, T x, Op op) {
208 if constexpr (detail::kSplitComplexSum<T, Op>) {
209 using R = typename T::value_type;
210 return T(portable::reduce_over_group(g, x.real(), sycl::plus<R>()),
211 portable::reduce_over_group(g, x.imag(), sycl::plus<R>()));
212 } else if constexpr (ACPP_LIBKERNEL_IS_DEVICE_PASS_SSCP && detail::kWorkGroup<Group> &&
214 return detail::wg_reduce_floating(g, x, op);
215 } else if constexpr (ACPP_LIBKERNEL_IS_DEVICE_PASS_SSCP && detail::kWorkGroup<Group> &&
217 using R = typename detail::sycl_vec<T>::elem;
218 using ElemOp = typename detail::rebind_op<Op, R>::type;
219 T out;
220 for (int i = 0; i < detail::sycl_vec<T>::size; ++i)
221 out[i] = portable::reduce_over_group(g, static_cast<R>(x[i]), ElemOp());
222 return out;
223 } else {
224 return sycl::reduce_over_group(g, x, op);
225 }
226}
227
228/// @brief `sycl::joint_reduce` with an initial value, also for a `std::complex` sum. Collective.
229/// @pre `first` and `last` are raw pointers: AdaptiveCpp has no accessor-iterator overload.
230template <typename Group, typename Ptr, typename T, typename Op>
231 requires sycl::is_group_v<Group>
232inline T joint_reduce(Group g, Ptr first, Ptr last, T init, Op op) {
233 if constexpr (detail::kSplitComplexSum<T, Op>) {
234 T part = T(0);
235 for (Ptr p = first + g.get_local_linear_id(); p < last; p += g.get_local_linear_range()) part += *p;
236 return op(init, portable::reduce_over_group(g, part, op));
237 } else if constexpr (detail::kFloating<T> && sycl::has_known_identity_v<Op, T>) {
238 // Not sycl::joint_reduce: it pads idle items with numeric_limits<T>::min() as the maximum
239 // identity, the smallest POSITIVE value (sscp/group_functions.hpp:347).
240 T part = sycl::known_identity_v<Op, T>;
241 for (Ptr p = first + g.get_local_linear_id(); p < last; p += g.get_local_linear_range())
242 part = op(part, static_cast<T>(*p));
243 return op(init, portable::reduce_over_group(g, part, op));
244 } else {
245 return sycl::joint_reduce(g, first, last, init, op);
246 }
247}
248
249/// @brief `sycl::joint_reduce` without an initial value (an empty range gives `T{}`). Collective.
250/// @pre `first` and `last` are raw pointers.
251template <typename Group, typename Ptr, typename Op>
252 requires sycl::is_group_v<Group>
253inline auto joint_reduce(Group g, Ptr first, Ptr last, Op op) {
254 using T = std::remove_cv_t<std::remove_reference_t<decltype(*first)>>;
255 if constexpr ((detail::kFloating<T> && sycl::has_known_identity_v<Op, T>) ||
257 if (first == last) return T{};
258 // sycl::joint_reduce's mapping (item i reads first[i], first[i + range], ...): stedc fills
259 // its scratch per item and calls this with no barrier, as that mapping allows.
260 if constexpr (detail::kSplitComplexSum<T, Op>) return portable::joint_reduce(g, first, last, T{}, op);
261 else return portable::joint_reduce(g, first, last, sycl::known_identity_v<Op, T>, op);
262 } else {
263 return sycl::joint_reduce(g, first, last, op);
264 }
265}
266
267/// @brief `sycl::joint_exclusive_scan` with an initial value; `result` may equal `first`. Collective.
268/// @pre `first`, `last` and `result` are raw pointers; `T` is an integral type.
269template <typename Group, typename InPtr, typename OutPtr, typename T, typename Op>
270 requires sycl::is_group_v<Group>
271inline OutPtr joint_exclusive_scan(Group g, InPtr first, InPtr last, OutPtr result, T init, Op op) {
272 // acpp 25.10's exclusive_scan_over_group is exact only for integers.
273 static_assert(std::is_integral_v<T>, "portable::joint_exclusive_scan: integral T only");
274 // AdaptiveCpp's own version writes result[i + 1] while scanning first[i], which corrupts the
275 // next chunk's input when the scan is in place. This one reads and writes index i in one chunk.
276 const std::ptrdiff_t n = last - first;
277 const std::ptrdiff_t lid = static_cast<std::ptrdiff_t>(g.get_local_linear_id());
278 const std::ptrdiff_t width = static_cast<std::ptrdiff_t>(g.get_local_linear_range());
279 T carry = init;
280 for (std::ptrdiff_t chunk = 0; chunk < n; chunk += width) {
281 const std::ptrdiff_t i = chunk + lid;
282 const T x = i < n ? static_cast<T>(first[i]) : carry;
283 const T out = sycl::exclusive_scan_over_group(g, x, carry, op);
284 if (i < n) result[i] = out;
285 carry = portable::group_broadcast(g, op(out, x), width - 1);
286 }
287 sycl::group_barrier(g);
288 return result + n;
289}
290
291/// @}
292
293#else
294
295using sycl::group_broadcast;
296using sycl::joint_exclusive_scan;
297using sycl::joint_reduce;
298using sycl::permute_group_by_xor;
299using sycl::reduce_over_group;
300using sycl::select_from_group;
301using sycl::shift_group_left;
302using sycl::shift_group_right;
303
304#endif
305
306} // namespace batchlas::portable
T reduce_over_group(Group g, T x, Op op)
sycl::reduce_over_group, also for a std::complex sum; a work-group floating result is exact (see deta...
T shift_group_right(Group g, T x, Delta... delta)
sycl::shift_group_right for any trivially copyable T. Collective.
T shift_group_left(Group g, T x, Delta... delta)
sycl::shift_group_left for any trivially copyable T. Collective.
T select_from_group(Group g, T x, Id id)
sycl::select_from_group for any trivially copyable T. Collective.
T permute_group_by_xor(Group g, T x, Mask mask)
sycl::permute_group_by_xor for any trivially copyable T. Collective.
T group_broadcast(Group g, T x, Id... id)
sycl::group_broadcast for any trivially copyable T. Collective.
OutPtr joint_exclusive_scan(Group g, InPtr first, InPtr last, OutPtr result, T init, Op op)
sycl::joint_exclusive_scan with an initial value; result may equal first.
T joint_reduce(Group g, Ptr first, Ptr last, T init, Op op)
sycl::joint_reduce with an initial value, also for a std::complex sum.
T map_words(const T &v, F &&f)
T wg_reduce_floating(Group g, T x, Op op)
__attribute__((always_inline)) inline bool on_metal()
__attribute__((address_space(3))) T * local_ptr