steqr_cta_device.hh
Go to the documentation of this file.
1#pragma once
2
3// Device-side building blocks of the CTA (sub-group-partition) STEQR solver, shared by
4// steqr_cta.cc and syev_cta_fused.cc. Keep ONE definition: it is what makes a
5// fused-vs-partitioned benchmark measure fusion and nothing else.
6// evidence: docs/perf/steqr.md#cta-steqr-chase-micro-structure-decisions
7
11#include <src/kernels/common/sg_partition/sg_partition.hh>
12#include <src/math-helpers.hh>
13#include <src/sycl/kernel_attrs.hh>
14
15#include <array>
16#include <cstdint>
17#include <type_traits>
18
19namespace batchlas {
20
21 // Givens rotation for the real bulge chase: in range, algebraically internal::lartg()
22 // but with rsqrt + one Newton step instead of sqrt and two divisions. Out-of-range
23 // inputs fall back to the fully scaled reference implementation.
24 // evidence: docs/perf/steqr.md#cta-steqr-chase-micro-structure-decisions
25 template <typename T>
26 struct cta_rotation {
27 T c;
28 T s;
29 T r;
30 };
31
32 template <typename T>
33 inline cta_rotation<T> cta_lartg(const T f, const T g) {
34 if constexpr (std::is_same_v<T, float>) {
35 // Range guard: |f|, |g| in (sqrt(safmin), sqrt(safmax/2)) keeps f^2 + g^2 finite
36 // and normal; anything else (NaN included) takes the scaled reference.
37 // g == 0 must return exactly (1, 0, f): forced by the final select, not an
38 // early return, so an identity rotation does not diverge from its neighbours.
39 const bool gz = (g == T(0));
40 const T f_abs = sycl::fabs(f);
41 const T g_abs = sycl::fabs(g);
42
43 const T rtmin = sycl::sqrt(internal::safmin<T>());
44 const T rtmax = sycl::sqrt(internal::safmax<T>() / T(2));
45
46 T c = T(1), s = T(0), r = f;
47 if (f_abs > rtmin && f_abs < rtmax && (gz || (g_abs > rtmin && g_abs < rtmax))) {
48 const T t = f * f + g * g;
49 T inv = sycl::rsqrt(t);
50 // One fused Newton step brings the hardware estimate below 0.5 ulp.
51 inv = sycl::fma(inv * T(0.5), sycl::fma(-(t * inv), inv, T(1)), inv);
52 const T d = sycl::sqrt(t);
53 const T signed_inv = sycl::copysign(inv, f);
54 c = f_abs * inv;
55 s = g * signed_inv;
56 r = sycl::copysign(d, f);
57 } else if (!gz) {
58 const auto res = internal::lartg(f, g);
59 c = res.c;
60 s = res.s;
61 r = res.r;
62 }
63 return {gz ? T(1) : c, gz ? T(0) : s, gz ? f : r};
64 } else {
65 const auto res = internal::lartg(f, g);
66 return {res.c, res.s, res.r};
67 }
68 }
69
70 // Compile-time selectable shared-memory cache for Q, column-major:
71 // Q_local[base_q + row + col*LDQ] = Q(row, col). LDQ == P suits row-indexed use;
72 // a consumer that reads the tile by column (fused SYEV) needs LDQ == P+1 (banks).
73 template <typename T, size_t P, size_t LDQ, bool ComputeVecs, typename LocalAcc>
75
76 template <typename T, size_t P, size_t LDQ, typename LocalAcc>
77 struct QSharedCache<T, P, LDQ, true, LocalAcc> {
78 LocalAcc Q_local;
79 int32_t base_q;
80 int32_t lane;
81 int32_t n;
82 int32_t idx{};
83 T carry{};
84
85 QSharedCache(LocalAcc q, int32_t bq, int32_t ln, int32_t n_)
86 : Q_local(q), base_q(bq), lane(ln), n(n_) {}
87
88 template <typename QProb>
89 inline void load(const QProb& Q_prob) {
90 const int32_t pN = static_cast<int32_t>(LDQ);
91 // Zero, not skip, the padding rows (lane >= n): the chase then runs unguarded
92 // on all P lanes, and `store` reads back only the first n rows.
93 if (lane < n) {
94 for (int32_t c = 0; c < n; ++c) {
95 Q_local[base_q + lane + c * pN] = Q_prob(lane, c);
96 }
97 } else {
98 for (int32_t c = 0; c < n; ++c) {
99 Q_local[base_q + lane + c * pN] = T(0);
100 }
101 }
102 }
103
104 template <typename QProb>
105 inline void store(QProb& Q_prob) const {
106 if (lane >= n) return;
107 const int32_t pN = static_cast<int32_t>(LDQ);
108 for (int32_t c = 0; c < n; ++c) {
109 Q_prob(lane, c) = Q_local[base_q + lane + c * pN];
110 }
111 }
112
113 inline void apply(int32_t col0, int32_t col1, T c, T s) {
114 const int32_t pN = static_cast<int32_t>(LDQ);
115 const int32_t i0 = base_q + lane + col0 * pN;
116 const int32_t i1 = base_q + lane + col1 * pN;
117 const T q0 = Q_local[i0];
118 const T q1 = Q_local[i1];
119 Q_local[i0] = c * q0 - s * q1;
120 Q_local[i1] = s * q0 + c * q1;
121 }
122
123 // Streaming form of `apply` for a bulge chase: successive rotations share a
124 // column, so it stays in a register, and the element index moves by +/-P, so it
125 // advances by a compile-time constant (partner via the immediate offset).
126 // evidence: docs/perf/steqr.md#cta-steqr-chase-micro-structure-decisions
127 inline void chase_begin(int32_t col) {
128 idx = base_q + lane + col * static_cast<int32_t>(LDQ);
129 carry = Q_local[idx];
130 }
131
132 // Dir is +1 when the chase walks toward higher column indices and -1 when it
133 // walks toward lower ones; the written column is the current one and the read
134 // column is the neighbour in that direction.
135 template <int32_t Dir>
136 inline void chase_step(T c, T s) {
137 const int32_t next = idx + Dir * static_cast<int32_t>(LDQ);
138 const T q1 = Q_local[next];
139 Q_local[idx] = c * carry - s * q1;
140 carry = s * carry + c * q1;
141 idx = next;
142 }
143
144 // Padded form for a chunk that idles through a lockstep chase: an idle
145 // step reads its own current word and writes nothing, so the index never
146 // leaves the lane's own row, even in a tile shared with other chunks.
147 template <int32_t Dir>
148 inline void chase_step_masked(T c, T s, bool act) {
149 const int32_t next = act ? idx + Dir * static_cast<int32_t>(LDQ) : idx;
150 const T q1 = Q_local[next];
151 if (act) Q_local[idx] = c * carry - s * q1;
152 carry = act ? s * carry + c * q1 : carry;
153 idx = next;
154 }
155
156 inline void chase_end(int32_t) {
157 Q_local[idx] = carry;
158 }
159
160 inline void apply_if(bool pred, int32_t col0, int32_t col1, T c, T s) {
161 if (pred) apply(col0, col1, c, s);
162 }
163
164 // Q := Q*J on columns [bb, be]; lane-private (own row), so no barrier.
165 inline void reverse_columns(int32_t bb, int32_t be, bool rev) {
166 if (!rev) return;
167 const int32_t pN = static_cast<int32_t>(LDQ);
168 for (int32_t a = bb, b = be; a < b; ++a, --b) {
169 const int32_t ia = base_q + lane + a * pN;
170 const int32_t ib = base_q + lane + b * pN;
171 const T qa = Q_local[ia];
172 Q_local[ia] = Q_local[ib];
173 Q_local[ib] = qa;
174 }
175 }
176 };
177
178 template <typename T, size_t P, size_t LDQ, typename LocalAcc>
179 struct QSharedCache<T, P, LDQ, false, LocalAcc> {
180 QSharedCache(LocalAcc, int32_t, int32_t, int32_t) {}
181
182 template <typename QProb>
183 inline void load(const QProb&) {}
184
185 template <typename QProb>
186 inline void store(QProb&) const {}
187
188 inline void apply(int32_t, int32_t, T, T) {}
189 inline void chase_begin(int32_t) {}
190 template <int32_t Dir>
191 inline void chase_step(T, T) {}
192 template <int32_t Dir>
193 inline void chase_step_masked(T, T, bool) {}
194 inline void chase_end(int32_t) {}
195 inline void apply_if(bool, int32_t, int32_t, T, T) {}
196 inline void reverse_columns(int32_t, int32_t, bool) {}
197 };
198
199 template <typename T, typename Partition>
200 inline void deflate(Partition partition,
201 T& e,
202 T& d,
203 int32_t n,
204 int32_t start_ix,
205 int32_t end_ix) {
206 // Deflation follows LAPACK's relative test; there is no absolute threshold.
207 // evidence: docs/algorithms/steqr.md#steqr-the-relative-deflation-criterion
208 const int32_t lane = static_cast<int32_t>(partition.get_local_linear_id());
209 const bool lane_in_active_range = (lane + 1 < n) && (lane >= start_ix) && (lane + 1 < end_ix);
210
211 // d_{i+1} by a 1-lane shift. The last lane's value is unspecified (next chunk's
212 // lane 0 on a full-sub-group partition) and unused: lane_in_active_range.
213 const T d_ip1 = shift_group_left(partition, d, 1);
214
215 if (lane_in_active_range) {
216 if (e != T(0)) {
217 // LAPACK-style relative deflation test:
218 // |e|^2 <= eps2 * |d_i| * |d_{i+1}| + safmin
219 const T rhs = internal::eps2<T>() * sycl::fabs(d) * sycl::fabs(d_ip1) + internal::safmin<T>();
220 if (sycl::fabs(e) * sycl::fabs(e) <= rhs) {
221 e = T(0);
222 }
223 }
224 }
225 }
226
227 // T := J*T*J and Q := Q*J on [bb, be] when `rev` (J the reversal): a QR sweep is a QL
228 // sweep on J*T*J. d mirrors about bb+be, e about bb+be-1; e(bb-1) and e(be) stay.
229 // evidence: docs/algorithms/steqr.md#steqr-choosing-qr-versus-ql
230 template <size_t P, typename T, typename Partition, typename QCache>
231 inline void reverse_block(const Partition& partition,
232 T& diag,
233 T& offdiag,
234 QCache& qcache,
235 int32_t bb,
236 int32_t be,
237 bool rev) {
238 const int32_t lane = static_cast<int32_t>(partition.get_local_linear_id());
239 const int32_t src_d = (rev && lane >= bb && lane <= be) ? (bb + be - lane) : lane;
240 const int32_t src_e = (rev && lane >= bb && lane < be) ? (bb + be - 1 - lane) : lane;
241 diag = select_from_group(partition, diag, static_cast<uint32_t>(src_d));
242 offdiag = select_from_group(partition, offdiag, static_cast<uint32_t>(src_e));
243 qcache.reverse_columns(bb, be, rev);
244 }
245
246 // Butterfly (XOR-shuffle) all-reduce within the partition: log2(P) shuffles, every
247 // lane active, no local memory or barriers (boundary searches run once per sweep).
248 template <size_t P, typename Partition>
249 inline int32_t partition_reduce_min(const Partition& partition, int32_t value) {
250BATCHLAS_UNROLL_FULL
251 for (uint32_t mask = 1u; mask < static_cast<uint32_t>(P); mask <<= 1) {
252 const int32_t other = permute_group_by_xor(partition, value, mask);
253 value = (other < value) ? other : value;
254 }
255 return value;
256 }
257
258 template <size_t P, typename T, typename Partition>
259 inline T partition_reduce_fmax(const Partition& partition, T value) {
260BATCHLAS_UNROLL_FULL
261 for (uint32_t mask = 1u; mask < static_cast<uint32_t>(P); mask <<= 1) {
262 value = sycl::fmax(value, permute_group_by_xor(partition, value, mask));
263 }
264 return value;
265 }
266
267 template <typename T, size_t P, typename Partition, typename QCache>
268 inline void solve_2x2_and_update(Partition partition,
269 T& diag,
270 T& offdiag,
271 int32_t l0,
272 QCache& qcache,
273 bool pred = true) {
274 // `pred` false: shuffles still run (l0 must be in [0, P-2]), nothing is written.
275 const T a = select_from_group(partition, diag, l0);
276 const T b = select_from_group(partition, offdiag, l0);
277 const T c2 = select_from_group(partition, diag, l0 + 1);
278
279 // Every input is already partition-uniform, so evaluate laev2 redundantly on
280 // all lanes instead of computing on the leader and broadcasting four values.
281 const auto [rt1, rt2, cs, sn] = internal::laev2(a, b, c2);
282 const int32_t lane = static_cast<int32_t>(partition.get_local_linear_id());
283 if (pred && lane == l0) {
284 diag = rt1;
285 offdiag = T(0);
286 }
287 if (pred && lane == (l0 + 1)) {
288 diag = rt2;
289 }
290
291 // QL eigenvector update: apply (cs, sn) on columns (l0+1, l0).
292 qcache.apply_if(pred, l0 + 1, l0, cs, sn);
293 }
294
295 // Pad: the chunks of the lockstep domain share one trip count, and a chunk
296 // with `active` false or a shorter chase idles through identity rotations.
297 // Idle state is never multiplied by a mask, only discarded by selects, so a
298 // failed chunk's NaNs cannot reach d, e or Q. EXP only.
299 template <typename T, size_t P, bool Pad = false, typename Partition, typename QCache>
300 inline void implicit_ql_step(const Partition& partition,
301 T& diag,
302 T& offdiag,
303 QCache& qcache,
304 int32_t n,
305 int32_t l,
306 int32_t m,
307 SteqrShiftStrategy shift_strategy,
308 SteqrUpdateScheme update_scheme,
309 bool active = true) {
310 const int32_t lane = static_cast<int32_t>(partition.get_local_linear_id());
311 // An idle chunk's l and m are stale: clamp every shuffle source into the chunk.
312 const int32_t ls = Pad ? sycl::clamp(l, int32_t(0), static_cast<int32_t>(P) - 2) : l;
313 const int32_t ms = Pad ? sycl::clamp(m, int32_t(0), static_cast<int32_t>(P) - 1) : m;
314
315 // EXP scheme: QL as a QR-style bulge chase in *virtual reversed* indexing inside
316 // [l..m]. The only chase: QR sweeps run it on a mirrored block (reverse_block).
317 const auto explicit_ql_step_exp = [&]() {
318 // Preload shift inputs.
319 T p0 = select_from_group(partition, diag, ls);
320 T e0 = select_from_group(partition, offdiag, ls);
321 T dlp1 = select_from_group(partition, diag, ls + 1);
322 if constexpr (Pad) {
323 // A benign 2x2 keeps an idle chunk's shift finite (e0 == 0 divides by 0).
324 p0 = active ? p0 : T(0);
325 e0 = active ? e0 : T(1);
326 dlp1 = active ? dlp1 : T(0);
327 }
328
329 // Partition-uniform scalar state for the virtual QR bulge chase.
330 // Every lane evaluates it redundantly: the inputs are broadcast values, so
331 // the results agree bit-for-bit and no result broadcast is needed.
332 T mu = T(0);
333
334 if (shift_strategy == SteqrShiftStrategy::Wilkinson) {
335 // For QL, the shift is formed from the leading 2x2 of the physical block (l,l+1).
336 // wilkinson_shift picks the eigenvalue closest to its 3rd argument; we want closest to D(l).
337 mu = internal::wilkinson_shift(dlp1, e0, p0);
338 } else {
339 const T gg = (dlp1 - p0) / (T(2) * e0);
340 const T rr = sycl::hypot(gg, T(1));
341 mu = p0 - e0 / (gg + sycl::copysign(rr, gg));
342 }
343
344 const int32_t nrot = Pad ? lockstep_max(partition, active ? m - l : 0) : m - l;
345 // Virtual index v in [0..m-l-1] maps to physical indices:
346 // d_v(v) = d( m - v )
347 // d_v(v+1) = d( m - v - 1 )
348 // e_v(v) = e( m - v - 1 ) (couples the two diags above)
349 // e_v(v+1) = e( m - v - 2 )
350 // Iteration v+1's (di, ei) is the (dj_new, ej_new) this one produced, so it
351 // is carried in registers. Shuffles run UNCONDITIONALLY with a clamped source:
352 // a non-uniform condition costs a MATCH/VOTE/BRA.DIV wrapper and lockstep.
353 T di = select_from_group(partition, diag, ms);
354 T ei = select_from_group(partition, offdiag, std::max(ms - 1, 0));
355 ei = (ms >= 1) ? ei : T(0);
356 T e_own = T(0);
357
358 // Snapshot the tridiagonal before the chase. The chase writes lane `hi` but only
359 // reads lanes below it, so broadcasts see pre-chase values; immutable
360 // snapshots let the compiler hoist the shuffles off the lartg dependency path.
361 // evidence: docs/perf/steqr.md#cta-steqr-chase-micro-structure-decisions
362 const T diag_snap = diag;
363 const T offdiag_snap = offdiag;
364
365 // Seed the running (eprev, bulge) pair with (d(m) - mu, e(m-1)) so the first
366 // rotation needs no special case in the hottest loop.
367 T eprev = di - mu;
368 T bulge = ei;
369
370 // e(hi) is written only for v > 0 (the bulge has to have moved past it) and
371 // only when hi indexes a real offdiagonal. For v > 0 <=> hi < m, so both
372 // conditions collapse into a single compare against this limit.
373 const int32_t e_hi_limit = std::min(m, n - 1);
374
375 qcache.chase_begin(ms);
376
377 for (int32_t v = 0; v < nrot; ++v) {
378 const int32_t hi = m - v;
379 const int32_t lo = m - v - 1;
380 const bool act = !Pad || (active && v < m - l);
381
382 const T dj = select_from_group(partition, diag_snap, Pad ? std::max(lo, 0) : lo);
383
384 // Next virtual offdiag (toward physical l). It is safe to read outside the block because
385 // deflation boundaries force those couplings to zero.
386 const T ej_raw = select_from_group(partition, offdiag_snap, std::max(lo - 1, 0));
387 const T ej = (lo >= 1) ? ej_raw : T(0);
388
389 const auto upd = [&]() {
390 // Idle: (1, 0) is the exact identity rotation c = 1, s = 0.
391 const T x = act ? eprev : T(1);
392 const T y = act ? bulge : T(0);
393
394 const auto [c1, s1, r1] = cta_lartg(x, y);
395 const T sigma = -s1; // match steqr.cc / saved-rotation convention
396
397 // Update the offdiagonal to the *higher* physical index (virtual e(v-1) -> physical e(hi)).
398 // This corresponds to LAPACK's QL inner-loop assignment E(i+1)=r.
399 const T e_hi_new = x * c1 - y * sigma; // only meaningful when !first
400
401 // Explicit similarity update (steqr.cc apply_givens_rotation), virtual order.
402 const T di_new = c1 * (c1 * di - ei * sigma) - sigma * (ei * c1 - sigma * dj);
403 const T dj_new = c1 * (c1 * dj + ei * sigma) + sigma * (ei * c1 + sigma * di);
404 const T ei_new = c1 * (c1 * ei + sigma * di) - sigma * (c1 * dj + sigma * ei);
405
406 const T ej_new = c1 * ej;
407 const T bulge_new = -ej * sigma;
408
409 // Advance the (uniform) chase state.
410 eprev = act ? ei_new : eprev;
411 bulge = act ? bulge_new : bulge;
412
413 // Return {c, sigma, di_new, dj_new, ei_new, ej_new, e_hi_new}
414 return std::array<T, 7>{c1, sigma, di_new, dj_new, ei_new, ej_new, e_hi_new};
415 }();
416
417 const T c1 = upd[0];
418 const T sigma = upd[1];
419
420 // d(hi), e(hi) are final once the bulge passes; d(lo), e(lo), e(lo-1) are
421 // recomputed next rotation, so they are carried and written once after
422 // the chase. Selects, not `if`s: one target lane means an `if` diverges
423 // on every rotation.
424 const bool owns_hi = act && (lane == hi);
425 diag = owns_hi ? upd[2] : diag;
426 offdiag = (owns_hi && hi < e_hi_limit) ? upd[6] : offdiag;
427
428 // QL eigenvector update: columns are reversed in physical ordering.
429 // In your existing PG path you do apply(i+1, i, c, -s); here the physical pair is (hi, lo).
430 if constexpr (Pad) {
431 qcache.template chase_step_masked<-1>(c1, sigma, act);
432 } else {
433 qcache.template chase_step<-1>(c1, sigma);
434 }
435
436 // Carry the values the next iteration would otherwise re-shuffle:
437 // d(lo) and e(lo-1) are exactly what was just written.
438 di = act ? upd[3] : di;
439 ei = act ? upd[5] : ei;
440 e_own = act ? upd[4] : e_own;
441 }
442
443 // Flush the carried tail values (lo == l on the final iteration).
444 if (active && lane == l) {
445 diag = di;
446 offdiag = e_own;
447 }
448 if (active && l >= 1 && lane == (l - 1)) {
449 offdiag = ei;
450 }
451
452 if (active) qcache.chase_end(l);
453 };
454
455 if (Pad || update_scheme == SteqrUpdateScheme::EXP) {
456 explicit_ql_step_exp();
457 return;
458 }
459
460 // Broadcast values needed for the shift (all lanes participate).
461 const T p0 = select_from_group(partition, diag, l);
462 const T e0 = select_from_group(partition, offdiag, l);
463 const T dlp1 = select_from_group(partition, diag, l + 1);
464 const T dm = select_from_group(partition, diag, m);
465
466 // Partition-uniform scalar state (evaluated redundantly on every lane).
467 T g = T(0);
468 T c = T(1);
469 T s = T(1);
470 T p = T(0);
471
472 {
473 T mu = T(0);
474 if (shift_strategy == SteqrShiftStrategy::Wilkinson) {
475 // Want eigenvalue closest to D(l); wilkinson_shift picks closest to its third arg.
476 mu = internal::wilkinson_shift(dlp1, e0, p0);
477 } else {
478 // LAPACK-style stable implicit shift.
479 const T gg = (dlp1 - p0) / (T(2) * e0);
480 const T rr = sycl::hypot(gg, T(1));
481 mu = p0 - e0 / (gg + sycl::copysign(rr, gg));
482 }
483 g = dm - mu;
484 }
485
486 qcache.chase_begin(m);
487
488 for (int32_t i = m; i-- > l;) {
489 // Broadcast the tridiagonal entries needed for this step.
490 const T ei = select_from_group(partition, offdiag, i);
491 const T di = select_from_group(partition, diag, i);
492 const T dip1 = select_from_group(partition, diag, i + 1);
493
494 // Whether E(i+1) should be updated is a pure function of (i, m, n).
495 const bool do_e_upd = (i != (m - 1)) && ((i + 1) < (n - 1));
496
497 const auto [c1b, s1b, d_ip1_new_b, r1_out_b] = [&]() {
498 // {c1, s1, d_ip1_new, r1_out}
499 const T f = s * ei;
500 T rout = T(0);
501
502 const auto [c1, s1, r1] = cta_lartg(g, f);
503
504 // In the original local-memory version: E(i+1) = r1 for i != m-1, when i+1 < N-1.
505 if (do_e_upd) {
506 rout = r1;
507 }
508
509 const T g2 = dip1 - p;
510 const T r2 = (di - g2) * s1 + T(2) * c1 * (c * ei);
511 p = s1 * r2;
512
513 const T d_ip1_new = g2 + p;
514 g = c1 * r2 - (c * ei);
515 c = c1;
516 s = s1;
517
518 return std::array{c1, s1, d_ip1_new, rout};
519 }();
520
521 // Apply D/E updates directly to registers (predicated, not branched).
522 const bool owns_ip1 = (lane == (i + 1));
523 diag = owns_ip1 ? d_ip1_new_b : diag;
524 offdiag = (owns_ip1 && do_e_upd) ? r1_out_b : offdiag;
525
526 // QL uses reversed-column convention; keep the same sign as before: apply(i+1,i,c,-s).
527 qcache.template chase_step<-1>(c1b, -s1b);
528 }
529
530 qcache.chase_end(l);
531
532 // Final updates: D(l) = D(l) - p, and E(l) = g.
533 const T d_l_new_b = p0 - p;
534 const T e_l_new_b = g;
535 if (lane == l) {
536 diag = d_l_new_b;
537 if (l < (n - 1)) {
538 offdiag = e_l_new_b;
539 }
540 }
541 }
542
543 // One problem's STEQR outer loop: split at zero offdiagonals, then shifted QL sweeps
544 // per block (QR as QL on the mirrored block). Lane i owns d(i), e(i); `qcache`
545 // accumulates rotations (no-op without vectors). True if any block failed.
546 template <typename T, size_t P, typename Partition, typename QCache>
547 inline bool steqr_cta_solve_nested(const Partition& partition,
548 T& diag,
549 T& offdiag,
550 QCache& qcache,
551 int32_t n,
552 int32_t max_sweeps,
553 SteqrShiftStrategy cta_shift_strategy,
554 SteqrUpdateScheme cta_update_scheme) {
555 const int32_t lane = static_cast<int32_t>(partition.get_local_linear_id());
556
557 // Defensive convergence budget to avoid unbounded looping on hard inputs.
558 // Each implicit step consumes one unit. Budget scales with problem size.
559 int32_t sweep_budget = max_sweeps * n;
560 bool failed = false;
561
562 // ---- Outer split loop over blocks separated by E==0 ----
563 for (int32_t next_block_begin = 0; next_block_begin < n;) {
564 const int32_t block_begin = next_block_begin;
565
566 // Mark the split explicitly as LAPACK does: E(block_begin-1)=0.
567 if (block_begin > 0 && lane == (block_begin - 1) && lane < (n - 1)) {
568 offdiag = T(0);
569 }
570
571 // Deflation pass over the remaining tail to create more zeros in E.
572 deflate(partition, offdiag, diag, n, block_begin, n);
573
574 // Find end of current block: first i>=block_begin where E(i)==0; if none, block ends at n-1.
575 const int32_t block_end_candidate =
576 (lane >= block_begin && lane < (n - 1) && offdiag == T(0)) ? lane : (n - 1);
577 const int32_t block_end = partition_reduce_min<P>(partition, block_end_candidate);
578
579 // Next block starts after block_end.
580 next_block_begin = block_end + 1;
581
582 // Size-0/1 block.
583 if (block_end <= block_begin) {
584 continue;
585 }
586
587 // Numerical scaling (LAPACK-style): bring the active block norm into
588 // a safe range to avoid overflow/underflow on tough inputs.
589 T anorm_cand = T(0);
590 if (lane >= block_begin && lane <= block_end) {
591 anorm_cand = sycl::fabs(diag);
592 }
593 if (lane >= block_begin && lane < block_end) {
594 anorm_cand = sycl::fmax(anorm_cand, sycl::fabs(offdiag));
595 }
596 const T anorm = partition_reduce_fmax<P>(partition, anorm_cand);
597
598 T scale = T(1);
599 if (anorm > internal::ssfmax<T>()) {
600 scale = internal::ssfmax<T>() / anorm;
601 } else if (anorm < internal::ssfmin<T>() && anorm != T(0)) {
602 scale = internal::ssfmin<T>() / anorm;
603 }
604 const T inv_scale = T(1) / scale;
605
606 if (scale != T(1)) {
607 if (lane >= block_begin && lane <= block_end) {
608 diag *= scale;
609 }
610 if (lane >= block_begin && lane < block_end) {
611 offdiag *= scale;
612 }
613 }
614
615 // QL if |D(l)| <= |D(lend)|, QR otherwise (as dsteqr: the graded small end
616 // converges first; the inverted rule loses relative accuracy). QR runs as QL
617 // on the mirrored block so chunks share one loop nest; mirrored back even on
618 // failure. evidence: docs/perf/steqr.md#cta-steqr-chase-micro-structure-decisions
619 const T d_first = sycl::fabs(select_from_group(partition, diag, block_begin));
620 const T d_last = sycl::fabs(select_from_group(partition, diag, block_end));
621 const bool rev = d_last < d_first;
622 reverse_block<P>(partition, diag, offdiag, qcache, block_begin, block_end, rev);
623
624 // QL iteration: converge from the top (l grows).
625 for (int32_t l = block_begin; l <= block_end && !failed;) {
626 if (l == block_end) {
627 l += 1;
628 continue;
629 }
630
631 bool advanced = false;
632 for (int32_t sweep = 0; sweep < max_sweeps; ++sweep) {
633 // Deflate within current active subproblem [l..lend].
634 deflate(partition, offdiag, diag, n, l, block_end + 1);
635
636 // Find first m in [l..lend-1] such that E(m)==0; if none, m=lend.
637 const int32_t m_candidate = (lane >= l && lane < block_end && offdiag == T(0)) ? lane : block_end;
638 const int32_t m = partition_reduce_min<P>(partition, m_candidate);
639
640 if (m == l) {
641 l += 1;
642 advanced = true;
643 break;
644 }
645
646 if (m == l + 1) {
647 solve_2x2_and_update<T, P>(partition, diag, offdiag, l, qcache);
648 l += 2;
649 advanced = true;
650 break;
651 }
652
653 if (sweep_budget <= 0) {
654 failed = true;
655 break;
656 }
657 sweep_budget -= 1;
658
659 implicit_ql_step<T, P>(partition, diag, offdiag, qcache, n, l, m, cta_shift_strategy, cta_update_scheme);
660 }
661
662 if (!advanced) {
663 failed = true;
664 }
665 }
666
667 reverse_block<P>(partition, diag, offdiag, qcache, block_begin, block_end, rev);
668
669 // Rescale converged block back to the original magnitude.
670 if (scale != T(1)) {
671 if (lane >= block_begin && lane <= block_end) {
672 diag *= inv_scale;
673 }
674 if (lane >= block_begin && lane < block_end) {
675 offdiag *= inv_scale;
676 }
677 }
678
679 if (failed) break;
680 }
681
682 return failed;
683 }
684
685 // steqr_cta_solve_nested with the sweep hoisted out of the loop nest, for a partition
686 // whose collectives are chunk-local (a masked SubGroupPartition): each chunk settles
687 // until it has a sweep or is done, then every chunk with a sweep chases in the same
688 // pass. Same per-chunk operation sequence as the nested solver: bitwise equal.
689 // evidence: docs/perf/steqr.md#lockstep-flat-solver
690 template <typename T, size_t P, typename Partition, typename QCache>
691 inline bool steqr_cta_solve_flat(const Partition& partition,
692 T& diag,
693 T& offdiag,
694 QCache& qcache,
695 int32_t n,
696 int32_t max_sweeps,
697 SteqrShiftStrategy cta_shift_strategy,
698 SteqrUpdateScheme cta_update_scheme) {
699 const int32_t lane = static_cast<int32_t>(partition.get_local_linear_id());
700 int32_t sweep_budget = max_sweeps * n;
701 int32_t next_block_begin = 0;
702 int32_t block_begin = 0;
703 int32_t block_end = 0;
704 int32_t l = 0;
705 int32_t m = 0;
706 int32_t sweeps_at_l = 0;
707 bool rev = false;
708 T scale = T(1);
709 T inv_scale = T(1);
710 bool need_block = true;
711 bool done = false;
712 bool failed = false;
713
714 while (!done) {
715 // Settle: every exit of this loop is either "sweep [l, m] next" or done,
716 // so the chunks of a warp reconverge right before the chase.
717 for (;;) {
718 if (need_block) {
719 // ---- Open the next block (nested: top of the split loop). ----
720 const int32_t bb = next_block_begin;
721 if (bb > 0 && lane == (bb - 1) && lane < (n - 1)) {
722 offdiag = T(0);
723 }
724 deflate(partition, offdiag, diag, n, bb, n);
725 const int32_t be = partition_reduce_min<P>(
726 partition, (lane >= bb && lane < (n - 1) && offdiag == T(0)) ? lane : (n - 1));
727 next_block_begin = be + 1;
728 if (be <= bb) {
729 if (next_block_begin >= n) {
730 done = true;
731 break;
732 }
733 continue;
734 }
735
736 T anorm_cand = T(0);
737 if (lane >= bb && lane <= be) {
738 anorm_cand = sycl::fabs(diag);
739 }
740 if (lane >= bb && lane < be) {
741 anorm_cand = sycl::fmax(anorm_cand, sycl::fabs(offdiag));
742 }
743 const T anorm = partition_reduce_fmax<P>(partition, anorm_cand);
744 scale = T(1);
745 if (anorm > internal::ssfmax<T>()) {
746 scale = internal::ssfmax<T>() / anorm;
747 } else if (anorm < internal::ssfmin<T>() && anorm != T(0)) {
748 scale = internal::ssfmin<T>() / anorm;
749 }
750 inv_scale = T(1) / scale;
751 if (scale != T(1)) {
752 if (lane >= bb && lane <= be) {
753 diag *= scale;
754 }
755 if (lane >= bb && lane < be) {
756 offdiag *= scale;
757 }
758 }
759
760 const T d_first = sycl::fabs(select_from_group(partition, diag, bb));
761 const T d_last = sycl::fabs(select_from_group(partition, diag, be));
762 rev = d_last < d_first;
763 reverse_block<P>(partition, diag, offdiag, qcache, bb, be, rev);
764 block_begin = bb;
765 block_end = be;
766 l = bb;
767 sweeps_at_l = 0;
768 need_block = false;
769 }
770
771 // ---- Test [l, block_end] (nested: sweep loop top). ----
772 // The sweep cap comes first: the nested loop gives up after its last
773 // sweep without deflating or searching for m again.
774 bool closing = false;
775 if (sweeps_at_l >= max_sweeps) {
776 failed = true;
777 closing = true;
778 } else {
779 deflate(partition, offdiag, diag, n, l, block_end + 1);
781 partition, (lane >= l && lane < block_end && offdiag == T(0)) ? lane : block_end);
782 if (m == l) {
783 l += 1;
784 } else if (m == l + 1) {
785 solve_2x2_and_update<T, P>(partition, diag, offdiag, l, qcache);
786 l += 2;
787 } else if (sweep_budget <= 0) {
788 failed = true;
789 } else {
790 break;
791 }
792 sweeps_at_l = 0;
793 closing = failed || l >= block_end;
794 }
795
796 // ---- Close the block: mirror back and rescale, even on failure. ----
797 if (closing) {
798 reverse_block<P>(partition, diag, offdiag, qcache, block_begin, block_end, rev);
799 if (scale != T(1)) {
800 if (lane >= block_begin && lane <= block_end) {
801 diag *= inv_scale;
802 }
803 if (lane >= block_begin && lane < block_end) {
804 offdiag *= inv_scale;
805 }
806 }
807 need_block = true;
808 if (failed || next_block_begin >= n) {
809 done = true;
810 break;
811 }
812 }
813 }
814 if (done) break;
815
816 implicit_ql_step<T, P>(partition, diag, offdiag, qcache, n, l, m,
817 cta_shift_strategy, cta_update_scheme);
818 sweep_budget -= 1;
819 sweeps_at_l += 1;
820 }
821 return failed;
822 }
823
824 // The same state machine when the lockstep domain is the whole sub-group: every branch
825 // guarding a collective runs under a sub-group vote with per-chunk gated updates, and
826 // a chunk outside a phase feeds in-range sources and discards the results.
827 template <typename T, size_t P, typename Partition, typename QCache>
828 inline bool steqr_cta_solve_lockstep(const Partition& partition,
829 T& diag,
830 T& offdiag,
831 QCache& qcache,
832 int32_t n,
833 int32_t max_sweeps,
834 SteqrShiftStrategy cta_shift_strategy,
835 SteqrUpdateScheme cta_update_scheme) {
836 const int32_t lane = static_cast<int32_t>(partition.get_local_linear_id());
837
838 int32_t sweep_budget = max_sweeps * n;
839 int32_t next_block_begin = 0;
840 int32_t block_begin = 0;
841 int32_t block_end = 0;
842 int32_t l = 0;
843 int32_t m = 0;
844 int32_t sweeps_at_l = 0;
845 bool rev = false;
846 T scale = T(1);
847 T inv_scale = T(1);
848 bool need_block = true;
849 bool ready = false; // a sweep on [l, m] is pending
850 bool done = false;
851 bool failed = false;
852
853 while (lockstep_any(partition, !done)) {
854 while (lockstep_any(partition, !done && !ready)) {
855 // ---- Open the next block (nested: top of the split loop). ----
856 const bool opening = !done && !ready && need_block;
857 if (lockstep_any(partition, opening)) {
858 const int32_t bb = opening ? next_block_begin : 0;
859 if (opening && bb > 0 && lane == (bb - 1) && lane < (n - 1)) {
860 offdiag = T(0);
861 }
862 deflate(partition, offdiag, diag, n, opening ? bb : n, n);
863 const int32_t be = partition_reduce_min<P>(
864 partition, (opening && lane >= bb && lane < (n - 1) && offdiag == T(0)) ? lane : (n - 1));
865 const bool opened = opening && be > bb;
866
867 T anorm_cand = T(0);
868 if (opened && lane >= bb && lane <= be) {
869 anorm_cand = sycl::fabs(diag);
870 }
871 if (opened && lane >= bb && lane < be) {
872 anorm_cand = sycl::fmax(anorm_cand, sycl::fabs(offdiag));
873 }
874 const T anorm = partition_reduce_fmax<P>(partition, anorm_cand);
875 T sc = T(1);
876 if (anorm > internal::ssfmax<T>()) {
877 sc = internal::ssfmax<T>() / anorm;
878 } else if (anorm < internal::ssfmin<T>() && anorm != T(0)) {
879 sc = internal::ssfmin<T>() / anorm;
880 }
881 if (opened && sc != T(1)) {
882 if (lane >= bb && lane <= be) {
883 diag *= sc;
884 }
885 if (lane >= bb && lane < be) {
886 offdiag *= sc;
887 }
888 }
889
890 const T d_first = sycl::fabs(select_from_group(partition, diag, bb));
891 const T d_last = sycl::fabs(select_from_group(partition, diag, be));
892 const bool rv = opened && (d_last < d_first);
893 reverse_block<P>(partition, diag, offdiag, qcache, bb, be, rv);
894
895 // A size-0/1 block is skipped: stay in `need_block` for the next one.
896 next_block_begin = opening ? be + 1 : next_block_begin;
897 if (opened) {
898 block_begin = bb;
899 block_end = be;
900 l = bb;
901 sweeps_at_l = 0;
902 rev = rv;
903 scale = sc;
904 inv_scale = T(1) / sc;
905 need_block = false;
906 }
907 done = done || (opening && !opened && be + 1 >= n);
908 }
909
910 // ---- Test the active subproblem [l, block_end] (nested: sweep loop top). ----
911 const bool testing = !done && !ready && !need_block;
912 // The sweep cap comes first: the nested loop gives up after its last
913 // sweep without deflating or searching for m again.
914 const bool cap = testing && sweeps_at_l >= max_sweeps;
915 const bool active = testing && !cap;
916 if (lockstep_any(partition, active)) {
917 deflate(partition, offdiag, diag, n, active ? l : n, block_end + 1);
918 const int32_t mm = partition_reduce_min<P>(
919 partition, (active && lane >= l && lane < block_end && offdiag == T(0)) ? lane : block_end);
920 const bool adv1 = active && mm == l;
921 const bool adv2 = active && mm == l + 1;
922 const bool want = active && !adv1 && !adv2;
923 failed = failed || (want && sweep_budget <= 0);
924 m = (want && sweep_budget > 0) ? mm : m;
925 ready = ready || (want && sweep_budget > 0);
926
927 if (lockstep_any(partition, adv2)) {
928 const int32_t l0 = sycl::clamp(l, int32_t(0), static_cast<int32_t>(P) - 2);
929 solve_2x2_and_update<T, P>(partition, diag, offdiag, l0, qcache, adv2);
930 }
931 l += adv1 ? 1 : (adv2 ? 2 : 0);
932 sweeps_at_l = (adv1 || adv2) ? 0 : sweeps_at_l;
933 }
934 failed = failed || cap;
935
936 // ---- Close the block: mirror back and rescale, even on failure. ----
937 const bool closing = testing && (l >= block_end || failed);
938 if (lockstep_any(partition, closing)) {
939 reverse_block<P>(partition, diag, offdiag, qcache, block_begin, block_end, closing && rev);
940 if (closing && scale != T(1)) {
941 if (lane >= block_begin && lane <= block_end) {
942 diag *= inv_scale;
943 }
944 if (lane >= block_begin && lane < block_end) {
945 offdiag *= inv_scale;
946 }
947 }
948 need_block = need_block || closing;
949 done = done || (closing && (failed || next_block_begin >= n));
950 }
951 }
952
953 // ---- Every chunk with a pending sweep chases in the same pass. ----
954 if (lockstep_any(partition, ready)) {
955 implicit_ql_step<T, P, true>(partition, diag, offdiag, qcache, n, l, m,
956 cta_shift_strategy, cta_update_scheme, ready);
957 }
958 sweep_budget -= ready ? 1 : 0;
959 sweeps_at_l += ready ? 1 : 0;
960 ready = false;
961 }
962
963 return failed;
964 }
965
966 // Where the hoisted sweep pays for its bookkeeping. It does not at P == 32
967 // (one chunk per warp) or P == 4, whose 1-3 rotation sweeps are too short for
968 // realignment to beat the extra per-pass work; float P == 8 breaks even.
969 // evidence: docs/perf/steqr.md#lockstep-flat-solver
970 template <typename T, size_t P>
971 inline constexpr bool kSteqrCtaFlatPays = (P == 16) || (P == 8 && std::is_same_v<T, double>);
972
973 // Returns true if any block failed to converge within budget.
974 //
975 // A full-sub-group (emulated) partition promises every lane of the sub-group runs
976 // this. P == 32 runs the nested loops maskless; smaller chunks on NVPTX take the
977 // chunk-masked partition (every maskless form measured slower there); elsewhere EXP
978 // takes the lockstep solver, while PG's nested loops are legal only while the chunks
979 // happen not to diverge. Judge changes here by real-kernel A/B, not microbenchmarks.
980 // evidence: docs/perf/steqr.md#full-warp-partition
981 template <typename T, size_t P, typename Partition, typename QCache>
982 inline bool steqr_cta_solve(const Partition& partition,
983 T& diag,
984 T& offdiag,
985 QCache& qcache,
986 int32_t n,
987 int32_t max_sweeps,
988 SteqrShiftStrategy cta_shift_strategy,
989 SteqrUpdateScheme cta_update_scheme) {
990 if constexpr (lockstep_spans_subgroup_v<Partition> && P < 32) {
991 if constexpr (batchlas::sgp::kMaskedByDefault) {
992 const auto chunk = make_partition<P, true>(partition.sg);
993 return steqr_cta_solve<T, P>(chunk, diag, offdiag, qcache, n, max_sweeps,
994 cta_shift_strategy, cta_update_scheme);
995 } else if (cta_update_scheme == SteqrUpdateScheme::EXP) {
996 return steqr_cta_solve_lockstep<T, P>(partition, diag, offdiag, qcache, n, max_sweeps,
997 cta_shift_strategy, cta_update_scheme);
998 }
999 } else if constexpr (kSteqrCtaFlatPays<T, P>) {
1000 return steqr_cta_solve_flat<T, P>(partition, diag, offdiag, qcache, n, max_sweeps,
1001 cta_shift_strategy, cta_update_scheme);
1002 }
1003 return steqr_cta_solve_nested<T, P>(partition, diag, offdiag, qcache, n, max_sweeps,
1004 cta_shift_strategy, cta_update_scheme);
1005 }
1006
1007} // namespace batchlas
Leader-only invocation and leader broadcast for SYCL groups and sub-group partitions.
Event inv(Queue &ctx, const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &Ainv, Span< std::byte > workspace)
Computes the explicit inverse of each dense square matrix in the batch, , by LU factorisation (getrf(...
Event scale(Queue &ctx, const T &alpha, const MatrixView< T, MType > &mat_view)
In-place A := alpha * A on every element of every item (dense).
SteqrShiftStrategy
Shift used by the CTA STEQR's implicit QR/QL steps.
SteqrUpdateScheme
Update scheme of the CTA STEQR's implicit QR/QL steps.
@ Wilkinson
Wilkinson shift computed from the trailing 2x2 block.
@ EXP
Explicit similarity update mirroring steqr.cc's bulge-chasing math.
The batched containers: Matrix (owning), MatrixView (non-owning), KernelMatrixView (device-side),...
int32_t partition_reduce_min(const Partition &partition, int32_t value)
bool steqr_cta_solve_lockstep(const Partition &partition, T &diag, T &offdiag, QCache &qcache, int32_t n, int32_t max_sweeps, SteqrShiftStrategy cta_shift_strategy, SteqrUpdateScheme cta_update_scheme)
void solve_2x2_and_update(Partition partition, T &diag, T &offdiag, int32_t l0, QCache &qcache, bool pred=true)
bool steqr_cta_solve_flat(const Partition &partition, T &diag, T &offdiag, QCache &qcache, int32_t n, int32_t max_sweeps, SteqrShiftStrategy cta_shift_strategy, SteqrUpdateScheme cta_update_scheme)
bool steqr_cta_solve(const Partition &partition, T &diag, T &offdiag, QCache &qcache, int32_t n, int32_t max_sweeps, SteqrShiftStrategy cta_shift_strategy, SteqrUpdateScheme cta_update_scheme)
void reverse_block(const Partition &partition, T &diag, T &offdiag, QCache &qcache, int32_t bb, int32_t be, bool rev)
cta_rotation< T > cta_lartg(const T f, const T g)
bool steqr_cta_solve_nested(const Partition &partition, T &diag, T &offdiag, QCache &qcache, int32_t n, int32_t max_sweeps, SteqrShiftStrategy cta_shift_strategy, SteqrUpdateScheme cta_update_scheme)
void implicit_ql_step(const Partition &partition, T &diag, T &offdiag, QCache &qcache, int32_t n, int32_t l, int32_t m, SteqrShiftStrategy shift_strategy, SteqrUpdateScheme update_scheme, bool active=true)
T partition_reduce_fmax(const Partition &partition, T value)
void deflate(Partition partition, T &e, T &d, int32_t n, int32_t start_ix, int32_t end_ix)
constexpr bool kSteqrCtaFlatPays
QSharedCache(LocalAcc, int32_t, int32_t, int32_t)
QSharedCache(LocalAcc q, int32_t bq, int32_t ln, int32_t n_)
void apply(int32_t col0, int32_t col1, T c, T s)
void reverse_columns(int32_t bb, int32_t be, bool rev)
void apply_if(bool pred, int32_t col0, int32_t col1, T c, T s)