register_128x128.hh
Go to the documentation of this file.
1#pragma once
2
3// A 128x128x8 register-tiled GEMM with 8x8 accumulators per thread.
4//
5// Shallow K (8) with M = N = 128 buys an 8x8 thread tile, so a k-step is 4
6// vectorized shared loads for 64 FFMAs. The shared tiles use stride exactly
7// TileM / TileN (an odd stride defeats the 16-byte alignment the LDS.128 form
8// needs), and B is staged [k][n] so a thread's 8 B values are contiguous.
9//
10// Four more things are load-bearing, and each fails silently if undone:
11// * prefetch registers are clang ext_vector values, NOT a struct (LLVM turns a
12// struct copy into a memcpy sunk to the store: no prefetch at all);
13// * the lane swizzle: m takes lane bit 0, n lane bit 1 (an LDS.128 whose address
14// depends on both bits costs twice the wavefronts); warp sub-tile 64 x 32;
15// * the L2::128B fetch hint on the B loads;
16// * the launch bound, two groups per CU (128-register cap), is a guard: the
17// kernel fits at 127, one register more is one group per SM, 3/CU spills.
18// evidence: docs/perf/gemm.md#the-128x128-float-kernel
19
23
25
26#include <src/linalg-impl.hh>
27#include <src/sycl/kernel_attrs.hh>
28
29#include <sycl/sycl.hpp>
30
31namespace batchlas::sycl_gemm {
32
33template <typename T, bool AlignedFastPath, bool StagedEpilogue>
35
36template <typename T>
37inline const Vec4<T>& vec4_ref(const T* p) {
38 return *reinterpret_cast<const Vec4<T>*>(__builtin_assume_aligned(p, 4 * sizeof(T)));
39}
40
41template <typename T>
42inline Vec4<T>& vec4_ref(T* p) {
43 return *reinterpret_cast<Vec4<T>*>(__builtin_assume_aligned(p, 4 * sizeof(T)));
44}
45
46// A 16-byte global load that asks L2 to fetch the whole 128-byte line. PTX has
47// no portable spelling, so every other target takes the plain load.
48template <typename T>
49inline Vec4<T> load4_l2_line(const T* p) {
50#if defined(__SYCL_DEVICE_ONLY__) && defined(__NVPTX__)
51 if constexpr (std::is_same_v<T, float>) {
52 Vec4<T> r;
53 asm("ld.global.L2::128B.v4.f32 {%0, %1, %2, %3}, [%4];"
54 : "=f"(r.x), "=f"(r.y), "=f"(r.z), "=f"(r.w)
55 : "l"(p));
56 return r;
57 }
58#elif BATCHLAS_SYCL_IMPL_ACPP && defined(__x86_64__)
59 // SSCP: the asm passes x86 Sema (no "=f"; `{` is a dialect brace, hence %{ %}).
60 // An arm64 host (macOS) rejects "l" and has no CUDA device: it takes the plain load.
61 if constexpr (std::is_same_v<T, float>) {
62 if (sycl_impl::on_ptx()) {
63 unsigned x = 0, y = 0, z = 0, w = 0;
64 __acpp_if_target_sscp(asm("ld.global.L2::128B.v4.b32 %{%0, %1, %2, %3%}, [%4];"
65 : "=r"(x), "=r"(y), "=r"(z), "=r"(w)
66 : "l"(p));)
67 return Vec4<T>{__builtin_bit_cast(T, x), __builtin_bit_cast(T, y),
68 __builtin_bit_cast(T, z), __builtin_bit_cast(T, w)};
69 }
70 }
71#endif
72 return vec4_ref(p);
73}
74
75inline constexpr int kStagedEpilogueMaxK = 64;
76
77// The scalar form, for the predicated leg's B loads.
78template <typename T>
79inline T load1_l2_line(const T* p) {
80#if defined(__SYCL_DEVICE_ONLY__) && defined(__NVPTX__)
81 if constexpr (std::is_same_v<T, float>) {
82 T r;
83 asm("ld.global.L2::128B.f32 %0, [%1];" : "=f"(r) : "l"(p));
84 return r;
85 }
86#elif BATCHLAS_SYCL_IMPL_ACPP && defined(__x86_64__)
87 if constexpr (std::is_same_v<T, float>) {
88 if (sycl_impl::on_ptx()) {
89 unsigned r = 0;
90 __acpp_if_target_sscp(asm("ld.global.L2::128B.b32 %0, [%1];" : "=r"(r) : "l"(p));)
91 return __builtin_bit_cast(T, r);
92 }
93 }
94#endif
95 return *p;
96}
97
98// Does this problem satisfy everything the unpredicated path assumes?
99template <typename T>
103 constexpr int TileM = 128, TileN = 128, TileK = 8;
104 const auto m = A.rows();
105 const auto k = A.cols();
106 const auto n = B.cols();
107 // k == 0 would pass the modulus test, and the prefetch of slab 0 runs before the loop.
108 if (k < TileK || (m % TileM) != 0 || (n % TileN) != 0 || (k % TileK) != 0) {
109 return false;
110 }
111 // Every 128-bit access this kernel makes is at a multiple of 4 elements
112 // from the base pointer, so the base itself must be 4-element aligned and
113 // the leading dimensions must preserve that.
114 auto aligned = [](const T* p, int ld, int stride) {
115 return p != nullptr && (reinterpret_cast<std::uintptr_t>(p) % (4 * sizeof(T))) == 0 &&
116 (ld % 4) == 0 && (stride % 4) == 0;
117 };
118 return aligned(A.data_ptr(), A.ld(), A.stride()) &&
119 aligned(B.data_ptr(), B.ld(), B.stride()) &&
120 aligned(C.data_ptr(), C.ld(), C.stride());
121}
122
123// A FUNCTOR, not a lambda, only so the launch bound can be spelled.
124// StagedEpilogue (aligned leg only) routes C through local memory so each
125// warp stores whole 128-row columns; see launch_register_128x128_k8.
126template <typename T, bool AlignedFastPath, bool StagedEpilogue>
128 static constexpr int TileM = 128, TileN = 128, TileK = 8;
129 static constexpr int ThreadTile = 8, Band = 4, Threads = 256;
130 static constexpr int BandM = 32, BandN = 16; // second band's offset
131 static constexpr int AStride = TileM, BStride = TileN;
132 static constexpr int kMinGroupsPerCu = 2;
133 static constexpr int StageStride = TileM + 4; // staged C column stride
134 static constexpr int TileAElems = StagedEpilogue ? 32 * StageStride : 2 * TileK * AStride;
135 static_assert(!StagedEpilogue || AlignedFastPath, "the staged epilogue stores unpredicated");
136
137 const T* a_ptr;
138 const T* b_ptr;
140 int m, n, k, lda, ldb, ldc;
141 std::ptrdiff_t stride_a, stride_b, stride_c;
142 int batch;
144 sycl::local_accessor<T, 1> tile_a, tile_b;
145
146 BATCHLAS_LAUNCH_BOUNDS(Threads, kMinGroupsPerCu)
147 void operator()(sycl::nd_item<3> item) const {
148 const int bid = static_cast<int>(item.get_group(0));
149 if (bid >= batch) {
150 return;
151 }
152 const int tid = static_cast<int>(item.get_local_id(2));
153 const int lane = tid & 31, warp = tid >> 5;
154 const int ml = (lane & 1) | (((lane >> 2) & 3) << 1);
155 const int nl = ((lane >> 1) & 1) | (((lane >> 4) & 1) << 1);
156 const int mb = (warp & 1) * 64 + ml * Band;
157 const int nb = (warp >> 1) * 32 + nl * Band;
158
159 const int m0 = static_cast<int>(item.get_group(1)) * TileM;
160 const int n0 = static_cast<int>(item.get_group(2)) * TileN;
161 const T* Ab = a_ptr + static_cast<std::ptrdiff_t>(bid) * stride_a;
162 const T* Bb = b_ptr + static_cast<std::ptrdiff_t>(bid) * stride_b;
163 T* Cb = c_ptr + static_cast<std::ptrdiff_t>(bid) * stride_c;
164 T* sa = tile_a.template get_multi_ptr<sycl::access::decorated::no>().get();
165 T* sb = tile_b.template get_multi_ptr<sycl::access::decorated::no>().get();
166
167 T accum[ThreadTile][ThreadTile];
168BATCHLAS_UNROLL_FULL
169 for (int i = 0; i < ThreadTile; ++i) {
170BATCHLAS_UNROLL_FULL
171 for (int j = 0; j < ThreadTile; ++j) {
172 accum[i][j] = T(0);
173 }
174 }
175
176 // A is read down m, B down k and transposed into shared, both
177 // coalesced; one packet of each per thread per slab.
178 const int a_m = (tid % 32) * 4;
179 const int a_k = tid / 32;
180 const int b_k = (tid % 2) * 4;
181 const int b_n = tid / 2;
182 const T* ga = Ab + (m0 + a_m) + static_cast<std::ptrdiff_t>(a_k) * lda;
183 const T* gb = Bb + b_k + static_cast<std::ptrdiff_t>(n0 + b_n) * ldb;
184
185 Vec4<T> ra, rb;
186 auto gload = [&](int k0) __attribute__((always_inline)) {
187 if constexpr (AlignedFastPath) {
188 ra = vec4_ref(ga + static_cast<std::ptrdiff_t>(k0) * lda);
189 rb = load4_l2_line(gb + k0);
190 } else {
191 // Zero outside the matrix, so the inner loop needs no bounds.
192 const int gk_a = k0 + a_k;
193 const int gn_b = n0 + b_n;
194BATCHLAS_UNROLL_FULL
195 for (int i = 0; i < 4; ++i) {
196 const int gm = m0 + a_m + i;
197 ra[i] = (gm < m && gk_a < k) ? ga[static_cast<std::ptrdiff_t>(k0) * lda + i] : T(0);
198 rb[i] = (k0 + b_k + i < k && gn_b < n) ? load1_l2_line(gb + k0 + i) : T(0);
199 }
200 }
201 };
202 auto sstore = [&](int buf) __attribute__((always_inline)) {
203 vec4_ref(&sa[buf * TileK * AStride + a_k * AStride + a_m]) = ra;
204BATCHLAS_UNROLL_FULL
205 for (int i = 0; i < 4; ++i) {
206 sb[buf * TileK * BStride + (b_k + i) * BStride + b_n] = rb[i];
207 }
208 };
209 auto compute = [&](int buf) __attribute__((always_inline)) {
210 const T* xa = sa + buf * TileK * AStride;
211 const T* xb = sb + buf * TileK * BStride;
212BATCHLAS_UNROLL_FULL
213 for (int kk = 0; kk < TileK; ++kk) {
214 const Vec4<T> a0 = vec4_ref(&xa[kk * AStride + mb]);
215 const Vec4<T> a1 = vec4_ref(&xa[kk * AStride + BandM + mb]);
216 const Vec4<T> b0 = vec4_ref(&xb[kk * BStride + nb]);
217 const Vec4<T> b1 = vec4_ref(&xb[kk * BStride + BandN + nb]);
218 const T af[ThreadTile] = {a0.x, a0.y, a0.z, a0.w, a1.x, a1.y, a1.z, a1.w};
219 const T bf[ThreadTile] = {b0.x, b0.y, b0.z, b0.w, b1.x, b1.y, b1.z, b1.w};
220BATCHLAS_UNROLL_FULL
221 for (int i = 0; i < ThreadTile; ++i) {
222BATCHLAS_UNROLL_FULL
223 for (int j = 0; j < ThreadTile; ++j) {
224 accum[i][j] += af[i] * bf[j];
225 }
226 }
227 }
228 };
229
230 // One barrier per slab is enough: slab s+1 is stored into the buffer
231 // slab s-1 was read from, and every read of that finished before the
232 // barrier that ended iteration s-1. Unrolled by two so the buffer
233 // index is a constant; a runtime one costs a spill at the 128 cap.
234 const int slabs = (k + TileK - 1) / TileK;
235 gload(0);
236 sstore(0);
237 item.barrier(sycl::access::fence_space::local_space);
238 int s = 0;
239 for (; s + 1 < slabs; s += 2) {
240 gload((s + 1) * TileK);
241 compute(0);
242 sstore(1);
243 item.barrier(sycl::access::fence_space::local_space);
244 const bool more = s + 2 < slabs;
245 if (more) gload((s + 2) * TileK);
246 compute(1);
247 if (more) sstore(0);
248 item.barrier(sycl::access::fence_space::local_space);
249 }
250 if (s < slabs) {
251 compute(0); // an odd slab count leaves the last slab in buffer 0
252 }
253
254 if constexpr (StagedEpilogue) {
255 // Pass p stages the 32 tile columns c with c % 4 == p, slot c / 4,
256 // then each warp stores 4 of them as whole 128-row columns.
257 T* sc = sa;
258 const int tid2 = static_cast<int>(item.get_local_id(2));
259 item.barrier(sycl::access::fence_space::local_space); // the odd tail read sa
260BATCHLAS_UNROLL_FULL
261 for (int p = 0; p < Band; ++p) {
262BATCHLAS_UNROLL_FULL
263 for (int half = 0; half < 2; ++half) {
264 const int slot = (nb >> 2) + half * (BandN / 4);
265BATCHLAS_UNROLL_FULL
266 for (int band = 0; band < 2; ++band) {
267 Vec4<T> v;
268BATCHLAS_UNROLL_FULL
269 for (int i = 0; i < 4; ++i) {
270 v[i] = accum[band * Band + i][p + half * Band];
271 }
272 vec4_ref(&sc[slot * StageStride + band * BandM + mb]) = v;
273 }
274 }
275 item.barrier(sycl::access::fence_space::local_space);
276BATCHLAS_UNROLL_FULL
277 for (int q = 0; q < 4; ++q) {
278 const int slot = (tid2 >> 5) * 4 + q;
279 const int row = (tid2 & 31) * 4;
280 T* dst = &Cb[m0 + row + static_cast<std::ptrdiff_t>(n0 + slot * 4 + p) * ldc];
281 const Vec4<T> v = vec4_ref(&sc[slot * StageStride + row]);
282 Vec4<T> out;
283 if (beta == T(0)) {
284 out = alpha * v;
285 } else {
286 const Vec4<T> prior = vec4_ref(const_cast<const T*>(dst));
287BATCHLAS_UNROLL_FULL
288 for (int i = 0; i < 4; ++i) {
289 out[i] = LinearEpilogue<T>::apply(alpha, beta, v[i], prior[i]);
290 }
291 }
292 vec4_ref(dst) = out;
293 }
294 item.barrier(sycl::access::fence_space::local_space);
295 }
296 return;
297 }
298
299 // Within a band the four rows are consecutive in m, the contiguous
300 // direction of a column-major C, so a band is one 128-bit access.
301BATCHLAS_UNROLL_FULL
302 for (int band = 0; band < 2; ++band) {
303 const int gm = m0 + band * BandM + mb;
304BATCHLAS_UNROLL_FULL
305 for (int j = 0; j < ThreadTile; ++j) {
306 const int gn = n0 + (j < Band ? nb + j : BandN + nb + j - Band);
307 if constexpr (AlignedFastPath) {
308 T* p = &Cb[gm + static_cast<std::ptrdiff_t>(gn) * ldc];
309 Vec4<T> out;
310 if (beta == T(0)) {
311BATCHLAS_UNROLL_FULL
312 for (int i = 0; i < 4; ++i) {
313 out[i] = alpha * accum[band * Band + i][j];
314 }
315 } else {
316 const Vec4<T> prior = vec4_ref(const_cast<const T*>(p));
317BATCHLAS_UNROLL_FULL
318 for (int i = 0; i < 4; ++i) {
319 out[i] = LinearEpilogue<T>::apply(alpha, beta, accum[band * Band + i][j], prior[i]);
320 }
321 }
322 vec4_ref(p) = out;
323 } else {
324 if (gn >= n) {
325 continue;
326 }
327BATCHLAS_UNROLL_FULL
328 for (int i = 0; i < 4; ++i) {
329 const int row = gm + i;
330 if (row >= m) {
331 continue;
332 }
333 T* p = &Cb[row + static_cast<std::ptrdiff_t>(gn) * ldc];
334 *p = LinearEpilogue<T>::apply(alpha, beta, accum[band * Band + i][j],
335 beta == T(0) ? T(0) : *p);
336 }
337 }
338 }
339 }
340 }
341};
342
343template <typename T, bool AlignedFastPath, bool StagedEpilogue>
348 T alpha,
349 T beta,
350 const char* (*kernel_trace_name)(KernelVariant)) {
351 BATCHLAS_KERNEL_TRACE_SCOPE(kernel_trace_name(KernelVariant::Tiled128x128RegisterK8));
353
354 const int m = static_cast<int>(A.rows());
355 const int k = static_cast<int>(A.cols());
356 const int n = static_cast<int>(B.cols());
357 const int group_rows = (m + Body::TileM - 1) / Body::TileM;
358 const int group_cols = (n + Body::TileN - 1) / Body::TileN;
359
360 const sycl::range<3> local(1, 1, Body::Threads);
361 const sycl::range<3> global(static_cast<size_t>(A.batch_size()), static_cast<size_t>(group_rows),
362 static_cast<size_t>(group_cols) * Body::Threads);
363
364 ctx->submit([&](sycl::handler& h) {
365 sycl::local_accessor<T, 1> tile_a(sycl::range<1>(Body::TileAElems), h);
366 sycl::local_accessor<T, 1> tile_b(sycl::range<1>(2 * Body::TileK * Body::BStride), h);
367 Body body{A.data_ptr(), B.data_ptr(), C.data_ptr(), m, n, k, A.ld(), B.ld(), C.ld(),
368 A.stride(), B.stride(), C.stride(), A.batch_size(), alpha, beta, tile_a, tile_b};
369 batchlas::impl::parallel_for<GemmRegister128x128Kernel<T, AlignedFastPath, StagedEpilogue>>(h,
370 sycl::nd_range<3>(global, local), body);
371 });
372
373 return ctx.get_event();
374}
375
376// Short k is store-bound, and there the direct epilogue's four 128-byte
377// column pieces per store run DRAM measurably slower than whole columns; at
378// long k the staging round trip costs more than it saves.
379// evidence: docs/perf/gemm.md#the-staged-epilogue
380template <typename T, bool AlignedFastPath = false>
385 T alpha,
386 T beta,
387 const char* (*kernel_trace_name)(KernelVariant)) {
388 if constexpr (AlignedFastPath) {
389 if (A.cols() <= kStagedEpilogueMaxK) {
390 return launch_register_128x128_k8_leg<T, true, true>(ctx, A, B, C, alpha, beta, kernel_trace_name);
391 }
392 }
393 return launch_register_128x128_k8_leg<T, AlignedFastPath, false>(ctx, A, B, C, alpha, beta,
394 kernel_trace_name);
395}
396
397} // 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
constexpr int kStagedEpilogueMaxK
Vec4< T > load4_l2_line(const T *p)
const Vec4< T > & vec4_ref(const T *p)
Event launch_register_128x128_k8_leg(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))
T load1_l2_line(const T *p)
T __attribute__((ext_vector_type(4))) Vec4
Definition vec4.hh:33
Event launch_register_128x128_k8(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_128x128_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.
sycl::local_accessor< T, 1 > tile_a
sycl::local_accessor< T, 1 > tile_b
static T apply(T alpha, T beta, T accum, T prior)