kernel-heuristics.hh
Go to the documentation of this file.
1#pragma once
2
3#include <cstddef>
4#include <algorithm>
5#include <limits>
6#include <cstdint>
7// Installed header: public includes only (src/ is not installed).
9
10/// @file
11/// @brief Launch-size heuristics used by the library's own kernels. Installed, not API.
12/// @ingroup api_internal_helpers
13
14namespace batchlas {
15
16/// @addtogroup api_internal_helpers
17/// @{
18
19/**
20 * @brief Kernel types for heuristic selection
21 */
22enum class KernelType {
23 ELEMENTWISE, ///< Element-wise operations (fill, copy, etc.)
24 REDUCTION, ///< Reduction operations (sum, max, etc.)
25 SCAN, ///< Prefix scan operations
26 MEMORY_BOUND, ///< Memory bandwidth limited
27 COMPUTE_BOUND, ///< Compute intensive
28 SPARSE, ///< Sparse matrix operations
29 GEMM, ///< General matrix multiply
30 SMALL_MATRIX, ///< Small matrix operations
31 TASK_BASED ///< Task-based parallelism, e.g. no fine-grained parallelism, each thread solves one problem
32};
33
34/// @brief Result of compute_batched_nd_range_sizes().
36 size_t global_size; ///< total work-items, a multiple of `local_size`
37 size_t local_size; ///< work-group size
38 bool use_grid_stride; ///< the kernel must grid-stride: `global_size` was capped below the total work
39};
40
41/// @brief Result of compute_batched_matrix_decomposition().
43 size_t global_size; ///< batch * work_groups_per_matrix * local_size
44 size_t local_size; ///< work-group size
45 size_t work_groups_per_matrix; ///< work-groups assigned to each matrix
46};
47
48/**
49 * @brief Compute a work-group size from the device type and kernel type
50 *
51 * Starts from a per-device-type base (GPU 256, CPU min(64, 2 x compute units),
52 * accelerator 128), caps it per kernel type, then by `problem_size` (when
53 * nonzero) and the device's maximum work-group size. Never returns 0.
54 *
55 * @param device The target device
56 * @param kernel_type Type of kernel operation
57 * @param problem_size Characteristic problem size (e.g., matrix dimension); 0 = no cap
58 * @param batch_size Number of matrices in batch; currently unused
59 * @param memory_per_problem Currently unused
60 * @return Work-group size
61 * @trap For REDUCTION and SCAN the "power of two" rounding is `1 << (31 - __builtin_clzl(x))`,
62 * which assumes a 32-bit `long`. On LP64 the shift count is negative (undefined
63 * behaviour), so the result is NOT guaranteed to be a power of two.
64 */
65inline size_t compute_optimal_wg_size(const Device& device, KernelType kernel_type,
66 size_t problem_size = 0, size_t batch_size = 1, size_t memory_per_problem = 0) {
67 const size_t max_wg_size = device.get_property(DeviceProperty::MAX_WORK_GROUP_SIZE);
68 const size_t max_compute_units = device.get_property(DeviceProperty::MAX_COMPUTE_UNITS);
69 const DeviceType dev_type = device.type;
70
71 size_t base_wg_size;
72 switch (dev_type) {
73 case DeviceType::GPU:
74 base_wg_size = 256; // Common choice for NVIDIA/AMD GPUs
75 break;
76 case DeviceType::CPU:
77 base_wg_size = std::min(size_t(64), max_compute_units * 2);
78 break;
80 base_wg_size = 128;
81 break;
82 default:
83 base_wg_size = 64;
84 break;
85 }
86
87 switch (kernel_type) {
89 if (dev_type == DeviceType::GPU) {
90 base_wg_size = std::min(base_wg_size, size_t(512));
91 } else {
92 base_wg_size = std::min(base_wg_size, size_t(128));
93 }
94 break;
95
97 if (dev_type == DeviceType::GPU) {
98 base_wg_size = std::min(base_wg_size, size_t(512));
99 } else {
100 base_wg_size = std::min(base_wg_size, size_t(64));
101 }
102 // "Power of 2" only where long is 32-bit: see the @trap above.
103 base_wg_size = size_t(1) << (31 - __builtin_clzl(base_wg_size));
104 break;
105
106 case KernelType::SCAN:
107 base_wg_size = std::min(base_wg_size, size_t(256));
108 base_wg_size = size_t(1) << (31 - __builtin_clzl(base_wg_size));
109 break;
110
112 base_wg_size = std::min(base_wg_size, size_t(128));
113 break;
114
116 if (dev_type == DeviceType::GPU) {
117 base_wg_size = std::min(base_wg_size, size_t(1024));
118 }
119 break;
120
122 base_wg_size = std::min(base_wg_size, size_t(128));
123 break;
124
125 case KernelType::GEMM:
126 base_wg_size = std::min(base_wg_size, size_t(256));
127 break;
128
130 base_wg_size = std::min({base_wg_size, problem_size, size_t(64)});
131 break;
132
134 base_wg_size = std::min(base_wg_size, size_t(32));
135 }
136
137 if (problem_size > 0) {
138 base_wg_size = std::min(base_wg_size, problem_size);
139 }
140
141 base_wg_size = std::min(base_wg_size, max_wg_size);
142
143 if (memory_per_problem > 0) {
144
145 }
146
147 return std::max(base_wg_size, size_t(1));
148}
149
150/**
151 * @brief Compute nd_range sizes for a batched operation
152 *
153 * When `total_work` exceeds INT32_MAX / 2 the result grid-strides with four
154 * work-groups per compute unit. Otherwise, with at least as many matrices as
155 * compute units, each matrix gets ceil(problem_size / local_size) work-groups;
156 * with fewer, the compute units are spread over the matrices, capped at what
157 * one matrix needs. The global size is never below `total_work` rounded up.
158 *
159 * @param total_work Total number of work items needed
160 * @param device Target device
161 * @param kernel_type Type of kernel operation
162 * @param batch_size Number of matrices in batch
163 * @param problem_size Elements (or work items) per matrix
164 * @param preferred_wg_size Optional preferred work-group size (0 = auto)
165 * @param footprint_per_problem Bytes per problem; currently has no effect (its estimate is overwritten)
166 * @param max_wg_size_for_kernel Currently unused
167 * @return BatchedNdRangeSizes {global_size, local_size, use_grid_stride}
168 */
170 const Device& device,
171 KernelType kernel_type,
172 size_t batch_size,
173 size_t problem_size,
174 size_t preferred_wg_size = 0,
175 size_t footprint_per_problem = 0,
176 size_t max_wg_size_for_kernel = 0
177 ) {
178 const size_t max_compute_units = device.get_property(DeviceProperty::MAX_COMPUTE_UNITS);
179
180 const size_t INT32_MAX_SAFE = static_cast<size_t>(std::numeric_limits<int32_t>::max()) / 2;
181 bool use_grid_stride = total_work > INT32_MAX_SAFE;
182 auto num_cus = device.get_property(DeviceProperty::MAX_COMPUTE_UNITS);
183 auto vendor = device.get_vendor();
184 auto shedulers_per_cu = 2; //Default to 2 schedulers per CU
185 if ((vendor == Vendor::NVIDIA || vendor == Vendor::AMD) && device.type == DeviceType::GPU) {
186 shedulers_per_cu = 4;
187 } else if (vendor == Vendor::INTEL && device.type == DeviceType::GPU) {
188 shedulers_per_cu = 8; //Intel GPUs have 8 or 16 "Vector Engines" per CU / Xe Core
189 }
190
191 auto L2_cache_size = device.get_property(DeviceProperty::GLOBAL_MEM_CACHE_SIZE); //L2 cache size
192
193
194
195 size_t local_size;
196 if (preferred_wg_size > 0) {
197 local_size = preferred_wg_size;
198 } else {
199 local_size = compute_optimal_wg_size(device, kernel_type, problem_size, batch_size);
200 }
201
202 size_t global_size;
203
204 if (footprint_per_problem > 0) {
205 size_t problems_in_L2 = L2_cache_size / footprint_per_problem;
206 global_size = std::min(batch_size, problems_in_L2) * local_size;
207 }
208
209 if (use_grid_stride) {
210 size_t target_workgroups = max_compute_units * 4; // 4x oversubscription
211 global_size = target_workgroups * local_size;
212
213 global_size = std::min(global_size, INT32_MAX_SAFE);
214 } else {
215 if (batch_size >= max_compute_units) {
216 size_t workgroups_per_matrix = 1;
217 size_t target_elements_per_workgroup = problem_size;
218
219 if (target_elements_per_workgroup > local_size) {
220 workgroups_per_matrix = (target_elements_per_workgroup + local_size - 1) / local_size;
221 }
222
223 global_size = batch_size * workgroups_per_matrix * local_size;
224 } else {
225 size_t workgroups_per_matrix = (max_compute_units + batch_size - 1) / batch_size;
226
227 size_t max_workgroups_per_matrix = (problem_size + local_size - 1) / local_size;
228 workgroups_per_matrix = std::min(workgroups_per_matrix, max_workgroups_per_matrix);
229
230 global_size = batch_size * workgroups_per_matrix * local_size;
231 }
232
233 size_t min_global_size = ((total_work + local_size - 1) / local_size) * local_size;
234 global_size = std::max(global_size, min_global_size);
235 }
236
237 return {.global_size = global_size,
238 .local_size = local_size,
239 .use_grid_stride = use_grid_stride};
240}
241
242/**
243 * @brief Compute batched matrix decomposition for work-group assignment
244 *
245 * With at least as many matrices as compute units, one work-group per matrix,
246 * and the kernel grid-strides over the rest of the matrix. Otherwise
247 * work_groups_per_matrix = ceil(CUs / batch_size), never more than
248 * ceil(elements_per_matrix / local_size). The global size is then capped to fit
249 * a signed 32-bit int, keeping at least one work-group per matrix; the kernel's
250 * grid-stride picks up the slack.
251 *
252 * @param batch_size Number of matrices in batch
253 * @param elements_per_matrix Number of elements per matrix
254 * @param device Target device
255 * @param kernel_type Type of kernel operation
256 * @param preferred_wg_size Optional preferred work-group size (0 = auto)
257 * @return BatchedMatrixDecomposition {global_size, local_size, work_groups_per_matrix}
258 */
260 size_t batch_size,
261 size_t elements_per_matrix,
262 const Device& device,
263 KernelType kernel_type,
264 size_t preferred_wg_size = 0)
265{
266 const size_t max_compute_units = device.get_property(DeviceProperty::MAX_COMPUTE_UNITS);
267
268 size_t local_size = preferred_wg_size > 0
269 ? preferred_wg_size
270 : compute_optimal_wg_size(device, kernel_type, elements_per_matrix, batch_size);
271
272 const size_t required_wgs_per_matrix = (elements_per_matrix + local_size - 1) / local_size;
273
274 size_t work_groups_per_matrix;
275 if (batch_size >= max_compute_units) {
276 work_groups_per_matrix = 1;
277 } else {
278 const size_t target_wgs_per_matrix = (max_compute_units + batch_size - 1) / batch_size; // ceil
279 work_groups_per_matrix = std::min(target_wgs_per_matrix, required_wgs_per_matrix);
280 work_groups_per_matrix = std::max(work_groups_per_matrix, size_t(1));
281 }
282
283 const size_t INT32_MAX_SAFE = static_cast<size_t>(std::numeric_limits<int32_t>::max()) - local_size;
284 size_t global_size = batch_size * work_groups_per_matrix * local_size;
285
286 if (global_size > INT32_MAX_SAFE) {
287 const size_t max_total_wgs = INT32_MAX_SAFE / local_size;
288 const size_t max_wgs_per_mat = std::max<size_t>(1, max_total_wgs / batch_size);
289 work_groups_per_matrix = std::min(work_groups_per_matrix, max_wgs_per_mat);
290 global_size = batch_size * work_groups_per_matrix * local_size;
291 }
292
293 return {.global_size = global_size,
294 .local_size = local_size,
295 .work_groups_per_matrix = work_groups_per_matrix};
296}
297
298/**
299 * @brief Compute optimal global and local work sizes for nd_range kernels with overflow protection
300 *
301 * @param total_work Total number of work items needed
302 * @param device Target device
303 * @param kernel_type Type of kernel operation
304 * @param preferred_wg_size Optional preferred work-group size (0 = auto)
305 * @return std::pair<size_t, size_t> {global_size, local_size}
306 */
307inline std::pair<size_t, size_t> compute_nd_range_sizes(size_t total_work,
308 const Device& device,
309 KernelType kernel_type,
310 size_t preferred_wg_size = 0) {
311 size_t local_size;
312 if (preferred_wg_size > 0) {
313 local_size = preferred_wg_size;
314 } else {
315 local_size = compute_optimal_wg_size(device, kernel_type, total_work);
316 }
317
318 const size_t max_safe_work_items = std::numeric_limits<int>::max() / 2;
319
320 size_t global_size;
321 if (total_work > max_safe_work_items) {
322 size_t max_work_groups = std::min(device.get_property(DeviceProperty::MAX_COMPUTE_UNITS) * 16, max_safe_work_items / local_size);
323 global_size = max_work_groups * local_size;
324 } else {
325 global_size = ((total_work + local_size - 1) / local_size) * local_size;
326 }
327
328 return {global_size, local_size};
329}
330
331/// @}
332
333} // namespace batchlas
DeviceType
The kind of SYCL device.
@ ACCELERATOR
An accelerator device.
@ MAX_WORK_GROUP_SIZE
Maximum work-group size.
@ MAX_COMPUTE_UNITS
Number of compute units.
@ GLOBAL_MEM_CACHE_SIZE
Global memory cache size (bytes).
BatchedMatrixDecomposition compute_batched_matrix_decomposition(size_t batch_size, size_t elements_per_matrix, const Device &device, KernelType kernel_type, size_t preferred_wg_size=0)
Compute batched matrix decomposition for work-group assignment.
size_t compute_optimal_wg_size(const Device &device, KernelType kernel_type, size_t problem_size=0, size_t batch_size=1, size_t memory_per_problem=0)
Compute a work-group size from the device type and kernel type.
std::pair< size_t, size_t > compute_nd_range_sizes(size_t total_work, const Device &device, KernelType kernel_type, size_t preferred_wg_size=0)
Compute optimal global and local work sizes for nd_range kernels with overflow protection.
KernelType
Kernel types for heuristic selection.
BatchedNdRangeSizes compute_batched_nd_range_sizes(size_t total_work, const Device &device, KernelType kernel_type, size_t batch_size, size_t problem_size, size_t preferred_wg_size=0, size_t footprint_per_problem=0, size_t max_wg_size_for_kernel=0)
Compute nd_range sizes for a batched operation.
@ SPARSE
Sparse matrix operations.
@ ELEMENTWISE
Element-wise operations (fill, copy, etc.).
@ GEMM
General matrix multiply.
@ TASK_BASED
Task-based parallelism, e.g. no fine-grained parallelism, each thread solves one problem.
@ REDUCTION
Reduction operations (sum, max, etc.).
@ MEMORY_BOUND
Memory bandwidth limited.
@ COMPUTE_BOUND
Compute intensive.
@ SMALL_MATRIX
Small matrix operations.
@ SCAN
Prefix scan operations.
Result of compute_batched_matrix_decomposition().
size_t global_size
batch * work_groups_per_matrix * local_size
size_t work_groups_per_matrix
work-groups assigned to each matrix
Result of compute_batched_nd_range_sizes().
size_t local_size
work-group size
size_t global_size
total work-items, a multiple of local_size
bool use_grid_stride
the kernel must grid-stride: global_size was capped below the total work
A handle to one SYCL device: its type and its index among devices of that type.
batchlas::Device, batchlas::Event and batchlas::Queue: where work runs and how it is ordered.