syrk_gram_tiles.hh
Go to the documentation of this file.
1#pragma once
2
3// A single-tile batched SYRK/HERK for n <= kGramMaxTile (ortho's tall, skinny
4// A). The tile is sized to n, one shared tile serves both operands, and only the
5// thread tiles meeting the requested triangle are carried. The shared stride is
6// exactly n (unpadded) so fragments stay LDS.128; the swizzle below keeps that
7// conflict-free. Staging assignment depends on the transpose mode.
8// evidence: docs/perf/level3.md#syrk-gram-tiles-kernel-design
9
10#include <src/kernels/common/triangular_tiles.hh>
11
12#include <src/queue.hh>
13#include <src/util/kernel-trace.hh>
14#include <src/sycl/kernel_attrs.hh>
15
18
19#include <sycl/sycl.hpp>
20
21#include <cstddef>
22#include <cstdint>
23#include <type_traits>
24
26
27// The output tile is the whole matrix, so this is also the largest n the
28// kernel can serve.
29inline constexpr int kGramMaxTile = 128;
30
31// Drops the imaginary part BLAS guarantees is zero on a HERK diagonal. It is
32// zero only up to rounding, and leaving the residue there would make a matrix
33// that is not quite Hermitian, which the eigensolvers downstream do notice.
34template <typename T>
35inline T real_part_of(const T& value) {
36 if constexpr (is_std_complex_v<T>) {
37 return T(value.real(), typename T::value_type(0));
38 } else {
39 return value;
40 }
41}
42
43template <typename T, int NTile, int ThreadTile, int KC, bool TransOperand, bool Conjugate>
45
46template <typename T, int NTile, int ThreadTile, int KC, bool TransOperand, bool Conjugate>
50 T alpha,
51 T beta,
52 Uplo uplo) {
53 BATCHLAS_KERNEL_TRACE_SCOPE("syrk_cuda_custom.gram_tiles");
54
55 static_assert(NTile % ThreadTile == 0, "the thread grid has to tile the output exactly");
56 static_assert(ThreadTile % 4 == 0, "fragments are loaded as 4-wide bands");
57 static_assert(KC % 4 == 0, "staging moves whole packets");
58
59 constexpr int Lanes = NTile / ThreadTile; // thread tiles per side
60 constexpr int TriTiles = Lanes * (Lanes + 1) / 2; // ... that meet the triangle
61 constexpr int Threads = ((TriTiles + 31) / 32) * 32;
62 constexpr int SPad = NTile; // aligned: LDS.128 fragments
63 constexpr int Bands = ThreadTile / 4; // vectorized bands per thread
64 constexpr int Packs = NTile / 4; // 4-wide packets per row
65 constexpr int SwizzleMask = Packs - 1;
66 constexpr int Packets = KC * Packs;
67 static_assert((Packs & SwizzleMask) == 0, "the swizzle needs a power-of-two packet count");
68
69 const int n = static_cast<int>(C.rows());
70 const int k = TransOperand ? static_cast<int>(A.rows()) : static_cast<int>(A.cols());
71 const int batch = A.batch_size();
72
73 // Four scalar staging loads, not one 128-bit load, deliberately (measured slower).
74 // evidence: docs/perf/level3.md#syrk-gram-tiles-kernel-design
75
76 const sycl::range<2> local(1, static_cast<size_t>(Threads));
77 const sycl::range<2> global(static_cast<size_t>(batch), static_cast<size_t>(Threads));
78
79 impl::check_group_count(*ctx, sycl::nd_range<2>(global, local));
80 ctx->submit([&](sycl::handler& h) {
81 sycl::local_accessor<T, 1> tile(sycl::range<1>(KC * SPad), h);
82
83 const T* a_ptr = A.data_ptr();
84 T* c_ptr = C.data_ptr();
85 const int lda = A.ld();
86 const int ldc = C.ld();
87 const int stride_a = A.stride();
88 const int stride_c = C.stride();
89 const bool lower = uplo == Uplo::Lower;
90
91 batchlas::impl::parallel_for<SyrkGramTilesKernel<T, NTile, ThreadTile, KC, TransOperand, Conjugate>>(h,
92 sycl::nd_range<2>(global, local), [=](sycl::nd_item<2> item) {
93 const int bid = static_cast<int>(item.get_group(0));
94 if (bid >= batch) {
95 return;
96 }
97 const int tid = static_cast<int>(item.get_local_id(1));
98 const bool active = tid < TriTiles;
99
100 int bi = 0;
101 int bj = 0;
102 triangular_tile_decode(active ? tid : 0, bi, bj);
103 const int tr = lower ? bi : bj; // row tile
104 const int tc = lower ? bj : bi; // column tile
105
106 const T* Ab = a_ptr + static_cast<std::ptrdiff_t>(bid) * stride_a;
107 T* Cb = c_ptr + static_cast<std::ptrdiff_t>(bid) * stride_c;
108 T* sh = tile.template get_multi_ptr<sycl::access::decorated::no>().get();
109
110 // Packet q of row kk lives at q ^ (kk / 4): the aligned stride
111 // alone puts every staging lane in one bank. Whole-packet
112 // rotation keeps fragment loads 16-byte aligned (LDS.128).
113 auto swizzle = [](int packet, int kk) {
114 return packet ^ ((kk >> 2) & SwizzleMask);
115 };
116
117 T accum[ThreadTile][ThreadTile];
118BATCHLAS_UNROLL_FULL
119 for (int i = 0; i < ThreadTile; ++i) {
120BATCHLAS_UNROLL_FULL
121 for (int j = 0; j < ThreadTile; ++j) {
122 accum[i][j] = T(0);
123 }
124 }
125
126 for (int k0 = 0; k0 < k; k0 += KC) {
127 // Zero-filled to the full KC x NTile, so the inner loop has no bounds checks.
128 if constexpr (TransOperand) {
129 // Coalesced global side; the shared side pays a conflict (written once).
130 for (int flat = tid; flat < Packets; flat += Threads) {
131 const int col = flat / (KC / 4);
132 const int kk0 = (flat % (KC / 4)) * 4;
133 const bool col_ok = col < n;
134 const int phys = (swizzle(col >> 2, kk0) << 2) | (col & 3);
135BATCHLAS_UNROLL_FULL
136 for (int i = 0; i < 4; ++i) {
137 const int gk = k0 + kk0 + i;
138 sh[(kk0 + i) * SPad + phys] =
139 (col_ok && gk < k)
140 ? Ab[gk + static_cast<std::ptrdiff_t>(col) * lda]
141 : T(0);
142 }
143 }
144 } else {
145 for (int flat = tid; flat < Packets; flat += Threads) {
146 const int kk = flat / Packs;
147 const int col0 = (flat % Packs) * 4;
148 const int gk = k0 + kk;
149 const bool k_ok = gk < k;
150 const int phys = swizzle(col0 >> 2, kk) << 2;
151BATCHLAS_UNROLL_FULL
152 for (int i = 0; i < 4; ++i) {
153 const int col = col0 + i;
154 sh[kk * SPad + phys + i] =
155 (k_ok && col < n)
156 ? Ab[col + static_cast<std::ptrdiff_t>(gk) * lda]
157 : T(0);
158 }
159 }
160 }
161 item.barrier(sycl::access::fence_space::local_space);
162
163 if (active) {
164#pragma unroll 4
165 for (int p = 0; p < KC; ++p) {
166 const T* row = &sh[p * SPad];
167 T af[ThreadTile];
168 T bf[ThreadTile];
169BATCHLAS_UNROLL_FULL
170 for (int b = 0; b < Bands; ++b) {
171 // Invariant: a thread's rows/columns are
172 // contiguous. Two bands 64 apart (as in the
173 // 128x128 kernels) silently leave elements unwritten.
174 // evidence: docs/perf/level3.md#the-band-split-syrk-bug
175 const int qa = swizzle(tr * Bands + b, p);
176 const int qb = swizzle(tc * Bands + b, p);
177 const TileVec4<T> va = tile_load4(row + qa * 4);
178 const TileVec4<T> vb = tile_load4(row + qb * 4);
179BATCHLAS_UNROLL_FULL
180 for (int e = 0; e < 4; ++e) {
181 // Conjugate the operand carrying the ^H (row
182 // for ConjTrans, column for NoTrans). The
183 // wrong one is still Hermitian: test vs GEMM.
184 if constexpr (Conjugate) {
185 af[b * 4 + e] = TransOperand ? batchlas::device::detail::conj(va.v[e]) : va.v[e];
186 bf[b * 4 + e] = TransOperand ? vb.v[e] : batchlas::device::detail::conj(vb.v[e]);
187 } else {
188 af[b * 4 + e] = va.v[e];
189 bf[b * 4 + e] = vb.v[e];
190 }
191 }
192 }
193BATCHLAS_UNROLL_FULL
194 for (int i = 0; i < ThreadTile; ++i) {
195BATCHLAS_UNROLL_FULL
196 for (int j = 0; j < ThreadTile; ++j) {
197 accum[i][j] = accumulate(accum[i][j], af[i], bf[j]);
198 }
199 }
200 }
201 }
202 item.barrier(sycl::access::fence_space::local_space);
203 }
204
205 if (!active) {
206 return;
207 }
208
209 // Only the requested triangle is read or written; only
210 // diagonal thread tiles need the element mask.
211 const bool on_diagonal = bi == bj;
212BATCHLAS_UNROLL_FULL
213 for (int j = 0; j < ThreadTile; ++j) {
214 const int col = tc * ThreadTile + j;
215 if (col >= n) {
216 continue;
217 }
218BATCHLAS_UNROLL_FULL
219 for (int i = 0; i < ThreadTile; ++i) {
220 const int row = tr * ThreadTile + i;
221 if (row >= n) {
222 continue;
223 }
224 if (on_diagonal && (lower ? row < col : row > col)) {
225 continue;
226 }
227 T* p = &Cb[row + static_cast<std::ptrdiff_t>(col) * ldc];
228 T value = beta == T(0) ? alpha * accum[i][j]
229 : alpha * accum[i][j] + beta * *p;
230 // HERK's diagonal is real; drop the rounding residue.
231 if constexpr (Conjugate) {
232 if (row == col) {
233 value = real_part_of(value);
234 }
235 }
236 *p = value;
237 }
238 }
239 });
240 });
241
242 return ctx.get_event();
243}
244
245// Problem-side support, independent of scalar type and queue. Only n <=
246// kGramMaxTile; above it non-float has no native route (the triangular kernel is float-only).
247template <typename T>
250 Transpose transA,
251 bool conjugated) {
252 if (C.rows() != C.cols() || C.rows() <= 0 || C.rows() > kGramMaxTile) {
253 return false;
254 }
255 if (A.batch_size() != C.batch_size()) {
256 return false;
257 }
258 if (A.is_heterogeneous() || C.is_heterogeneous()) {
259 return false;
260 }
261 // SYRK spells A*A^T and must not be handed a ConjTrans; HERK spells A*A^H
262 // and must not be handed a plain Trans, which would be complex-symmetric.
263 if (conjugated ? (transA != Transpose::NoTrans && transA != Transpose::ConjTrans)
264 : (transA == Transpose::ConjTrans)) {
265 return false;
266 }
267 const int n = C.rows();
268 const int k = transA == Transpose::NoTrans ? A.cols() : A.rows();
269 const int expected_n = transA == Transpose::NoTrans ? A.rows() : A.cols();
270 return expected_n == n && k > 0;
271}
272
273// The tile covers n with as little slack as possible: slack is wasted arithmetic.
274template <typename T, bool Conjugate = false>
278 T alpha,
279 T beta,
280 Uplo uplo,
281 Transpose transA) {
282 const int n = static_cast<int>(C.rows());
283 const bool trans = transA != Transpose::NoTrans;
284
285 auto dispatch = [&](auto tile_tag) {
286 constexpr int NTile = decltype(tile_tag)::value;
287 // Complex at NTile 128 needs the 8-wide thread tile: the 4-wide one
288 // exceeds the work-group register file and the launch is rejected.
289 // evidence: docs/perf/level3.md#syrk-gram-tiles-kernel-design
290 constexpr int ThreadTile =
291 (NTile == 128 && is_std_complex_v<T>) ? 8 : 4;
292 constexpr int KC = 32;
293 return trans
295 ctx, A, C, alpha, beta, uplo)
297 ctx, A, C, alpha, beta, uplo);
298 };
299
300 if (n <= 32) {
301 return dispatch(std::integral_constant<int, 32>{});
302 }
303 if (n <= 64) {
304 return dispatch(std::integral_constant<int, 64>{});
305 }
306 return dispatch(std::integral_constant<int, 128>{});
307}
308
309} // namespace batchlas::backend::detail
Non-owning view of a batch of matrices: the argument type of every entry point.
Definition matrix.hh:485
T * data_ptr() const
Base pointer of item 0.
Definition matrix.hh:600
int rows() const
Rows per item (the capacity on a heterogeneous batch).
Definition matrix.hh:605
bool is_heterogeneous() const
True when per-item active extents are attached.
Definition matrix.hh:621
int cols() const
Columns per item (the capacity on a heterogeneous batch).
Definition matrix.hh:611
int ld() const
Leading dimension: element distance between columns.
Definition matrix.hh:818
int stride() const
Batch stride: element distance between items.
Definition matrix.hh:823
int batch_size() const
Number of batch items.
Definition matrix.hh:603
Enumerations, scalar traits and scalar concepts shared by every public header.
Transpose
The operator op() applied to an operand: op(A) = A, A^T or A^H.
Definition enums.hh:171
Uplo
Which triangle of a symmetric, Hermitian or triangular operand is referenced.
Definition enums.hh:287
constexpr bool is_std_complex_v
Value of is_std_complex<T>.
Definition enums.hh:53
@ NoTrans
op(A) = A.
Definition enums.hh:172
@ ConjTrans
op(A) = A^H (the same as Trans for real scalars).
Definition enums.hh:174
@ Lower
The lower triangle (i >= j).
Definition enums.hh:289
The batched containers: Matrix (owning), MatrixView (non-owning), KernelMatrixView (device-side),...
Event syrk_gram_tiles(Queue &ctx, const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &C, T alpha, T beta, Uplo uplo, Transpose transA)
T real_part_of(const T &value)
bool syrk_gram_supported(const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &C, Transpose transA, bool conjugated)
Event launch_syrk_gram_tiles(Queue &ctx, const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &C, T alpha, T beta, Uplo uplo)
constexpr T conj(const T &value)
Completion handle of enqueued work, returned by every entry point.
Where BatchLAS work runs: a device, a backend, an ordered SYCL queue and a workspace arena.
Event get_event() const
An Event that completes after everything enqueued on this Queue so far.