geqrf_cta_device.hh
Go to the documentation of this file.
1#pragma once
2
3// All of the native batched GEQRF panel's device code: LAPACK ?GEQR2 over a `Tile`,
4// instantiated on a local_accessor (CTA tier) and a raw pointer (blocked panel leaf).
5// Deliberately NOT internal::larfg -- that preserves alpha's phase where clarfg/zlarfg
6// and the vendors return a REAL beta, and tau is a contract ormqr/orgqr/ormbr/sy2sb read.
7// evidence: docs/perf/qr.md#a-residual-test-cannot-guard-a-convention
8
9#include <src/sycl/device_scalar.hh>
10
12
13#include <sycl/sycl.hpp>
14
15#include <cstddef>
16#include <cstdint>
17
19
20using batchlas::sycl_device::Cx;
21
22template <typename D> struct RealOf { using type = D; };
23template <typename R> struct RealOf<Cx<R>> { using type = R; };
24template <typename D> using real_of = typename RealOf<D>::type;
25
26template <typename D>
27inline D dev_zero() {
28 if constexpr (batchlas::sycl_device::dev_is_complex_v<D>) {
29 return D{real_of<D>(0), real_of<D>(0)};
30 } else {
31 return D(0);
32 }
33}
34
35template <typename D>
36inline D dev_add(D a, D b) {
37 if constexpr (batchlas::sycl_device::dev_is_complex_v<D>) {
38 return D{a.re + b.re, a.im + b.im};
39 } else {
40 return a + b;
41 }
42}
43
44template <typename D>
45inline real_of<D> dev_imag(D a) {
46 if constexpr (batchlas::sycl_device::dev_is_complex_v<D>) {
47 return a.im;
48 } else {
49 static_cast<void>(a);
50 return real_of<D>(0);
51 }
52}
53
54// max(|Re|, |Im|) -- NOT |a|, whose square-and-add is the overflow this avoids.
55template <typename D>
57 if constexpr (batchlas::sycl_device::dev_is_complex_v<D>) {
58 return sycl::fmax(sycl::fabs(a.re), sycl::fabs(a.im));
59 } else {
60 return sycl::fabs(a);
61 }
62}
63
64// |a/s|^2, formed by dividing FIRST. s is guaranteed positive by the caller.
65template <typename D>
67 using R = real_of<D>;
68 if constexpr (batchlas::sycl_device::dev_is_complex_v<D>) {
69 const R x = a.re / s;
70 const R y = a.im / s;
71 return sycl::fma(x, x, y * y);
72 } else {
73 const R x = a / s;
74 return x * x;
75 }
76}
77
78// The Householder scalars, LAPACK ?LARFG exactly. `s` scales the WHOLE column
79// (alpha included); `ssq` is the TAIL's sum of |x/s|^2. identity == true means
80// tau = 0, H = I, A(j,j) unchanged and v unwritten; otherwise A(j,j) := beta
81// (REAL even for complex) and A(j+1:m, j) := x / (alpha - beta).
82template <typename D>
84 D tau;
85 D beta; // written into A(j,j)
86 D vfactor; // multiply v by this, or divide by it -- see use_mul
87 bool use_mul;
89};
90
91template <typename D>
93 using R = real_of<D>;
95 out.tau = dev_zero<D>();
96 out.beta = alpha;
97 out.vfactor = batchlas::sycl_device::dev_one<D>();
98 out.use_mul = true;
99 out.identity = true;
100
101 const R alphi = dev_imag(alpha);
102
103 // xLARFG's early out: a zero tail and a REAL alpha means H = I; a complex
104 // alpha with nonzero imaginary part still needs a reflector.
105 if (ssq == R(0) && alphi == R(0)) return out;
106 // Redundant in exact arithmetic; it makes the divisions below safe.
107 if (!(s > R(0))) return out;
108
109 const R alphr = batchlas::sycl_device::dev_real(alpha);
110 const R ar = alphr / s;
111 const R ai = alphi / s;
112
113 const R nrm = sycl::sqrt(sycl::fma(ar, ar, sycl::fma(ai, ai, ssq)));
114
115 // beta = -SIGN(nrm, alphr). Fortran's SIGN returns +|x| for a +0 second
116 // argument and alphr == 0 is reachable, so the test is `>= 0`, not `> 0`.
117 const R beta_s = (alphr >= R(0)) ? -nrm : nrm;
118
119 if constexpr (batchlas::sycl_device::dev_is_complex_v<D>) {
120 out.tau = D{(beta_s - ar) / beta_s, -ai / beta_s};
121 out.beta = D{beta_s * s, R(0)}; // REAL beta -- the LAPACK convention
122 } else {
123 out.tau = (beta_s - ar) / beta_s;
124 out.beta = beta_s * s;
125 }
126
127 const D d = batchlas::sycl_device::dev_sub(alpha, out.beta);
128
129 const D r = batchlas::sycl_device::dev_recip(d);
130 out.use_mul = batchlas::sycl_device::dev_isfinite(r) &&
131 !batchlas::sycl_device::dev_is_zero(r);
132 out.vfactor = out.use_mul ? r : d;
133 out.identity = false;
134 return out;
135}
136
137template <typename D>
139 D* p;
140 int ld;
141 D& at(int r, int c) const {
142 return p[static_cast<std::ptrdiff_t>(r) + static_cast<std::ptrdiff_t>(c) * ld];
143 }
144};
145
146// The resident leaf addresses its tile through a raw pointer too: G panels share one
147// local_accessor, so each needs its own base. Unpadded ld is deliberate -- both hot
148// patterns walk consecutive rows within a column, conflict-free at any ld.
149template <typename D> using GeqrfRawTile = GeqrfGlobalTile<D>;
150
151// Which group the phase barriers synchronise, and where the two column reductions are
152// taken. SubGroup is what makes G-packing legal -- every barrier becomes a sub-group
153// barrier and both reductions butterflies, so the G sub-groups never synchronise with
154// each other. It also removes the reduce_over_group calls, and with them the static
155// shared allocation that makes this kernel, alone of the three, trip the 48 KB hole.
156// evidence: docs/perf/qr.md#the-48-kib-launch-hole
158
159// Sub-group sum, replicated to every lane. An XOR butterfly rather than
160// sycl::reduce_over_group, whose CUDA path has had float-reduction limits.
161template <typename D>
162inline D geqrf_sg_sum(const sycl::sub_group& sg, D v) {
163 const uint32_t lanes = static_cast<uint32_t>(sg.get_local_linear_range());
164 if constexpr (batchlas::sycl_device::dev_is_complex_v<D>) {
165 using R = real_of<D>;
166 R re = v.re;
167 R im = v.im;
168 for (uint32_t off = lanes / 2; off > 0; off >>= 1) {
169 re += sycl::permute_group_by_xor(sg, re, off);
170 im += sycl::permute_group_by_xor(sg, im, off);
171 }
172 return D{re, im};
173 } else {
174 for (uint32_t off = lanes / 2; off > 0; off >>= 1) {
175 v += sycl::permute_group_by_xor(sg, v, off);
176 }
177 return v;
178 }
179}
180
181// Sub-group max, replicated to every lane; the SubGroup-scoped counterpart of
182// reduce_over_group(g, ., maximum), and free of the static shared it emits.
183template <typename R>
184inline R geqrf_sg_max(const sycl::sub_group& sg, R v) {
185 const uint32_t lanes = static_cast<uint32_t>(sg.get_local_linear_range());
186 for (uint32_t off = lanes / 2; off > 0; off >>= 1) {
187 v = sycl::fmax(v, sycl::permute_group_by_xor(sg, v, off));
188 }
189 return v;
190}
191
192// LAPACK ?GEQR2 on an m x n tile, in place. The apply's conj(tau) is zgeqr2's,
193// not a transcription slip: dropping it still passes every real-typed test.
194// `tau_ptr` is a GLOBAL pointer already offset to this matrix; reflector j lands
195// at tau_ptr[j]. kmax lets a caller factor fewer reflectors than min(m, n).
196template <typename D, GeqrfScope SC, typename Tile>
197inline void geqr2_panel_device(sycl::nd_item<1> it, Tile A, int m, int n, int kmax,
198 D* tau_ptr) {
199 using R = real_of<D>;
200
201 const auto g = it.get_group();
202 const auto sg = it.get_sub_group();
203 const int lane = static_cast<int>(sg.get_local_linear_id());
204 const int nlanes = static_cast<int>(sg.get_local_linear_range());
205
206 // Under SubGroup scope this sub-group IS the panel's whole work-group: it strides the
207 // rows with its own lanes and walks every column itself.
208 const int wg = (SC == GeqrfScope::SubGroup) ? nlanes
209 : static_cast<int>(it.get_local_range(0));
210 const int tid = (SC == GeqrfScope::SubGroup) ? lane
211 : static_cast<int>(it.get_local_linear_id());
212 const int team = (SC == GeqrfScope::SubGroup)
213 ? 0 : static_cast<int>(sg.get_group_linear_id());
214 const int nteams = (SC == GeqrfScope::SubGroup)
215 ? 1 : static_cast<int>(sg.get_group_linear_range());
216
217 const auto phase_barrier = [&]() {
218 if constexpr (SC == GeqrfScope::SubGroup) {
219 sycl::group_barrier(sg);
220 } else {
221 sycl::group_barrier(g);
222 }
223 };
224
225 for (int j = 0; j < kmax; ++j) {
226 // B1 -- the previous reflector's column writes become visible.
227 phase_barrier();
228
229 const D alpha = A.at(j, j);
230
231 R smax = dev_absmax(alpha);
232 for (int r = j + 1 + tid; r < m; r += wg) {
233 smax = sycl::fmax(smax, dev_absmax(A.at(r, j)));
234 }
235 if constexpr (SC == GeqrfScope::SubGroup) {
236 smax = geqrf_sg_max<R>(sg, smax);
237 } else {
238 smax = batchlas::portable::reduce_over_group(g, smax, sycl::maximum<R>());
239 }
240
241 // The collective sits outside the `smax > 0` test: all items reach it.
242 R ssq = R(0);
243 if (smax > R(0)) {
244 for (int r = j + 1 + tid; r < m; r += wg) {
245 ssq += dev_abs2_scaled(A.at(r, j), smax);
246 }
247 }
248 if constexpr (SC == GeqrfScope::SubGroup) {
249 ssq = geqrf_sg_sum<R>(sg, ssq);
250 } else {
251 ssq = batchlas::portable::reduce_over_group(g, ssq, sycl::plus<R>());
252 }
253
254 const LarfgScalars<D> h = geqrf_larfg_scalars<D>(alpha, smax, ssq);
255
256 // B2 -- every work-item has read A(j,j) as `alpha`; item 0 overwrites it.
257 phase_barrier();
258
259 if (!h.identity) {
260 if (tid == 0) {
261 A.at(j, j) = h.beta;
262 }
263 // No barrier needed here: work-item t writes exactly the rows it read.
264 if (h.use_mul) {
265 for (int r = j + 1 + tid; r < m; r += wg) {
266 A.at(r, j) = batchlas::sycl_device::dev_mul(A.at(r, j), h.vfactor);
267 }
268 } else {
269 for (int r = j + 1 + tid; r < m; r += wg) {
270 A.at(r, j) = batchlas::sycl_device::dev_div(A.at(r, j), h.vfactor);
271 }
272 }
273 }
274 if (tid == 0) {
275 tau_ptr[j] = h.tau;
276 }
277
278 // B3 -- beta and v are published to the teams about to read them.
279 phase_barrier();
280
281 // `h.identity` is uniform over whichever group the barriers above synchronise --
282 // it is derived from two collectives taken at that same scope -- so this
283 // `continue` cannot desync B1/B2/B3.
284 if (h.identity) continue;
285
286 const D ctau = batchlas::sycl_device::dev_conj(h.tau);
287
288 // TEAM -> column, LANE -> row. The trip count is sub-group- but NOT
289 // work-group-uniform: no work-group barrier may appear in this region.
290 for (int c = j + 1 + team; c < n; c += nteams) {
291 // w = v^H A(j:m, c); v(j) = 1 implicitly but A(j,j) holds beta.
292 D acc = (lane == 0) ? A.at(j, c) : dev_zero<D>();
293 for (int r = j + 1 + lane; r < m; r += nlanes) {
294 acc = dev_add(acc, batchlas::sycl_device::dev_mul(
295 batchlas::sycl_device::dev_conj(A.at(r, j)),
296 A.at(r, c)));
297 }
298 const D w = geqrf_sg_sum<D>(sg, acc);
299 const D f = batchlas::sycl_device::dev_mul(ctau, w);
300
301 if (lane == 0) {
302 A.at(j, c) = batchlas::sycl_device::dev_sub(A.at(j, c), f);
303 }
304 for (int r = j + 1 + lane; r < m; r += nlanes) {
305 A.at(r, c) = batchlas::sycl_device::dev_sub(
306 A.at(r, c), batchlas::sycl_device::dev_mul(f, A.at(r, j)));
307 }
308 }
309 }
310}
311
312} // namespace batchlas::geqrf_native
batchlas::portable:: group broadcast, shuffles and sums for any trivially copyable or complex T.
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...
LarfgScalars< D > geqrf_larfg_scalars(D alpha, real_of< D > s, real_of< D > ssq)
D geqrf_sg_sum(const sycl::sub_group &sg, D v)
R geqrf_sg_max(const sycl::sub_group &sg, R v)
real_of< D > dev_imag(D a)
real_of< D > dev_absmax(D a)
void geqr2_panel_device(sycl::nd_item< 1 > it, Tile A, int m, int n, int kmax, D *tau_ptr)
GeqrfGlobalTile< D > GeqrfRawTile
typename RealOf< D >::type real_of
real_of< D > dev_abs2_scaled(D a, real_of< D > s)