register_64x64_k16_wide.hh
Go to the documentation of this file.
1#pragma once
2
3// A 64x64x16 register-tiled GEMM with a 4x4 thread tile, for wide scalars:
4// double, complex<float> and complex<double>; float routes to the in-tree
5// 128x128 kernel instead. The 4x4 tile is the widest shape that stays inside
6// the 65,536 registers-per-block limit -- the 128x128 kernel's 8x8 tile throws
7// at launch for complex<double>.
8// evidence: docs/perf/gemm.md#the-wide-scalar-kernel
9
10#include <src/sycl/device_scalar.hh>
12
13#include <src/linalg-impl.hh>
14#include <src/sycl/kernel_attrs.hh>
15
16#include <batchlas/backend_config.h>
17
18#include <sycl/sycl.hpp>
19
20#include <complex>
21#include <cstdint>
22#include <type_traits>
23
24namespace batchlas::sycl_gemm {
25
26namespace wide_scalar {
27
28// std::complex is deliberately kept out of device code; see ../device_scalar.hh.
29using batchlas::sycl_device::Cx;
30using batchlas::sycl_device::DevMap;
31using batchlas::sycl_device::dev_is_zero;
32using batchlas::sycl_device::fma_acc;
33
34inline float lin_epi(float alpha, float beta, float acc, float prior) {
35 return alpha * acc + beta * prior;
36}
37inline double lin_epi(double alpha, double beta, double acc, double prior) {
38 return alpha * acc + beta * prior;
39}
40template <typename R>
41inline Cx<R> lin_epi(Cx<R> alpha, Cx<R> beta, Cx<R> acc, Cx<R> prior) {
42 Cx<R> o;
43 o.re = alpha.re * acc.re - alpha.im * acc.im + beta.re * prior.re - beta.im * prior.im;
44 o.im = alpha.re * acc.im + alpha.im * acc.re + beta.re * prior.im + beta.im * prior.re;
45 return o;
46}
47
48// The access granule is 16 BYTES, not 4 elements, whatever the scalar width:
49// that is what keeps an 8-lane LDS phase on exactly the 32 banks.
50// may_alias is load-bearing: without it -O3 may reorder the shared stores
51// against the fragment loads across the barrier.
52template <typename D>
53struct alignas(16) __attribute__((may_alias)) Vec16 {
54 static constexpr int N = 16 / static_cast<int>(sizeof(D));
55 D v[N];
56};
57
58template <typename D>
59inline const Vec16<D>& vec_ref(const D* p) {
60 return *reinterpret_cast<const Vec16<D>*>(p);
61}
62template <typename D>
63inline Vec16<D>& vec_ref(D* p) {
64 return *reinterpret_cast<Vec16<D>*>(p);
65}
66
67// A raw 16-byte move for staging. It must be a native vector type, not a
68// `struct { T v[N]; }`: SROA splits a struct copy back into element loads and
69// stores and the 16-byte form is lost. Metal has neither vectors nor double; its emitter
70// maps an i128 to a uint4, so there the move is one 16-byte integer.
71#if BATCHLAS_DEVICE_NO_VECTOR_TYPES
72typedef unsigned __int128 Raw16 __attribute__((may_alias));
73#else
74typedef double Raw16Base __attribute__((ext_vector_type(2)));
75typedef Raw16Base Raw16 __attribute__((may_alias));
76#endif
77
78inline Raw16 raw16_load(const void* p) { return *reinterpret_cast<const Raw16*>(p); }
79inline void raw16_store(void* p, Raw16 v) { *reinterpret_cast<Raw16*>(p) = v; }
80
81struct Tile {
82 static constexpr int M = 64;
83 static constexpr int N = 64;
84 static constexpr int K = 16;
85 static constexpr int TT = 4; // thread tile, both directions
86 static constexpr int LocalRows = M / TT; // 16, the m direction
87 static constexpr int LocalCols = N / TT; // 16, the n direction
88 static constexpr int Threads = LocalRows * LocalCols; // 256
89 // No padding: with an odd stride the compiler cannot prove 16-byte
90 // alignment and every fragment load degrades to a scalar ld.shared.
91 static constexpr int AStride = M;
92 static constexpr int BStride = N;
93 static constexpr int PerThreadA = M * K / Threads; // 4
94 static constexpr int PerThreadB = K * N / Threads; // 4
95};
96
97static_assert(Tile::M * Tile::K == Tile::PerThreadA * Tile::Threads, "A staging");
98static_assert(Tile::K * Tile::N == Tile::PerThreadB * Tile::Threads, "B staging");
99
100} // namespace wide_scalar
101
102template <typename T, bool AlignedFastPath>
104
105// VecLen counts elements; the granule the fast path enforces is 16 bytes.
106template <typename T>
110 constexpr int TileM = wide_scalar::Tile::M;
111 constexpr int TileN = wide_scalar::Tile::N;
112 constexpr int TileK = wide_scalar::Tile::K;
113 constexpr int VecLen = 16 / static_cast<int>(sizeof(T));
114 const auto m = A.rows();
115 const auto k = A.cols();
116 const auto n = B.cols();
117 if ((m % TileM) != 0 || (n % TileN) != 0 || (k % TileK) != 0) {
118 return false;
119 }
120 auto aligned = [](const T* p, int64_t ld, int64_t stride) {
121 return p != nullptr && (reinterpret_cast<std::uintptr_t>(p) % 16u) == 0 &&
122 (ld % VecLen) == 0 && (stride % VecLen) == 0;
123 };
124 return aligned(A.data_ptr(), A.ld(), A.stride()) &&
125 aligned(B.data_ptr(), B.ld(), B.stride()) &&
126 aligned(C.data_ptr(), C.ld(), C.stride());
127}
128
129// NN only: A is read as m x k and B as k x n directly, so a transposed operand
130// has to fall back at the caller rather than be transposed here.
131template <typename T, bool AlignedFastPath = false>
136 T alpha,
137 T beta,
138 const char* (*kernel_trace_name)(KernelVariant)) {
139 BATCHLAS_KERNEL_TRACE_SCOPE(
141
142 using namespace wide_scalar;
143
144 // std::complex is re-typed to the POD device type here, at the pointer
145 // boundary, and never crosses into the kernel body -- alpha and beta too.
146 using D = typename DevMap<T>::type;
147 static_assert(sizeof(D) == sizeof(T), "device scalar must be layout-compatible");
148
149 constexpr int TileM = Tile::M;
150 constexpr int TileN = Tile::N;
151 constexpr int TileK = Tile::K;
152 constexpr int TT = Tile::TT;
153 constexpr int LocalRows = Tile::LocalRows;
154 constexpr int LocalCols = Tile::LocalCols;
155 constexpr int AStride = Tile::AStride;
156 constexpr int BStride = Tile::BStride;
157
158 constexpr int VecN = Vec16<D>::N; // elements per 16 bytes
159 constexpr int Wb = (VecN < TT) ? VecN : TT; // band width in elements
160 constexpr int NB = TT / Wb; // number of bands
161 constexpr int MSep = TileM / NB; // band separation, m
162 constexpr int NSep = TileN / NB; // band separation, n
163 constexpr int Chunks = Tile::PerThreadA / VecN; // 16B chunks per stage
164 constexpr int AGranPerRow = TileM / VecN; // 16B granules per k-row of A
165
166 static_assert(TileM % VecN == 0, "A tile must split into whole granules");
167 static_assert(AGranPerRow * TileK == Chunks * Tile::Threads, "A granule handout");
168 static_assert(TT % Wb == 0, "thread tile must split into whole bands");
169 static_assert(Tile::PerThreadA % VecN == 0, "staging must be whole granules");
170 static_assert(Tile::PerThreadA == Tile::PerThreadB, "Chunks is shared by A and B staging");
171 static_assert(MSep % VecN == 0 && NSep % VecN == 0, "band base must stay aligned");
172 static_assert(AStride % VecN == 0 && BStride % VecN == 0, "shared stride alignment");
173
174 const int m = static_cast<int>(A.rows());
175 const int k = static_cast<int>(A.cols());
176 const int n = static_cast<int>(B.cols());
177
178 const int gm_tiles = (m + TileM - 1) / TileM;
179 const int gn_tiles = (n + TileN - 1) / TileN;
180
181 // local_id(2) is the fastest-varying SYCL dimension; it carries m.
182 const sycl::range<3> local(1, LocalCols, LocalRows);
183 const sycl::range<3> global(static_cast<size_t>(A.batch_size()),
184 static_cast<size_t>(gn_tiles) * LocalCols,
185 static_cast<size_t>(gm_tiles) * LocalRows);
186
187 ctx->submit([&](sycl::handler& h) {
188 sycl::local_accessor<Vec16<D>, 1> tile_a(sycl::range<1>(TileK * AStride / VecN), h);
189 sycl::local_accessor<Vec16<D>, 1> tile_b(sycl::range<1>(TileK * BStride / VecN), h);
190
191 const D* a_ptr = reinterpret_cast<const D*>(A.data_ptr());
192 const D* b_ptr = reinterpret_cast<const D*>(B.data_ptr());
193 D* c_ptr = reinterpret_cast<D*>(C.data_ptr());
194 const int lda = static_cast<int>(A.ld());
195 const int ldb = static_cast<int>(B.ld());
196 const int ldc = static_cast<int>(C.ld());
197 const int stride_a = static_cast<int>(A.stride());
198 const int stride_b = static_cast<int>(B.stride());
199 const int stride_c = static_cast<int>(C.stride());
200 const int batch = static_cast<int>(A.batch_size());
201
202 D alpha_d;
203 D beta_d;
204 __builtin_memcpy(&alpha_d, &alpha, sizeof(D));
205 __builtin_memcpy(&beta_d, &beta, sizeof(D));
206
207 batchlas::impl::parallel_for<GemmRegister64x64K16WideKernel<T, AlignedFastPath>>(h,
208 sycl::nd_range<3>(global, local), [=](sycl::nd_item<3> item) {
209 const int bid = static_cast<int>(item.get_group(0));
210 if (bid >= batch) {
211 return;
212 }
213
214 // m is the fastest-varying thread index: C is column-major, so
215 // lanes differing in m touch consecutive addresses. Reversing it
216 // makes the read of C one transaction per lane at beta != 0.
217 const int ty = static_cast<int>(item.get_local_id(2)); // 0..15, m
218 const int tx = static_cast<int>(item.get_local_id(1)); // 0..15, n
219 const int tid = tx * LocalRows + ty; // linear local id
220
221 const int m0 = static_cast<int>(item.get_group(2)) * TileM;
222 const int n0 = static_cast<int>(item.get_group(1)) * TileN;
223
224 const D* Ab = a_ptr + static_cast<std::ptrdiff_t>(bid) * stride_a;
225 const D* Bb = b_ptr + static_cast<std::ptrdiff_t>(bid) * stride_b;
226 D* Cb = c_ptr + static_cast<std::ptrdiff_t>(bid) * stride_c;
227
228 D* sa = reinterpret_cast<D*>(
229 tile_a.template get_multi_ptr<sycl::access::decorated::no>().get());
230 D* sb = reinterpret_cast<D*>(
231 tile_b.template get_multi_ptr<sycl::access::decorated::no>().get());
232
233 // Plain local array, fully unrolled: this is the register file.
234 D accum[TT][TT];
235BATCHLAS_UNROLL_FULL
236 for (int i = 0; i < TT; ++i) {
237BATCHLAS_UNROLL_FULL
238 for (int j = 0; j < TT; ++j) {
239 accum[i][j] = D{};
240 }
241 }
242
243 // A is column-major m x k. Granule ids go out in linear tid
244 // order so consecutive lanes take consecutive 16-byte granules.
245 // Handing each thread PerThreadA consecutive *elements*, as the
246 // float kernels do, is a 4- to 8-way shared-store conflict here.
247 int a_gm[Chunks];
248 int a_gk[Chunks];
249BATCHLAS_UNROLL_FULL
250 for (int c = 0; c < Chunks; ++c) {
251 const int g = tid + c * Tile::Threads;
252 a_gm[c] = (g % AGranPerRow) * VecN;
253 a_gk[c] = g / AGranPerRow;
254 }
255
256 // B is scattered into shared as [k][n]. The 4-way bank conflict
257 // on the store is deliberate: it keeps the read of B coalesced.
258 const int b_k = (tid % (TileK / Tile::PerThreadB)) * Tile::PerThreadB;
259 const int b_n = tid / (TileK / Tile::PerThreadB);
260
261 for (int k0 = 0; k0 < k; k0 += TileK) {
262 if constexpr (AlignedFastPath) {
263BATCHLAS_UNROLL_FULL
264 for (int c = 0; c < Chunks; ++c) {
265 raw16_store(
266 &sa[a_gk[c] * AStride + a_gm[c]],
267 raw16_load(Ab + (m0 + a_gm[c]) +
268 static_cast<std::ptrdiff_t>(k0 + a_gk[c]) * lda));
269 }
270 alignas(16) D vb[Tile::PerThreadB];
271BATCHLAS_UNROLL_FULL
272 for (int c = 0; c < Chunks; ++c) {
273 raw16_store(&vb[c * VecN],
274 raw16_load(Bb + (k0 + b_k + c * VecN) +
275 static_cast<std::ptrdiff_t>(n0 + b_n) * ldb));
276 }
277BATCHLAS_UNROLL_FULL
278 for (int i = 0; i < Tile::PerThreadB; ++i) {
279 sb[(b_k + i) * BStride + b_n] = vb[i];
280 }
281 } else {
282 // Predicated staging fills the tile to its full 64x16,
283 // zero outside the matrix, so the inner loop below needs
284 // no bounds checks and is identical in both paths.
285BATCHLAS_UNROLL_FULL
286 for (int c = 0; c < Chunks; ++c) {
287 const int gk_a = k0 + a_gk[c];
288BATCHLAS_UNROLL_FULL
289 for (int e = 0; e < VecN; ++e) {
290 const int gm = m0 + a_gm[c] + e;
291 sa[a_gk[c] * AStride + a_gm[c] + e] =
292 (gm < m && gk_a < k)
293 ? Ab[gm + static_cast<std::ptrdiff_t>(gk_a) * lda]
294 : D{};
295 }
296 }
297 const int gn_b = n0 + b_n;
298BATCHLAS_UNROLL_FULL
299 for (int i = 0; i < Tile::PerThreadB; ++i) {
300 const int gk = k0 + b_k + i;
301 sb[(b_k + i) * BStride + b_n] =
302 (gk < k && gn_b < n)
303 ? Bb[gk + static_cast<std::ptrdiff_t>(gn_b) * ldb]
304 : D{};
305 }
306 }
307 item.barrier(sycl::access::fence_space::local_space);
308
309BATCHLAS_UNROLL_FULL
310 for (int kk = 0; kk < TileK; ++kk) {
311 D af[TT];
312 D bf[TT];
313BATCHLAS_UNROLL_FULL
314 for (int band = 0; band < NB; ++band) {
315 const Vec16<D> t =
316 vec_ref(&sa[kk * AStride + band * MSep + ty * Wb]);
317BATCHLAS_UNROLL_FULL
318 for (int w = 0; w < Wb; ++w) {
319 af[band * Wb + w] = t.v[w];
320 }
321 }
322BATCHLAS_UNROLL_FULL
323 for (int band = 0; band < NB; ++band) {
324 const Vec16<D> t =
325 vec_ref(&sb[kk * BStride + band * NSep + tx * Wb]);
326BATCHLAS_UNROLL_FULL
327 for (int w = 0; w < Wb; ++w) {
328 bf[band * Wb + w] = t.v[w];
329 }
330 }
331BATCHLAS_UNROLL_FULL
332 for (int i = 0; i < TT; ++i) {
333BATCHLAS_UNROLL_FULL
334 for (int j = 0; j < TT; ++j) {
335 fma_acc(accum[i][j], af[i], bf[j]);
336 }
337 }
338 }
339 item.barrier(sycl::access::fence_space::local_space);
340 }
341
342 // Within a band the Wb rows are consecutive in m, so a whole
343 // band is one 16-byte access into column-major C.
344 const bool beta_zero = dev_is_zero(beta_d);
345BATCHLAS_UNROLL_FULL
346 for (int bm = 0; bm < NB; ++bm) {
347 const int gm = m0 + bm * MSep + ty * Wb;
348BATCHLAS_UNROLL_FULL
349 for (int j = 0; j < TT; ++j) {
350 const int bn = j / Wb;
351 const int w_n = j % Wb;
352 const int gn = n0 + bn * NSep + tx * Wb + w_n;
353 if constexpr (AlignedFastPath) {
354 D* p = &Cb[gm + static_cast<std::ptrdiff_t>(gn) * ldc];
355 Vec16<D> out;
356 if (beta_zero) {
357BATCHLAS_UNROLL_FULL
358 for (int w = 0; w < Wb; ++w) {
359 out.v[w] =
360 lin_epi(alpha_d, D{}, accum[bm * Wb + w][j], D{});
361 }
362 } else {
363 const Vec16<D> prior = vec_ref(const_cast<const D*>(p));
364BATCHLAS_UNROLL_FULL
365 for (int w = 0; w < Wb; ++w) {
366 out.v[w] = lin_epi(alpha_d, beta_d,
367 accum[bm * Wb + w][j], prior.v[w]);
368 }
369 }
370 vec_ref(p) = out;
371 } else {
372 if (gn >= n) {
373 continue;
374 }
375BATCHLAS_UNROLL_FULL
376 for (int w = 0; w < Wb; ++w) {
377 const int row = gm + w;
378 if (row >= m) {
379 continue;
380 }
381 D* p = &Cb[row + static_cast<std::ptrdiff_t>(gn) * ldc];
382 const D prior = beta_zero ? D{} : *p;
383 *p = lin_epi(alpha_d, beta_zero ? D{} : beta_d,
384 accum[bm * Wb + w][j], prior);
385 }
386 }
387 }
388 }
389 });
390 });
391
392 return ctx.get_event();
393}
394
395} // 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
float lin_epi(float alpha, float beta, float acc, float prior)
double Raw16Base __attribute__((ext_vector_type(2)))
const Vec16< D > & vec_ref(const D *p)
Event launch_register_64x64_k16_wide(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 *(*kernel_trace_name)(KernelVariant))
bool can_use_64x64_k16_wide_fast_path(const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &B, const MatrixView< T, MatrixFormat::Dense > &C)
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.