Neko 1.99.9
A portable framework for high-order spectral element flow simulations
Loading...
Searching...
No Matches
elem_block_tune.h
Go to the documentation of this file.
1#ifndef __MATH_ELEM_BLOCK_TUNE_H__
2#define __MATH_ELEM_BLOCK_TUNE_H__
3/*
4 Copyright (c) 2026, The Neko Authors
5 All rights reserved.
6
7 Redistribution and use in source and binary forms, with or without
8 modification, are permitted provided that the following conditions
9 are met:
10
11 * Redistributions of source code must retain the above copyright
12 notice, this list of conditions and the following disclaimer.
13
14 * Redistributions in binary form must reproduce the above
15 copyright notice, this list of conditions and the following
16 disclaimer in the documentation and/or other materials provided
17 with the distribution.
18
19 * Neither the name of the authors nor the names of its
20 contributors may be used to endorse or promote products derived
21 from this software without specific prior written permission.
22
23 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26 FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
28 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
33 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 POSSIBILITY OF SUCH DAMAGE.
35*/
36
59#include <stdlib.h>
60#include <stdio.h>
61#include <math.h>
62#include <hip/hip_runtime.h>
64#include <device/hip/check.h>
65
66/* Sweep the elements per block candidates?
67 On by default, as on CUDA. It was off here because the blocked variants
68 measured a loss on MI250X and MI300A, where they spill -- but baking that
69 in also stopped the tuner from ever re-testing it, and the kstep family has
70 since measured badly enough on gfx90a (788 us/call at lx = 8 against 268
71 for the 1d variant) that its geometry is worth measuring per case rather
72 than assuming. Where the blocked variants still spill the tuner simply
73 rejects them; the cost is tuning time, not run time.
74
75 Note NEKO_TUNE_ROUNDS and NEKO_TUNE_ITERS below are not specific to
76 this sweep: they control the sampling of every candidate the tuner
77 times, chunk sizes included. */
78#ifndef NEKO_EB_SWEEP_DEFAULT
79#define NEKO_EB_SWEEP_DEFAULT 1
80#endif
81
82#define NEKO_TUNE_INIT 1.0e30f
83#define NEKO_TUNE_WARMUP 20
84
85static int neko_eb_sweep()
86{
87 const char *v = getenv("NEKO_EB_TUNE");
88
89 if (v != NULL) {
90 return (atoi(v) != 0);
91 }
93}
94
95/*
96 * Elements per block candidate pinned by NEKO_EB, or -1 to leave it to the
97 * sweep.
98 *
99 * NEKO_AUTOTUNE selects the formulation and nothing more -- the geometry
100 * inside it is still measured unless it is pinned here -- so "unset" has to
101 * be distinguishable from candidate 0, which is a candidate like any other.
102 * Hence the -1 rather than a plain default of 0. Out of range values clamp
103 * to 0 rather than releasing the pin, as they always have.
104 */
105static int neko_eb_pin()
106{
107 const char *v = getenv("NEKO_EB");
108 int c;
109
110 if (v == NULL) {
111 return -1;
112 }
113
114 c = atoi(v);
116 c = 0;
117 }
118 return c;
119}
120
121/* Chunk candidate pinned by NEKO_CHUNKS, or -1 to sweep, see neko_eb_pin() */
122static int neko_chunks_pin()
123{
124 const char *v = getenv("NEKO_CHUNKS");
125 int c;
126
127 if (v == NULL) {
128 return -1;
129 }
130
131 c = atoi(v);
133 c = 0;
134 }
135 return c;
136}
137
139{
140 const char *v = getenv("NEKO_TUNE_ROUNDS");
141 int n = (v != NULL) ? atoi(v) : 3;
142
143 return (n < 1) ? 1 : n;
144}
145
146static int neko_tune_iters()
147{
148 const char *v = getenv("NEKO_TUNE_ITERS");
149 int n = (v != NULL) ? atoi(v) : 100;
150
151 return (n < 1) ? 1 : n;
152}
153
154/*
155 * Loop over the candidates of one formulation, binding C to each in turn.
156 *
157 * ON gates the formulation itself: hardware support, and -- when NEKO_AUTOTUNE
158 * is set -- whether it is the formulation that was asked for. PIN is the
159 * candidate that formulation's own variable forces, or -1 to measure all N of
160 * them. So NEKO_AUTOTUNE narrows the search to one kernel family without
161 * deciding the geometry within it, which is still swept and reported.
162 *
163 * An out of play formulation yields an empty range, leaving its candidates at
164 * NEKO_TUNE_INIT so it loses the comparison at the end rather than having to
165 * be excluded from it.
166 */
167#define NEKO_TUNE_FOR(C, ON, PIN, N) \
168 for (int C = (((PIN) >= 0) ? (PIN) : 0), \
169 C##_last_ = ((ON) ? (((PIN) >= 0) ? (PIN) + 1 : (N)) : 0); \
170 C < C##_last_; C++)
171
172/* One timed round of LAUNCH at candidate C, min reduced into T[C] */
173#define NEKO_TUNE_TIME(T, LAUNCH, LX, C, ITERS) \
174 do { \
175 float t_; \
176 HIP_CHECK(hipEventRecord(start, stream)); \
177 for (int i_ = 0; i_ < (ITERS); i_++) { LAUNCH(LX, C); } \
178 HIP_CHECK(hipEventRecord(stop, stream)); \
179 HIP_CHECK(hipEventSynchronize(stop)); \
180 HIP_CHECK(hipEventElapsedTime(&t_, start, stop)); \
181 if (t_ < (T)[C]) { (T)[C] = t_; } \
182 } while (0)
183
184/*
185 * Elapsed time (ms, over ITERS launches) as microseconds per call.
186 *
187 * A division rather than a constant because ITERS is NEKO_TUNE_ITERS, which
188 * is settable: the factor of 10 this used to carry is only correct at the
189 * default of 100 and silently rescales every reported time otherwise. The
190 * ranking never moves, every candidate sharing the divisor, but the number
191 * the log prints does.
192 */
193#define NEKO_TUNE_US(T, ITERS) ((T) * 1000.0 / (double) (ITERS))
194
195#define NEKO_TUNE_BEST(T, BEST, N) \
196 do { \
197 for (int c = 1; c < (N); c++) { \
198 if ((T)[c] < (T)[BEST]) { BEST = c; } \
199 } \
200 } while (0)
201
202/* Report every measured candidate of one sweep, not just the winner. Split
203 per formulation because the vector operator has no 1d variant and so wants
204 the kstep half on its own */
205#define NEKO_TUNE_LOG_1D(LX, T1) \
206 do { \
207 for (int c = 0; c < NEKO_CHUNKS_CANDIDATES; c++) { \
208 if ((T1)[c] >= NEKO_TUNE_INIT) { continue; } \
209 sprintf(neko_log_buf, "1D ch=%-4d: %9.2f us/call", \
210 NEKO_CHUNKS_SEL(LX, c), NEKO_TUNE_US((T1)[c], iters)); \
211 log_message(neko_log_buf); \
212 } \
213 } while (0)
214
215#define NEKO_TUNE_LOG_KSTEP(LX, T2) \
216 do { \
217 for (int c = 0; c < NEKO_EB_CANDIDATES; c++) { \
218 if ((T2)[c] >= NEKO_TUNE_INIT) { continue; } \
219 sprintf(neko_log_buf, "KSTEP eb=%-4d: %9.2f us/call", \
220 NEKO_EB_SEL(LX, c), NEKO_TUNE_US((T2)[c], iters)); \
221 log_message(neko_log_buf); \
222 } \
223 } while (0)
224
225/* Both sweeps, for the operators that have a 1d variant */
226#define NEKO_TUNE_LOG(LX, T1, T2) \
227 do { \
228 NEKO_TUNE_LOG_1D(LX, T1); \
229 NEKO_TUNE_LOG_KSTEP(LX, T2); \
230 } while (0)
231
232
233/*
234 * TUNE-TIME VERIFICATION
235 *
236 * Every candidate the sweep is about to time is first run once on a synthetic
237 * input and compared with the reference formulation's result on the same
238 * input: the 1d kernel's first candidate where there is one, the kstep
239 * kernel's where there is not. A candidate whose max-norm difference exceeds
240 * NEKO_TUNE_VERIFY_TOL times the reference's max-norm is reported as REJECTED
241 * and excluded from the comparison, so it can be measured but cannot win.
242 *
243 * Why it exists: the matrix core contraction once ran for a long time with
244 * its lane layout wrong -- a wrong but symmetric operator makes CG converge
245 * happily to the solution of a different system (see mfma_contract_4x4 in
246 * mfma_kernel.h), so "the solver converges" never caught it. The single
247 * precision 4x4x1 tile added since carries a layout that is documented by
248 * AMD's calculator but has not been read out on hardware, and this is what
249 * keeps it honest: a wrong register index costs that candidate at tuning,
250 * loudly, not the solve.
251 *
252 * Why a synthetic input: the tuner runs on the first call of an operator,
253 * which can be on a zero field (a zero initial guess, a zero velocity), and
254 * every layout reproduces zero. The fields the operator differentiates are
255 * swapped for a deterministic non-zero pattern in (0.5, 1.5) for the duration
256 * of the pass -- the tune function hands over the addresses of its input
257 * pointer variables, which its launch macros name, so the swap is invisible
258 * to them -- while geometry, metrics and weights stay the case's own. The
259 * reference max-norm is logged with every rejection, so an inconclusive pass
260 * is visible as such.
261 *
262 * The tolerance is a layout detector, not an accuracy test: the variants sum
263 * the same terms in a different order, a few ulp per point, where a layout
264 * error reads as O(1). NaN or Inf anywhere in a candidate's output counts as
265 * an infinite difference, so it cannot slip through a comparison that is
266 * false for NaN.
267 *
268 * Cost: one launch per candidate plus a reduction over the output, once per
269 * (operator, order), and nin + nout arrays of n reals for the duration of the
270 * pass; a failed allocation skips the pass and says so in the log, it never
271 * stops the tune. NEKO_TUNE_VERIFY=0 turns it off, NEKO_TUNE_VERIFY_TOL
272 * overrides the tolerance.
273 */
274#ifndef NEKO_TUNE_VERIFY_DEFAULT
275#define NEKO_TUNE_VERIFY_DEFAULT 1
276#endif
277#define NEKO_TUNE_VERIFY_BLOCKS 256
278#define NEKO_TUNE_VERIFY_THREADS 256
279#define NEKO_TUNE_VERIFY_MAXIN 8
280/* Upper bound on any formulation's candidate count, for the reject flags */
281#define NEKO_TUNE_MAXCAND 16
282
284{
285 const char *v = getenv("NEKO_TUNE_VERIFY");
286
287 if (v != NULL) {
288 return (atoi(v) != 0);
289 }
291}
292
293static double neko_tune_verify_tol()
294{
295 const char *v = getenv("NEKO_TUNE_VERIFY_TOL");
296
297 if (v != NULL && atof(v) > 0.0) {
298 return atof(v);
299 }
300 return (sizeof(real) == 4) ? 1.0e-4 : 1.0e-10;
301}
302
303/* Deterministic non-zero pattern in (0.5, 1.5), different per input slot */
305 const int n,
306 const unsigned int seed) {
307 for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n;
308 i += gridDim.x * blockDim.x) {
309 unsigned int h = (unsigned int) i * 2654435761u + seed * 40503u;
310 h ^= h >> 15;
311 h *= 2246822519u;
312 h ^= h >> 13;
313 x[i] = (real) 0.5 + (real) (h & 0xffffffu) / (real) 0x1000000u;
314 }
315}
316
317/* Per block maxima of |a - b| and |b|, NaN or Inf in a counted as infinite */
319 const real * __restrict__ b,
320 const int n,
321 double * __restrict__ part) {
324 double md = 0.0, mr = 0.0;
325
326 for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n;
327 i += gridDim.x * blockDim.x) {
328 const double x = (double) a[i];
329 const double y = (double) b[i];
330 const double d = fabs(x - y);
331 const double r = fabs(y);
332 if (!(d < HUGE_VAL)) {
333 md = HUGE_VAL;
334 } else if (d > md) {
335 md = d;
336 }
337 if (r > mr) {
338 mr = r;
339 }
340 }
341 sd[threadIdx.x] = md;
342 sr[threadIdx.x] = mr;
344 for (int s = blockDim.x / 2; s > 0; s >>= 1) {
345 if (threadIdx.x < s) {
346 if (sd[threadIdx.x + s] > sd[threadIdx.x]) {
347 sd[threadIdx.x] = sd[threadIdx.x + s];
348 }
349 if (sr[threadIdx.x + s] > sr[threadIdx.x]) {
350 sr[threadIdx.x] = sr[threadIdx.x + s];
351 }
352 }
354 }
355 if (threadIdx.x == 0) {
356 part[2 * blockIdx.x] = sd[0];
357 part[2 * blockIdx.x + 1] = sr[0];
358 }
359}
360
361typedef struct {
362 real *buf; /* nin synthetic inputs, nout references */
363 double *part; /* per block maxima */
364 real **outs; /* the tune function's output arrays */
365 void ***ins; /* addresses of its input pointers */
367 int nin, nout, n;
368 double tol;
370
371/* Reject flags of one formulation's candidates, see NEKO_TUNE_VERIFY_CAND */
372typedef struct {
375
376/*
377 * Set the pass up: allocate, fill the synthetic inputs and point the tune
378 * function's input pointer variables at them. Returns 0, with nothing
379 * swapped, if the pass is off or cannot be set up.
380 */
382 const int nout, void ***ins, const int nin,
383 const int n, hipStream_t stream)
384{
385 const size_t bytes = (size_t) (nin + nout) * (size_t) n * sizeof(real);
386
388 return 0;
389 }
390 v->outs = outs;
391 v->ins = ins;
392 v->nin = nin;
393 v->nout = nout;
394 v->n = n;
395 v->tol = neko_tune_verify_tol();
396 v->buf = NULL;
397 v->part = NULL;
398 if (hipMalloc((void **) &v->buf, bytes) != hipSuccess) {
399 /* Clear the sticky error: the tuner checks hipGetLastError() after
400 every launch and would otherwise report this one there */
402 return 0;
403 }
404 if (hipMalloc((void **) &v->part,
405 2 * NEKO_TUNE_VERIFY_BLOCKS * sizeof(double)) != hipSuccess) {
407 HIP_CHECK(hipFree(v->buf));
408 return 0;
409 }
410 for (int i = 0; i < nin; i++) {
411 real *x = v->buf + (size_t) i * n;
413 dim3(NEKO_TUNE_VERIFY_THREADS), 0, stream,
414 x, n, (unsigned int) (i + 1));
416 v->saved[i] = *ins[i];
417 *ins[i] = (void *) x;
418 }
419 return 1;
420}
421
422/* Snapshot the outputs of the reference launch the caller has just made */
424{
425 for (int j = 0; j < v->nout; j++) {
426 /* hipMemcpyDefault rather than DeviceToDevice: the runtime then
427 resolves the kind from the pointers, which keeps this correct if
428 the operator's arrays are ever host-mapped on an APU */
429 HIP_CHECK(hipMemcpyAsync(v->buf + (size_t) (v->nin + j) * v->n,
430 v->outs[j], (size_t) v->n * sizeof(real),
431 hipMemcpyDefault, stream));
432 }
434}
435
436/*
437 * Compare the outputs of the launch the caller has just made with the
438 * snapshot. Returns 1 if every output is within tolerance, and leaves the
439 * worst max-norm difference and the reference max-norm behind for the log
440 * either way. Written so that an infinite difference fails.
441 */
443 double *maxdiff, double *maxref)
444{
445 double part[2 * NEKO_TUNE_VERIFY_BLOCKS];
446
447 *maxdiff = 0.0;
448 *maxref = 0.0;
449 for (int j = 0; j < v->nout; j++) {
450 const real *ref = v->buf + (size_t) (v->nin + j) * v->n;
452 dim3(NEKO_TUNE_VERIFY_THREADS), 0, stream,
453 (const real *) v->outs[j], ref, v->n, v->part);
455 HIP_CHECK(hipMemcpyAsync(part, v->part, sizeof(part),
456 hipMemcpyDeviceToHost, stream));
458 for (int b = 0; b < NEKO_TUNE_VERIFY_BLOCKS; b++) {
459 if (part[2 * b] > *maxdiff) {
460 *maxdiff = part[2 * b];
461 }
462 if (part[2 * b + 1] > *maxref) {
463 *maxref = part[2 * b + 1];
464 }
465 }
466 }
467 return (*maxdiff <= v->tol * (*maxref));
468}
469
470/* Restore the input pointers and release the buffers */
472{
474 for (int i = 0; i < v->nin; i++) {
475 *v->ins[i] = v->saved[i];
476 }
477 HIP_CHECK(hipFree(v->buf));
478 HIP_CHECK(hipFree(v->part));
479}
480
481/* Launch candidate C of LAUNCH, compare it with the reference and record the
482 verdict in REJ; a rejection is logged with the two norms behind it. Expects
483 `stream` and `neko_log_buf` in scope like the reporting macros above */
484#define NEKO_TUNE_VERIFY_CAND(VF, REJ, LAUNCH, LX, C, NAME) \
485 do { \
486 double md_, mr_; \
487 LAUNCH(LX, C); \
488 (REJ).rej[C] = !neko_tune_verify_check(&(VF), stream, &md_, &mr_); \
489 if ((REJ).rej[C]) { \
490 sprintf(neko_log_buf, \
491 "%-5s cand %2d REJECTED: |diff| %.1e, |ref| %.1e", \
492 NAME, C, md_, mr_); \
493 log_message(neko_log_buf); \
494 } \
495 } while (0)
496
497/* Take the rejected candidates of one formulation out of the comparison */
498#define NEKO_TUNE_APPLY_REJ(T, REJ, N) \
499 do { \
500 for (int c = 0; c < (N); c++) { \
501 if ((REJ).rej[c]) { (T)[c] = NEKO_TUNE_INIT; } \
502 } \
503 } while (0)
504
505#endif // __MATH_ELEM_BLOCK_TUNE_H__
__global__ void ale_add_kinematics_kernel(const int n, T *__restrict__ wx, T *__restrict__ wy, T *__restrict__ wz, const T *__restrict__ x_ref, const T *__restrict__ y_ref, const T *__restrict__ z_ref, const T *__restrict__ phi, const T *__restrict__ x, const T *__restrict__ y, const T *__restrict__ z, const kinematics_params_t kin_params)
const int i
__global__ void T *__restrict__ T *__restrict__ const T *__restrict__ const T *__restrict__ v
const int j
__syncthreads()
__global__ void const T *__restrict__ x
#define NEKO_CHUNKS_CANDIDATES
Definition elem_block.h:125
#define NEKO_EB_CANDIDATES
Definition elem_block.h:63
#define NEKO_EB_SWEEP_DEFAULT
static int neko_chunks_pin()
static int neko_tune_rounds()
static int neko_eb_pin()
static int neko_tune_iters()
static int neko_eb_sweep()
double real
#define HIP_CHECK(err)
Definition check.h:8
#define NEKO_TUNE_VERIFY_THREADS
static void neko_tune_verify_ref(neko_tune_verify_t *v, hipStream_t stream)
static double neko_tune_verify_tol()
#define NEKO_TUNE_MAXCAND
static int neko_tune_verify_check(neko_tune_verify_t *v, hipStream_t stream, double *maxdiff, double *maxref)
static void neko_tune_verify_end(neko_tune_verify_t *v, hipStream_t stream)
static int neko_tune_verify_on()
#define NEKO_TUNE_VERIFY_MAXIN
static __global__ void neko_tune_fill_kernel(real *__restrict__ x, const int n, const unsigned int seed)
#define NEKO_TUNE_VERIFY_DEFAULT
#define NEKO_TUNE_VERIFY_BLOCKS
static int neko_tune_verify_begin(neko_tune_verify_t *v, real **outs, const int nout, void ***ins, const int nin, const int n, hipStream_t stream)
static __global__ void neko_tune_diff_kernel(const real *__restrict__ a, const real *__restrict__ b, const int n, double *__restrict__ part)