register_wide_transposed.hh
Go to the documentation of this file.
1#pragma once
2
3// A register-tiled GEMM for TRANSPOSED and CONJUGATE-TRANSPOSED operands with
4// a wide scalar (double, complex<float>, complex<double>).
5//
6// register_tiled_common.hh goes through Annex-G std::complex operator*, and
7// register_64x64_k16_wide.hh is NN only. The macro tile is a PARAMETER matched
8// to a panel width (16 or 32), not square.
9// evidence: docs/perf/gemm.md#wide-scalar-transposed-tiles
10
11#include <src/linalg-impl.hh>
12#include <src/sycl/device_scalar.hh>
15#include <src/sycl/kernel_attrs.hh>
16
17#include <sycl/sycl.hpp>
18
19#include <cstddef>
20#include <type_traits>
21
22namespace batchlas::sycl_gemm {
23
24// One row of the wide-scalar transposed dispatch table, in the shape RegTile
25// has for the float family. Structural, so it can be an NTTP.
26struct WideTile {
27 int M; // macro tile rows
28 int N; // macro tile cols
29 int K = 16; // k step
30 int TTM = 4; // thread tile rows
31 int TTN = 4; // thread tile cols
34};
35
36template <typename T, int M, int N, int K, int TTM, int TTN, int OA, int OB>
38
39namespace wide_transposed {
40
41using batchlas::sycl_device::Cx;
42using batchlas::sycl_device::DevMap;
43using batchlas::sycl_device::dev_conj;
44using batchlas::sycl_device::dev_is_zero;
45using batchlas::sycl_device::fma_acc;
47using wide_scalar::Vec16;
49
50// A fragment slice of W consecutive elements out of the shared tile. The
51// 16-byte form is taken only when the slice IS the whole granule: at W < VecN
52// the element offset `lane * W` need not be a multiple of VecN, so a Vec16 read
53// there would be MISALIGNED, not merely wasteful. The 64x64 kernel's thread
54// tile is 4 in both directions, so W == VecN always and it hard-wires it.
55template <int W, int VecN, typename D>
56inline void load_fragment(D* dst, const D* src) {
57 if constexpr (W == VecN) {
58 const Vec16<D> packet = vec_ref(src);
59BATCHLAS_UNROLL_FULL
60 for (int w = 0; w < W; ++w) {
61 dst[w] = packet.v[w];
62 }
63 } else {
64BATCHLAS_UNROLL_FULL
65 for (int w = 0; w < W; ++w) {
66 dst[w] = src[w];
67 }
68 }
69}
70
71} // namespace wide_transposed
72
73// Does an instantiation's hard-wired transpose form serve the requested one?
74// A <ConjTrans, NoTrans> launcher handed a Trans operand silently drops the
75// conjugation and returns a plausible wrong matrix, so call sites gate on the
76// exact form -- with ONE licensed widening: for a real scalar conj is the
77// identity, so ConjTrans and Trans instantiate each other. That widening is
78// what lets one variant serve potrf_blocked.cc's kTrailingTransB<T>.
79template <typename T>
80inline bool wide_trans_matches(Transpose requested, Transpose instantiated) {
81 if (requested == instantiated) {
82 return true;
83 }
84 if constexpr (is_std_complex_v<T>) {
85 return false;
86 } else {
87 const bool both_transposing = requested != Transpose::NoTrans &&
88 instantiated != Transpose::NoTrans;
89 return both_transposing;
90 }
91}
92
93template <typename T, WideTile P>
98 T alpha,
99 T beta,
100 const char* trace) {
101 BATCHLAS_KERNEL_TRACE_SCOPE(trace);
102
103 using namespace wide_transposed;
104
105 using D = typename DevMap<T>::type;
106 static_assert(sizeof(D) == sizeof(T), "device scalar must be layout-compatible");
107
108 constexpr int TileM = P.M;
109 constexpr int TileN = P.N;
110 constexpr int TileK = P.K;
111 constexpr int TTM = P.TTM;
112 constexpr int TTN = P.TTN;
113 constexpr bool TransA = P.OpA != Transpose::NoTrans;
114 constexpr bool TransB = P.OpB != Transpose::NoTrans;
115 constexpr bool ConjA = P.OpA == Transpose::ConjTrans;
116 constexpr bool ConjB = P.OpB == Transpose::ConjTrans;
117
118 constexpr int LocalRows = TileM / TTM;
119 constexpr int LocalCols = TileN / TTN;
120 constexpr int Threads = LocalRows * LocalCols;
121
122 // No padding, for the reason the 64x64 kernel gives: an odd shared stride
123 // defeats the compiler's alignment proof and fragment loads go scalar.
124 constexpr int AStride = TileM;
125 constexpr int BStride = TileN;
126
127 constexpr int VecN = Vec16<D>::N;
128 constexpr int WbM = (VecN < TTM) ? VecN : TTM;
129 constexpr int WbN = (VecN < TTN) ? VecN : TTN;
130 constexpr int NBM = TTM / WbM;
131 constexpr int NBN = TTN / WbN;
132 constexpr int MSep = TileM / NBM;
133 constexpr int NSep = TileN / NBN;
134
135 static_assert(TileM % TTM == 0 && TileN % TTN == 0,
136 "macro tile must split into thread tiles");
137 static_assert(TTM % WbM == 0 && TTN % WbN == 0,
138 "thread tile must split into whole bands");
139 static_assert(Threads >= 32 && Threads <= 1024, "work-group size out of range");
140 static_assert((TileK * AStride) % VecN == 0 && (TileK * BStride) % VecN == 0,
141 "shared tile must be a whole number of 16-byte granules");
142 static_assert(WbM != VecN || (AStride % VecN == 0 && MSep % VecN == 0),
143 "vectorised A fragment needs an aligned band base");
144 static_assert(WbN != VecN || (BStride % VecN == 0 && NSep % VecN == 0),
145 "vectorised B fragment needs an aligned band base");
146
147 // Logical extents: A is stored k x m when OpA transposes it and B is stored
148 // n x k when OpB does, but every bound below is in LOGICAL indices, so the
149 // layouts differ only in the address formed.
150 const int m = static_cast<int>(TransA ? A.cols() : A.rows());
151 const int k = static_cast<int>(TransA ? A.rows() : A.cols());
152 const int n = static_cast<int>(TransB ? B.rows() : B.cols());
153
154 const int gm_tiles = (m + TileM - 1) / TileM;
155 const int gn_tiles = (n + TileN - 1) / TileN;
156
157 const sycl::range<3> local(1, LocalCols, LocalRows);
158 const sycl::range<3> global(static_cast<size_t>(A.batch_size()),
159 static_cast<size_t>(gn_tiles) * LocalCols,
160 static_cast<size_t>(gm_tiles) * LocalRows);
161
162 ctx->submit([&](sycl::handler& h) {
163 sycl::local_accessor<Vec16<D>, 1> tile_a(
164 sycl::range<1>(static_cast<size_t>(TileK) * AStride / VecN), h);
165 sycl::local_accessor<Vec16<D>, 1> tile_b(
166 sycl::range<1>(static_cast<size_t>(TileK) * BStride / VecN), h);
167
168 const D* a_ptr = reinterpret_cast<const D*>(A.data_ptr());
169 const D* b_ptr = reinterpret_cast<const D*>(B.data_ptr());
170 D* c_ptr = reinterpret_cast<D*>(C.data_ptr());
171 const int lda = static_cast<int>(A.ld());
172 const int ldb = static_cast<int>(B.ld());
173 const int ldc = static_cast<int>(C.ld());
174 const int stride_a = static_cast<int>(A.stride());
175 const int stride_b = static_cast<int>(B.stride());
176 const int stride_c = static_cast<int>(C.stride());
177 const int batch = static_cast<int>(A.batch_size());
178
179 D alpha_d;
180 D beta_d;
181 __builtin_memcpy(&alpha_d, &alpha, sizeof(D));
182 __builtin_memcpy(&beta_d, &beta, sizeof(D));
183
184 batchlas::impl::parallel_for<GemmWideTransposedKernel<T, TileM, TileN, TileK, TTM, TTN,
185 static_cast<int>(P.OpA),
186 static_cast<int>(P.OpB)>>(h,
187 sycl::nd_range<3>(global, local), [=](sycl::nd_item<3> item) {
188 const int bid = static_cast<int>(item.get_group(0));
189 if (bid >= batch) {
190 return;
191 }
192
193 // Dimension 2 is fastest-varying and carries m: lanes one
194 // apart touch consecutive addresses in column-major C.
195 const int ty = static_cast<int>(item.get_local_id(2));
196 const int tx = static_cast<int>(item.get_local_id(1));
197 const int tid = tx * LocalRows + ty;
198
199 const int m0 = static_cast<int>(item.get_group(2)) * TileM;
200 const int n0 = static_cast<int>(item.get_group(1)) * TileN;
201
202 const D* Ab = a_ptr + static_cast<std::ptrdiff_t>(bid) * stride_a;
203 const D* Bb = b_ptr + static_cast<std::ptrdiff_t>(bid) * stride_b;
204 D* Cb = c_ptr + static_cast<std::ptrdiff_t>(bid) * stride_c;
205
206 D* sa = reinterpret_cast<D*>(
207 tile_a.template get_multi_ptr<sycl::access::decorated::no>().get());
208 D* sb = reinterpret_cast<D*>(
209 tile_b.template get_multi_ptr<sycl::access::decorated::no>().get());
210
211 D accum[TTM][TTN];
212BATCHLAS_UNROLL_FULL
213 for (int i = 0; i < TTM; ++i) {
214BATCHLAS_UNROLL_FULL
215 for (int j = 0; j < TTN; ++j) {
216 accum[i][j] = D{};
217 }
218 }
219
220 for (int k0 = 0; k0 < k; k0 += TileK) {
221 // Staging always produces the SAME shared layout,
222 // sa[kk][i] and sb[kk][j], whatever the operand layout was,
223 // which keeps the inner loop and epilogue identical across
224 // all four forms. The index decomposition is per layout so
225 // consecutive `tid` reads consecutive global addresses: a
226 // transposed operand pays on the shared STORE, never on the
227 // global load.
228 for (int idx = tid; idx < TileM * TileK; idx += Threads) {
229 int i;
230 int kk;
231 if constexpr (TransA) {
232 kk = idx % TileK;
233 i = idx / TileK;
234 } else {
235 i = idx % TileM;
236 kk = idx / TileM;
237 }
238 const int gm = m0 + i;
239 const int gk = k0 + kk;
240 D value = D{};
241 if (gm < m && gk < k) {
242 value = TransA
243 ? Ab[gk + static_cast<std::ptrdiff_t>(gm) * lda]
244 : Ab[gm + static_cast<std::ptrdiff_t>(gk) * lda];
245 if constexpr (ConjA) {
246 value = dev_conj(value);
247 }
248 }
249 sa[kk * AStride + i] = value;
250 }
251
252 for (int idx = tid; idx < TileK * TileN; idx += Threads) {
253 int j;
254 int kk;
255 if constexpr (TransB) {
256 j = idx % TileN;
257 kk = idx / TileN;
258 } else {
259 kk = idx % TileK;
260 j = idx / TileK;
261 }
262 const int gn = n0 + j;
263 const int gk = k0 + kk;
264 D value = D{};
265 if (gn < n && gk < k) {
266 value = TransB
267 ? Bb[gn + static_cast<std::ptrdiff_t>(gk) * ldb]
268 : Bb[gk + static_cast<std::ptrdiff_t>(gn) * ldb];
269 if constexpr (ConjB) {
270 value = dev_conj(value);
271 }
272 }
273 sb[kk * BStride + j] = value;
274 }
275 item.barrier(sycl::access::fence_space::local_space);
276
277 // Out-of-range staging wrote a zero rather than being
278 // skipped, so the accumulation needs no bounds test.
279BATCHLAS_UNROLL_FULL
280 for (int kk = 0; kk < TileK; ++kk) {
281 D af[TTM];
282 D bf[TTN];
283BATCHLAS_UNROLL_FULL
284 for (int band = 0; band < NBM; ++band) {
285 load_fragment<WbM, VecN>(
286 &af[band * WbM], &sa[kk * AStride + band * MSep + ty * WbM]);
287 }
288BATCHLAS_UNROLL_FULL
289 for (int band = 0; band < NBN; ++band) {
290 load_fragment<WbN, VecN>(
291 &bf[band * WbN], &sb[kk * BStride + band * NSep + tx * WbN]);
292 }
293BATCHLAS_UNROLL_FULL
294 for (int i = 0; i < TTM; ++i) {
295BATCHLAS_UNROLL_FULL
296 for (int j = 0; j < TTN; ++j) {
297 fma_acc(accum[i][j], af[i], bf[j]);
298 }
299 }
300 }
301 item.barrier(sycl::access::fence_space::local_space);
302 }
303
304 // Predicated scalar epilogue only: these tiles MATCH a
305 // panel width, so the n edge is partial by construction at the
306 // shapes the drivers issue and the 64x64 NN kernel's banded
307 // 16-byte store would never fire. O(TileM * TileN) per group
308 // against O(TileM * TileN * k) multiplies.
309 const bool beta_zero = dev_is_zero(beta_d);
310BATCHLAS_UNROLL_FULL
311 for (int bm = 0; bm < NBM; ++bm) {
312BATCHLAS_UNROLL_FULL
313 for (int w = 0; w < WbM; ++w) {
314 const int row = m0 + bm * MSep + ty * WbM + w;
315 if (row >= m) {
316 continue;
317 }
318BATCHLAS_UNROLL_FULL
319 for (int bn = 0; bn < NBN; ++bn) {
320BATCHLAS_UNROLL_FULL
321 for (int v = 0; v < WbN; ++v) {
322 const int col = n0 + bn * NSep + tx * WbN + v;
323 if (col >= n) {
324 continue;
325 }
326 D* p = &Cb[row + static_cast<std::ptrdiff_t>(col) * ldc];
327 const D prior = beta_zero ? D{} : *p;
328 *p = lin_epi(alpha_d, beta_zero ? D{} : beta_d,
329 accum[bm * WbM + w][bn * WbN + v], prior);
330 }
331 }
332 }
333 }
334 });
335 });
336
337 return ctx.get_event();
338}
339
340} // namespace batchlas::sycl_gemm
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
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
Transpose
The operator op() applied to an operand: op(A) = A, A^T or A^H.
Definition enums.hh:171
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
float lin_epi(float alpha, float beta, float acc, float prior)
const Vec16< D > & vec_ref(const D *p)
Event launch_wide_transposed(Queue &ctx, const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &B, const MatrixView< T, MatrixFormat::Dense > &C, T alpha, T beta, const char *trace)
bool wide_trans_matches(Transpose requested, Transpose instantiated)
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.