geqrf_tiny_device.hh
Go to the documentation of this file.
1#pragma once
2
3// Device helpers for the register-resident GEQRF ("tiny") tier. geqrf_cta_device.hh's
4// LarfgScalars is reused VERBATIM -- tau is a contract ormqr/orgqr/ormbr/sy2sb consume, and a
5// second LAPACK real-beta convention is a second thing to get wrong. evidence:
6// docs/perf/qr.md#the-two-partition-butterflies, #a-residual-test-cannot-guard-a-convention
7
9#include <src/kernels/common/tiny_device.hh>
10
11#include <src/sycl/device_scalar.hh>
12#include <src/util/resident_capacity.hh>
13#include <src/sycl/kernel_attrs.hh>
14
15#include <cstddef>
16#include <cstdint>
17
18namespace batchlas::geqrf_native {
19
20// PARTITION-wide XOR butterflies, ALL-reduces on purpose: a WORK-GROUP reduce_over_group would
21// re-open the 48 KB hole. evidence: docs/perf/qr.md#the-two-partition-butterflies
22template <int N, typename R, typename Part>
23inline R geqrf_tiny_reduce_fmax(const Part& part, R value) {
24BATCHLAS_UNROLL_FULL
25 for (uint32_t mask = 1u; mask < static_cast<uint32_t>(N); mask <<= 1) {
26 value = sycl::fmax(value, permute_group_by_xor(part, value, mask));
27 }
28 return value;
29}
30
31template <int N, typename R, typename Part>
32inline R geqrf_tiny_reduce_sum(const Part& part, R value) {
33BATCHLAS_UNROLL_FULL
34 for (uint32_t mask = 1u; mask < static_cast<uint32_t>(N); mask <<= 1) {
35 value += permute_group_by_xor(part, value, mask);
36 }
37 return value;
38}
39
40// THE ONE source of truth for the register count. Keyed on the DEVICE scalar D, never on T:
41// Cx<float> and double are 8 bytes each with very different bodies.
42// evidence: docs/perf/qr.md#the-tiny-tier-register-table
43inline constexpr int kGeqrfTinyRegMargin = 8;
44
45constexpr int geqrf_tiny_reg_slot(int n_pad) { // -1 = off the ladder; never read
46 return (n_pad == 8) ? 0 : (n_pad == 16) ? 1 : (n_pad == 32) ? 2 : -1;
47}
48
49// Primary template = NO PROBE ROW. The zero sentinels, and why `pin` is not a free knob:
50// evidence: docs/perf/qr.md#the-two-partition-butterflies
51template <typename D> struct GeqrfTinyRegs {
52 static constexpr int at[3] = {0, 0, 0};
53 static constexpr int probed_chunk[3] = {0, 0, 0};
54 static constexpr int pin[3] = {0, 0, 0};
55};
56template <> struct GeqrfTinyRegs<float> {
57 static constexpr int at[3] = {64, 91, 142}; // N = 8, 16, 32
58 static constexpr int probed_chunk[3] = {8, 16, 32};
59 static constexpr int pin[3] = {0, 0, 0};
60};
61template <> struct GeqrfTinyRegs<double> {
62 // N = 32 is PINNED: the wider chunk costs a resident block.
63 // evidence: docs/perf/qr.md#the-chunk-width-and-the-one-cell-that-is-pinned
64 static constexpr int at[3] = {84, 124, 193};
65 static constexpr int probed_chunk[3] = {8, 16, 16};
66 static constexpr int pin[3] = {0, 0, 16};
67};
68template <> struct GeqrfTinyRegs<sycl_device::Cx<float>> {
69 static constexpr int at[3] = {72, 128, 168};
70 static constexpr int probed_chunk[3] = {8, 16, 16};
71 static constexpr int pin[3] = {0, 0, 0};
72};
73template <> struct GeqrfTinyRegs<sycl_device::Cx<double>> {
74 // N = 32 is not instantiated; the slot carries the ceiling it would have to respect, so
75 // the gate cannot widen without a re-probe. evidence: docs/perf/qr.md#the-cdouble-n32-cell
76 static constexpr int at[3] = {110, 162, 226};
77 static constexpr int probed_chunk[3] = {8, 8, 0};
78 static constexpr int pin[3] = {0, 0, 0};
79};
80
81template <typename D>
82constexpr int geqrf_tiny_words() { // 32-bit words per scalar: float 1, cdouble 4
83 return static_cast<int>(sizeof(D) / sizeof(float));
84}
85
86// The register MODEL: fallback for a cell with NO probe row only -- it is wrong in both
87// directions here. evidence: docs/perf/qr.md#the-tiny-tier-register-table
88inline constexpr int kGeqrfTinyRegOverhead = 88;
89constexpr int geqrf_tiny_regs(int n_pad, int words) {
90 return n_pad * words + kGeqrfTinyRegOverhead;
91}
92
93template <typename D, int N>
94constexpr int geqrf_tiny_regs_for() {
95 constexpr int slot = geqrf_tiny_reg_slot(N);
96 static_assert(slot >= 0, "N must be on the {8, 16, 32} ladder");
97 constexpr int probed = GeqrfTinyRegs<D>::at[slot];
98 return (probed > 0) ? probed : geqrf_tiny_regs(N, geqrf_tiny_words<D>());
99}
100
101// The ceil8 below is load-bearing: ptxas allocates registers in banks of eight, so a rule
102// reading the raw count admits a block the hardware refuses. 65,536 is the SM's whole file and
103// is the right number for an OCCUPANCY count; the LAUNCH gate is resident::sm89_fits, which
104// divides one sub-partition. evidence: docs/perf/lu.md#the-register-cap-that-binds-is-per-sub-partition
105inline constexpr int kGeqrfTinyRegsPerSm = 65536;
106inline constexpr int kGeqrfTinyThreadsPerSm = 1536;
107inline constexpr int kGeqrfTinyMaxBlocksPerSm = 24;
108
109constexpr int geqrf_tiny_ceil8(int regs) { return (regs + 7) & ~7; }
110
111constexpr int geqrf_tiny_blocks_by_regs(int regs, int wg) {
112 if (regs <= 0 || wg <= 0) return 0;
113 int b = kGeqrfTinyRegsPerSm / (geqrf_tiny_ceil8(regs) * wg);
114 const int by_threads = kGeqrfTinyThreadsPerSm / wg;
115 if (b > by_threads) b = by_threads;
117 return b;
118}
119
120// The N x (C+1) tile plus a SEPARATE C-element y -- separate, not a row of the tile, is why
121// two barriers per chunk suffice. evidence: docs/perf/qr.md#the-two-partition-butterflies
122constexpr std::size_t geqrf_tiny_slm_elems(int n_pad, int c) {
123 return static_cast<std::size_t>(n_pad) * static_cast<std::size_t>(c + 1) +
124 static_cast<std::size_t>(c);
125}
126
127inline constexpr std::size_t kGeqrfTinyReferenceSlm =
128 97280; // COMPILE-TIME derivation only; runtime reads resident::device_slm_budget
129
130constexpr int geqrf_tiny_blocks_by_slm(int n_pad, int c, std::size_t elem_bytes,
131 int matrices_per_wg) {
132 const std::size_t per_wg = static_cast<std::size_t>(matrices_per_wg) *
133 geqrf_tiny_slm_elems(n_pad, c) * elem_bytes;
134 return per_wg > 0 ? static_cast<int>(kGeqrfTinyReferenceSlm / per_wg) : 0;
135}
136
137// The shared constant, not a forked geometry. No tiny kernel declares reqd_work_group_size:
138// .maxntid lets ptxas trade registers against a launch bound, which would make the probed
139// table a function of the launch shape. evidence: docs/perf/qr.md#the-launch-shape-64-work-items-not-128
140inline constexpr int kGeqrfTinyWg = tiny_native::kTinyWgSize;
141
142// Through the shared helper, not `kGeqrfTinyWg / N`, so the power-of-two guarantee and the
143// max-work-group clamp stay where they are owned; the byte arguments are nominal.
144template <int N>
146 return resident::pack_matrices_per_wg(/*bytes_per_matrix=*/1u, N,
147 /*wg_slm_budget_bytes=*/~std::size_t(0),
148 /*max_wg_size=*/kGeqrfTinyWg, kGeqrfTinyWg,
149 /*max_pack=*/kGeqrfTinyWg / N);
150}
151
152static_assert(geqrf_tiny_matrices_per_wg<8>() * 8 == kGeqrfTinyWg, "N=8: 2 sub-groups x 4");
153static_assert(geqrf_tiny_matrices_per_wg<16>() * 16 == kGeqrfTinyWg, "N=16: 2 sub-groups x 2");
154static_assert(geqrf_tiny_matrices_per_wg<32>() * 32 == kGeqrfTinyWg, "N=32: 2 sub-groups x 1");
155static_assert(kGeqrfTinyWg / tiny_native::kTinySubGroupSize >= 2,
156 "R3 wants at least two sub-groups per work-group");
157
158// C is DERIVED, not fixed: the largest of {N, N/2, N/4} at which local-memory residency is at
159// least register residency. evidence: docs/perf/qr.md#the-chunk-width-and-the-one-cell-that-is-pinned
160template <typename D, int N>
161constexpr int geqrf_tiny_chunk() {
162 constexpr int pinned = GeqrfTinyRegs<D>::pin[geqrf_tiny_reg_slot(N)];
163 if (pinned > 0) return pinned;
164 const int mats = geqrf_tiny_matrices_per_wg<N>();
165 const int wg = mats * N;
167 if (geqrf_tiny_blocks_by_slm(N, N, sizeof(D), mats) >= want) return N;
168 if (N >= 2 && geqrf_tiny_blocks_by_slm(N, N / 2, sizeof(D), mats) >= want) return N / 2;
169 return (N >= 4) ? N / 4 : N;
170}
171
172// THE ONE-STEP FIXED POINT of a circular derivation (C moves the register count, the count picks
173// C): a re-probe crossing the boundary without updating `probed_chunk` must fail to COMPILE.
174template <typename D, int N>
176 constexpr int slot = geqrf_tiny_reg_slot(N);
177 constexpr int recorded = GeqrfTinyRegs<D>::probed_chunk[slot];
178 return recorded == 0 || recorded == geqrf_tiny_chunk<D, N>();
179}
180
181// THE GATE THAT BINDS -- resident::sm89_fits, the launch-abort gate, has too much slack at a
182// 64-lane group to guard anything. evidence: docs/perf/qr.md#the-tiny-tier-register-table
183template <typename D, int N>
187 resident::kMinBlocksPerSm;
188}
189
190// The +1 is load-bearing: an ODD row stride spreads a column's N rows over distinct banks at
191// every scalar width. evidence: docs/perf/qr.md#the-chunk-width-and-the-one-cell-that-is-pinned
192constexpr int geqrf_tiny_slda(int c) { return c + 1; }
193
194} // namespace batchlas::geqrf_native
constexpr std::size_t kGeqrfTinyReferenceSlm
constexpr int kGeqrfTinyRegOverhead
constexpr int geqrf_tiny_reg_slot(int n_pad)
constexpr int kGeqrfTinyMaxBlocksPerSm
constexpr std::size_t geqrf_tiny_slm_elems(int n_pad, int c)
constexpr int geqrf_tiny_blocks_by_slm(int n_pad, int c, std::size_t elem_bytes, int matrices_per_wg)
constexpr int geqrf_tiny_slda(int c)
constexpr int geqrf_tiny_blocks_by_regs(int regs, int wg)
constexpr int kGeqrfTinyRegMargin
constexpr int geqrf_tiny_regs_for()
constexpr int geqrf_tiny_words()
R geqrf_tiny_reduce_sum(const Part &part, R value)
constexpr bool geqrf_tiny_chunk_is_fixed_point()
constexpr int geqrf_tiny_matrices_per_wg()
constexpr int geqrf_tiny_regs(int n_pad, int words)
constexpr int geqrf_tiny_chunk()
R geqrf_tiny_reduce_fmax(const Part &part, R value)
constexpr int geqrf_tiny_ceil8(int regs)
constexpr int kGeqrfTinyThreadsPerSm
constexpr bool geqrf_tiny_cell_is_resident()