small_batched.hh
Go to the documentation of this file.
1#pragma once
2
3// Batched GEMM for orders <= 32: several matrices per work-group, op(B) staged in local
4// memory, each lane holding one row of op(A) in registers and a column strip of C.
5// Lanes run DOWN the rows, so the A loads and the C stores are coalesced; Direct runs
6// them across columns, ld apart. evidence: docs/perf/gemm.md#the-small-batched-kernel
7
11
12#include <sycl/sycl.hpp>
13
14#include <complex>
15#include <cstddef>
16#include <type_traits>
17#include <src/sycl/kernel_attrs.hh>
19
21
22template <typename T, int NB>
24
25inline constexpr int kSmallWg = 128;
26
27// The largest order the kernel serves; max(m, n, k) picks the bucket.
28inline constexpr int kSmallMaxDim = 64;
29
30inline int small_bucket(int max_dim) {
31 return max_dim <= 8 ? 8 : max_dim <= 16 ? 16 : max_dim <= 32 ? 32 : 64;
32}
33
34template <typename T, int NB>
39 T alpha, T beta, Transpose transA, Transpose transB,
40 int m, int n, int k) {
41 static_assert(!std::is_class_v<T>, "real scalars only: std::complex's operator* is Annex G");
42 constexpr int kTpm = 2 * NB; // lanes per matrix: NB rows x 2 column halves
43 constexpr int kCpt = NB / 2; // columns of C per lane
44 constexpr int kG = kSmallWg / kTpm; // matrices per work-group
45 constexpr int kLdb = NB + 1; // odd: a transposed stage spreads over the banks
46 static_assert(kG >= 1 && kG * kTpm == kSmallWg, "the work-group must hold whole matrices");
47
48 const int batch = static_cast<int>(A.batch_size());
49 const int num_wg = (batch + kG - 1) / kG;
50
51 const T* a_ptr = A.data_ptr();
52 const T* b_ptr = B.data_ptr();
53 T* c_ptr = C.data_ptr();
54 const std::ptrdiff_t lda = A.ld(), ldb = B.ld(), ldc = C.ld();
55 const std::ptrdiff_t sa = A.stride(), sb = B.stride(), sc = C.stride();
56 const bool ta = (transA != Transpose::NoTrans);
57 const bool tb = (transB != Transpose::NoTrans);
58 const bool beta_zero = (beta == T(0));
59
60 ctx->submit([&](sycl::handler& h) {
61 sycl::local_accessor<T, 1> slm(sycl::range<1>(static_cast<std::size_t>(kG) * NB * kLdb), h);
62 batchlas::impl::parallel_for<GemmSmallBatchedKernel<T, NB>>(h,
63 sycl::nd_range<1>(sycl::range<1>(static_cast<std::size_t>(num_wg) * kSmallWg),
64 sycl::range<1>(kSmallWg)),
65 [=](sycl::nd_item<1> it) BATCHLAS_REQD_SG_SIZE(32) {
66 const int t = static_cast<int>(it.get_local_id(0));
67 const int slot = t / kTpm;
68 const int tt = t % kTpm;
69 const int mat = static_cast<int>(it.get_group(0)) * kG + slot;
70 // Clamped, never returned: every lane must reach the barrier below.
71 const bool live = mat < batch;
72 const std::ptrdiff_t bi = live ? mat : 0;
73 const int r = tt % NB;
74 const int c0 = (tt / NB) * kCpt;
75 T* const sB = &slm[0] + static_cast<std::ptrdiff_t>(slot) * NB * kLdb;
76 const T* const Ab = a_ptr + bi * sa;
77 const T* const Bb = b_ptr + bi * sb;
78
79 // op(B) into sB[l + c * kLdb], walked in STORAGE order so the global
80 // read is coalesced either way; zero outside k x n.
81 for (int e = tt; e < NB * NB; e += kTpm) {
82 const int s0 = e % NB, s1 = e / NB;
83 const int l = tb ? s1 : s0;
84 const int c = tb ? s0 : s1;
85 T v = T(0);
86 if (live && l < k && c < n) v = Bb[s0 + s1 * ldb];
87 sB[l + c * kLdb] = v;
88 }
89
90 // Row r of op(A), unrolled so the array stays in registers.
91 T a[NB];
92BATCHLAS_UNROLL_FULL
93 for (int l = 0; l < NB; ++l) {
94 T v = T(0);
95 if (live && r < m && l < k) v = ta ? Ab[l + r * lda] : Ab[r + l * lda];
96 a[l] = v;
97 }
98 it.barrier(sycl::access::fence_space::local_space);
99
100 T acc[kCpt];
101BATCHLAS_UNROLL_FULL
102 for (int j = 0; j < kCpt; ++j) acc[j] = T(0);
103BATCHLAS_UNROLL_FULL
104 for (int l = 0; l < NB; ++l) {
105 if (l >= k) continue; // uniform; `continue` keeps the unroll, so a[] stays put
106BATCHLAS_UNROLL_FULL
107 for (int j = 0; j < kCpt; ++j) acc[j] += a[l] * sB[l + (c0 + j) * kLdb];
108 }
109
110 if (live && r < m) {
111 T* const Cb = c_ptr + bi * sc;
112BATCHLAS_UNROLL_FULL
113 for (int j = 0; j < kCpt; ++j) {
114 const int c = c0 + j;
115 if (c < n) {
116 T* const dst = Cb + r + c * ldc;
117 *dst = beta_zero ? alpha * acc[j] : alpha * acc[j] + beta * *dst;
118 }
119 }
120 }
121 });
122 });
123 return ctx.get_event();
124}
125
126template <typename T, int NB, bool PrefetchC>
128
129// NN, 32 < max(m, n, k) <= kSmallTiledMaxDim: one matrix per work-group, A
130// ([k][m]) and B (as stored) both staged in local memory, and a 4x4 register
131// tile of C per lane, so an FMA costs 1/8 of a shared load instead of one.
132// With beta != 0 the lane reads its C tile BEFORE the barrier so that latency
133// overlaps the staging; at beta == 0 that prefetch is compiled out, because
134// its registers cost more than it saves. evidence: docs/perf/gemm.md#the-small-tiled-kernel
135inline constexpr int kSmallTiledMaxDim = 56;
136
137template <typename T, int NB, bool PrefetchC>
142 T alpha, T beta, int m, int n, int k) {
143 using V4 = sycl_gemm::Vec4<T>;
144 constexpr int kTr = NB / 4; // lanes per row and per column of 4x4 tiles
145 constexpr int kWg = kTr * kTr;
146 constexpr int kLd = NB + 4; // keeps the V4 reads 16-byte aligned
147 static_assert(NB % 4 == 0, "4x4 lane tiles");
148
149 const int batch = static_cast<int>(A.batch_size());
150 const T* a_ptr = A.data_ptr();
151 const T* b_ptr = B.data_ptr();
152 T* c_ptr = C.data_ptr();
153 const std::ptrdiff_t lda = A.ld(), ldb = B.ld(), ldc = C.ld();
154 const std::ptrdiff_t sa = A.stride(), sb = B.stride(), sc = C.stride();
155 const bool beta_zero = (beta == T(0));
156
157 ctx->submit([&](sycl::handler& h) {
158 sycl::local_accessor<T, 1> slm(sycl::range<1>(static_cast<std::size_t>(2 * NB * kLd)), h);
159 batchlas::impl::parallel_for<GemmSmallTiledKernel<T, NB, PrefetchC>>(h,
160 sycl::nd_range<1>(sycl::range<1>(static_cast<std::size_t>(batch) * kWg),
161 sycl::range<1>(kWg)),
162 [=](sycl::nd_item<1> it) {
163 const int t = static_cast<int>(it.get_local_id(0));
164 const std::ptrdiff_t bi = static_cast<std::ptrdiff_t>(it.get_group(0));
165 const int kp = (k + 3) & ~3;
166 T* const sA = &slm[0]; // sA[l * kLd + r]
167 T* const sB = sA + NB * kLd; // sB[c * kLd + l]
168 const T* const Ab = a_ptr + bi * sa;
169 const T* const Bb = b_ptr + bi * sb;
170 T* const Cb = c_ptr + bi * sc;
171 // Storage order, so both reads are coalesced; zero-padded.
172 for (int e = t; e < NB * kp; e += kWg) {
173 const int r = e % NB, l = e / NB;
174 sA[l * kLd + r] = (r < m && l < k) ? Ab[r + l * lda] : T(0);
175 }
176 for (int e = t; e < kp * NB; e += kWg) {
177 const int l = e % kp, c = e / kp;
178 sB[c * kLd + l] = (l < k && c < n) ? Bb[l + c * ldb] : T(0);
179 }
180 const int tr = t % kTr, tc = t / kTr;
181 T prior[4][4];
182 if constexpr (PrefetchC) {
183BATCHLAS_UNROLL_FULL
184 for (int j = 0; j < 4; ++j) {
185BATCHLAS_UNROLL_FULL
186 for (int i = 0; i < 4; ++i) {
187 const int r = tr * 4 + i, c = tc * 4 + j;
188 prior[i][j] = (r < m && c < n) ? Cb[r + c * ldc] : T(0);
189 }
190 }
191 }
192 it.barrier(sycl::access::fence_space::local_space);
193
194 T acc[4][4];
195BATCHLAS_UNROLL_FULL
196 for (int i = 0; i < 4; ++i) {
197BATCHLAS_UNROLL_FULL
198 for (int j = 0; j < 4; ++j) acc[i][j] = T(0);
199 }
200 for (int l0 = 0; l0 < kp; l0 += 4) {
201 V4 bv[4];
202BATCHLAS_UNROLL_FULL
203 for (int j = 0; j < 4; ++j) {
204 bv[j] = *reinterpret_cast<const V4*>(&sB[(tc * 4 + j) * kLd + l0]);
205 }
206BATCHLAS_UNROLL_FULL
207 for (int ll = 0; ll < 4; ++ll) {
208 const V4 av = *reinterpret_cast<const V4*>(&sA[(l0 + ll) * kLd + tr * 4]);
209BATCHLAS_UNROLL_FULL
210 for (int i = 0; i < 4; ++i) {
211BATCHLAS_UNROLL_FULL
212 for (int j = 0; j < 4; ++j) acc[i][j] += av[i] * bv[j][ll];
213 }
214 }
215 }
216
217BATCHLAS_UNROLL_FULL
218 for (int j = 0; j < 4; ++j) {
219 const int c = tc * 4 + j;
220BATCHLAS_UNROLL_FULL
221 for (int i = 0; i < 4; ++i) {
222 const int r = tr * 4 + i;
223 if (r < m && c < n) {
224 T* const dst = Cb + r + c * ldc;
225 if constexpr (PrefetchC) {
226 *dst = alpha * acc[i][j] + beta * prior[i][j];
227 } else {
228 *dst = beta_zero ? alpha * acc[i][j] : alpha * acc[i][j] + beta * *dst;
229 }
230 }
231 }
232 }
233 });
234 });
235 return ctx.get_event();
236}
237
238template <typename T, int NB>
243 T alpha, T beta, int m, int n, int k) {
244 if (beta == T(0)) {
245 return launch_small_tiled<T, NB, false>(ctx, A, B, C, alpha, beta, m, n, k);
246 }
247 return launch_small_tiled<T, NB, true>(ctx, A, B, C, alpha, beta, m, n, k);
248}
249
250template <typename T>
255 T alpha, T beta, Transpose transA, Transpose transB,
256 int m, int n, int k) {
257 const int max_dim = m > n ? (m > k ? m : k) : (n > k ? n : k);
258 if constexpr (std::is_same_v<T, float>) {
259 if (transA == Transpose::NoTrans && transB == Transpose::NoTrans && max_dim > 32 &&
260 max_dim <= kSmallTiledMaxDim) {
261 return max_dim <= 48 ? small_tiled<T, 48>(ctx, A, B, C, alpha, beta, m, n, k)
262 : small_tiled<T, 56>(ctx, A, B, C, alpha, beta, m, n, k);
263 }
264 }
265 switch (small_bucket(max_dim)) {
266 case 8:
267 return launch_small_batched<T, 8>(ctx, A, B, C, alpha, beta, transA, transB, m, n, k);
268 case 16:
269 return launch_small_batched<T, 16>(ctx, A, B, C, alpha, beta, transA, transB, m, n, k);
270 case 32:
271 return launch_small_batched<T, 32>(ctx, A, B, C, alpha, beta, transA, transB, m, n, k);
272 default:
273 return launch_small_batched<T, 64>(ctx, A, B, C, alpha, beta, transA, transB, m, n, k);
274 }
275}
276
277} // namespace batchlas::sycl_gemm_small
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 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
@ NoTrans
op(A) = A.
Definition enums.hh:172
The batched containers: Matrix (owning), MatrixView (non-owning), KernelMatrixView (device-side),...
Event launch_small_batched(Queue &ctx, const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &B, const MatrixView< T, MatrixFormat::Dense > &C, T alpha, T beta, Transpose transA, Transpose transB, int m, int n, int k)
int small_bucket(int max_dim)
Event launch_small_tiled(Queue &ctx, const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &B, const MatrixView< T, MatrixFormat::Dense > &C, T alpha, T beta, int m, int n, int k)
Event small_batched(Queue &ctx, const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &B, const MatrixView< T, MatrixFormat::Dense > &C, T alpha, T beta, Transpose transA, Transpose transB, int m, int n, int k)
Event small_tiled(Queue &ctx, const MatrixView< T, MatrixFormat::Dense > &A, const MatrixView< T, MatrixFormat::Dense > &B, const MatrixView< T, MatrixFormat::Dense > &C, T alpha, T beta, int m, int n, int k)
T __attribute__((ext_vector_type(4))) Vec4
Definition vec4.hh:33
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.
batchlas::Device, batchlas::Event and batchlas::Queue: where work runs and how it is ordered.