Neko 1.99.9
A portable framework for high-order spectral element flow simulations
Loading...
Searching...
No Matches
mfma_kernel.h
Go to the documentation of this file.
1#ifndef __MATH_MFMA_KERNEL_H__
2#define __MATH_MFMA_KERNEL_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
91#include <stdlib.h>
92#include <string.h>
93#include <hip/hip_runtime.h>
95#include <device/hip/check.h>
96
97/*
98 * Reports whether the device code really was compiled for a matrix core
99 * architecture, i.e. whether the __gfx90a__ / __gfx942__ guard below was true
100 * in the device pass.
101 *
102 * This is not the same question as "does the device have matrix cores". The
103 * contraction primitives and their call sites are all guarded on those
104 * macros, so a build whose offload arch does not include the running device's
105 * -- or a code object selected from a fat binary built for something else --
106 * turns the whole strategy into a silent no-op: the kernel launches, writes
107 * nothing, and leaves stale values in the output. That fails as bad results
108 * rather than as an error, which is the worst way for it to fail. Checking it
109 * from the device removes the guesswork.
110 */
111/* static, not merely file scope by convention: this header is included by
112 every operator that offers an MFMA strategy -- ax_helm, dudxyz, opgrad,
113 conv1 and cdtp -- and a non-template __global__ with external linkage is
114 then defined once per translation unit, which the linker rejects as a
115 multiple definition. Internal linkage gives each unit its own copy, which
116 is what the rest of this header already relies on. */
117static __global__ void hip_mfma_arch_probe(int * flag) {
118#if defined(__gfx90a__) || defined(__gfx942__)
119 *flag = 1;
120#else
121 *flag = 0;
122#endif
123}
124
133static inline bool hip_have_mfma() {
134 static int cached = -1;
135 if (cached < 0) {
136 int dev = 0;
138 cached = 0;
139 if (hipGetDevice(&dev) == hipSuccess &&
141 (strstr(prop.gcnArchName, "gfx90a") != NULL ||
142 strstr(prop.gcnArchName, "gfx942") != NULL)) {
143 int *d_flag = NULL;
144 int flag = 0;
145 if (hipMalloc(&d_flag, sizeof(int)) == hipSuccess) {
146 if (hipMemcpy(d_flag, &flag, sizeof(int),
149 d_flag);
150 if (hipGetLastError() == hipSuccess &&
151 hipMemcpy(&flag, d_flag, sizeof(int),
153 cached = flag;
154 }
155 }
156 /* Unlike the queries above, a failure here is not "the strategy is
157 unavailable" -- the pointer came from a hipMalloc that succeeded, so
158 a bad free means the context is broken. Checked rather than folded
159 into cached, and checked rather than discarded: hipFree is
160 nodiscard */
162 }
163 }
164 }
165 return cached == 1;
166}
167
174/*
175 * Both precisions are offered, and both are verified against a reference on
176 * gfx90a (mfma_probe, 144 configurations, 2026-08-22). They do not share a
177 * code path: f64 goes through the batched 4x4x4 tile, f32 has no 4x4x4
178 * instruction and uses the 16x16x4 one. If a future part disagrees, excluding
179 * a precision here is a one-line change.
180 */
181template < const int LX >
182static inline bool mfma_lx_supported() {
183 return (sizeof(real) == 8 || sizeof(real) == 4) && (LX >= 4) && (LX <= 12);
184}
185
186/*
187 * Wavefronts per block for the MFMA kernels. Candidate C selects 2^C
188 * wavefronts, i.e. 1, 2, 4 or 8.
189 *
190 * Two things are being traded. The contraction stripes its N column groups
191 * across wavefronts, and there are only NGROUPS = ceil(LX^2/16) of them --
192 * one at LX = 4, four at LX = 8 -- so past that count the extra wavefronts
193 * idle through the matrix core work. The staging and pointwise loops, on the
194 * other hand, keep scaling: at LX = 8, eight wavefronts is 512 threads for
195 * 512 points, one each, which is why the branch this came from defaulted to
196 * eight. The measured LX = 8 curve was still improving at four, hence the
197 * fourth candidate.
198 */
199#define NEKO_MFMA_CANDIDATES 4
200#define NEKO_MFMA_NWF(C) (1 << (C))
201#define NEKO_MFMA_NTHRDS(C) dim3(64, NEKO_MFMA_NWF(C), 1)
202
203/*
204 * Column groups per contraction -- the wavefront-parallel work one element
205 * offers. Both tiles group N the same way, 16 columns at a time, so this is
206 * ceil(LX^2/16) either way: 1 at LX = 4, 4 at LX = 8, 9 at LX = 12.
207 *
208 * A wavefront beyond that count has no matrix core work left on that element,
209 * which is what the LX = 4 single precision sweep measured: 20.5 / 23.6 /
210 * 34.0 / 60.2 us as NWF went 1, 2, 4, 8, monotonically worse, against
211 * 218 / 157 / 136.4 / 136.4 at LX = 8 where four groups exist. Rather than
212 * cap the sweep, the surplus wavefronts are given their own element: NWF is
213 * read as wavefronts per block, the block covers EB elements and WPE =
214 * NWF/EB wavefronts cooperate on each. At LX = 4 with eight wavefronts that
215 * is eight elements, one each, with nothing idle; at LX = 12 it is one
216 * element and eight cooperating wavefronts, exactly as before. This matters
217 * because p-multigrid smooths at LX = 4 and 2, so low order Ax is hot rather
218 * than incidental.
219 *
220 * EB is the driving quantity and WPE follows from it, not the other way
221 * round: the block is partitioned into EB equal groups, so EB has to divide
222 * NWF exactly or the leftover wavefronts address an element the block does
223 * not own -- past the end of the shared staging arrays, and past the end of
224 * global storage in the last block. EB = NWF/NGROUPS is therefore rounded
225 * down to a power of two, which divides NWF for every candidate since NWF is
226 * itself 2^C. WPE may then exceed NGROUPS -- at LX = 6, NGROUPS = 3 and four
227 * wavefronts give EB = 1, WPE = 4 -- which is harmless: mfma_contract_4x4()
228 * strides the groups with `ng = wf + gp * NWF` under `ng < NGROUPS`, so a
229 * wavefront without a group of its own simply issues no matrix core work,
230 * while still taking its share of the staging and pointwise passes. The
231 * alternative, capping WPE at NGROUPS and letting EB absorb the remainder,
232 * would grow the shared footprint (LX = 10 with eight wavefronts would want
233 * 66 kB) for no gain.
234 */
235#define NEKO_MFMA_NGROUPS(LX) (((LX) * (LX) + 15) / 16)
236/* Elements per block: surplus wavefronts, rounded down to a power of two so
237 that WPE * EB == NWF exactly */
238#define NEKO_MFMA_EB_N(NWF, LX) \
239 ((NWF) / NEKO_MFMA_NGROUPS(LX) >= 8 ? 8 : \
240 (NWF) / NEKO_MFMA_NGROUPS(LX) >= 4 ? 4 : \
241 (NWF) / NEKO_MFMA_NGROUPS(LX) >= 2 ? 2 : 1)
242#define NEKO_MFMA_EB(LX, C) NEKO_MFMA_EB_N(NEKO_MFMA_NWF(C), LX)
243/* Wavefronts cooperating on one element */
244#define NEKO_MFMA_WPE(LX, C) (NEKO_MFMA_NWF(C) / NEKO_MFMA_EB(LX, C))
245#define NEKO_MFMA_NBLCKS(NELV, LX, C) \
246 dim3(((NELV) + NEKO_MFMA_EB(LX, C) - 1) / NEKO_MFMA_EB(LX, C), 1, 1)
247
248/*
249 * Whether the autotuner sweeps the MFMA strategy, on by default wherever the
250 * hardware and the polynomial order allow it.
251 *
252 * It was briefly off while the matrix core contraction was known broken -- the
253 * lane layout had the block selector and the contraction index interchanged,
254 * which a "the solver converges" check had failed to catch for a long time.
255 * The layout was measured on gfx90a, corrected, and mfma_contract_4x4 now
256 * reproduces a CPU reference to ~1e-16 over every supported order, axis,
257 * transpose/accumulate mode and wavefront count, so there is no reason to
258 * withhold it from the sweep. Kept as an off switch in the shape of
259 * NEKO_EB_TUNE, for A/B work.
260 */
261static int neko_mfma_sweep()
262{
263 const char *v = getenv("NEKO_MFMA_TUNE");
264
265 if (v != NULL) {
266 return (atoi(v) != 0);
267 }
268 return 1;
269}
270
271/* Forced candidate, used when NEKO_AUTOTUNE pins the MFMA variant */
272static int neko_mfma_env()
273{
274 const char *v = getenv("NEKO_MFMA_NWF");
275 int c = (v != NULL) ? atoi(v) : 0;
276
278 c = 0;
279 }
280 return c;
281}
282
283/* Report every measured MFMA candidate, see NEKO_TUNE_LOG in
284 elem_block_tune.h */
285#define NEKO_TUNE_LOG_MFMA(LX, T3) \
286 do { \
287 for (int c = 0; c < NEKO_MFMA_CANDIDATES; c++) { \
288 if ((T3)[c] >= NEKO_TUNE_INIT) { continue; } \
289 sprintf(neko_log_buf, "MFMA %dwf %-2de: %9.2f us/call", \
290 NEKO_MFMA_NWF(c), NEKO_MFMA_EB(LX, c), \
291 NEKO_TUNE_US((T3)[c], iters)); \
292 log_message(neko_log_buf); \
293 } \
294 } while (0)
295
296#if defined(__gfx90a__) || defined(__gfx942__)
297
298/* 4-wide accumulators for the gfx90a / gfx942 matrix cores. */
299typedef double mfma_f64x4 __attribute__((ext_vector_type(4)));
300typedef float mfma_f32x4 __attribute__((ext_vector_type(4)));
301
302/*
303 * Per-precision matrix-core traits: the 4-wide accumulator type, the MFMA
304 * builtin, and the accumulator-slot -> output-row mapping (see the layout
305 * note above; f64 spreads rows with stride 4, f32 packs four contiguous rows).
306 */
307template< typename T >
308struct mfma_traits;
309
310template< >
311struct mfma_traits< double > {
312 typedef mfma_f64x4 acc_t;
313 __device__ __forceinline__ static acc_t mma(double a, double b, acc_t c) {
314 return __builtin_amdgcn_mfma_f64_16x16x4f64(a, b, c, 0, 0, 0);
315 }
316 __device__ __forceinline__ static int out_row(const int g, const int r) {
317 return g + 4 * r;
318 }
319};
320
321template< >
322struct mfma_traits< float > {
323 typedef mfma_f32x4 acc_t;
324 __device__ __forceinline__ static acc_t mma(float a, float b, acc_t c) {
325 return __builtin_amdgcn_mfma_f32_16x16x4f32(a, b, c, 0, 0, 0);
326 }
327 __device__ __forceinline__ static int out_row(const int g, const int r) {
328 return 4 * g + r;
329 }
330};
331
332/*
333 * Linearised index into an LX^3 cube stored as i + LX*j + LX*LX*k, where the
334 * coordinate 'p' lies on contraction axis AXIS and 'n' enumerates the two
335 * remaining axes as n = a + LX*b.
336 */
337template< const int LX, const int AXIS >
338__device__ __forceinline__ int mfma_cube_idx(const int p, const int n) {
339 const int a = n % LX;
340 const int b = n / LX;
341 if (AXIS == 0) return p + LX * a + LX * LX * b; // contract i; n = (j,k)
342 if (AXIS == 1) return a + LX * p + LX * LX * b; // contract j; n = (i,k)
343 return a + LX * b + LX * LX * p; // contract k; n = (i,j)
344}
345
346/*
347 * One wavefront contracts the reference derivative matrix 'dmat' (LX x LX,
348 * stored column-major: D(row,col) = dmat[row + LX*col]) with the cube 'in'
349 * along axis AXIS, writing the cube 'out':
350 *
351 * out[idx(m,n)] (+)= sum_l D(m,l) * in[idx(l,n)] (TRANSPOSE = false)
352 * out[idx(m,n)] (+)= sum_l D(l,m) * in[idx(l,n)] (TRANSPOSE = true)
353 *
354 * GEMM dimensions M = LX, N = LX*LX, K = LX, tiled over 16x16x4 MFMA tiles
355 * with the partial M/N/K tiles masked. ACCUM selects += over = .
356 */
357/*
358 * NWF cooperating wavefronts (wf = 0..NWF-1) stripe the NTILES N-tiles among
359 * themselves -- wavefront wf handles nt = wf, wf+NWF, ... NWF = 1, wf = 0
360 * (the defaults) reproduces the single-wavefront contraction.
361 */
362template< typename T, const int LX, const int AXIS,
363 const bool TRANSPOSE, const bool ACCUM, const int NWF = 1 >
366 const T * __restrict__ dmat,
367 const T * __restrict__ in,
368 const int lane, const int wf = 0) {
369 typedef mfma_traits<T> mma_t;
370 const int g = lane >> 4; // lane / 16 -> 0..3
371 const int c = lane & 15; // lane % 16 -> 0..15
372 const int NTILES = (LX * LX + 15) / 16;
373 const int KSTEPS = (LX + 3) / 4;
374 const int NPASS = (NTILES + NWF - 1) / NWF; // N-tiles handled by this wave
375
376#pragma unroll
377 for (int p = 0; p < NPASS; ++p) {
378 const int nt = wf + p * NWF; // this wavefront's N-tile
379 if (nt < NTILES) {
380 const int n = nt * 16 + c; // free (column) index
381 typename mma_t::acc_t acc = {0, 0, 0, 0};
382#pragma unroll
383 for (int ks = 0; ks < KSTEPS; ++ks) {
384 const int l = ks * 4 + g; // contraction index
385 T a = 0;
386 if (c < LX && l < LX)
387 a = TRANSPOSE ? dmat[l + c * LX] // D(l,c) = D^T(c,l)
388 : dmat[c + l * LX]; // D(c,l)
389 T b = 0;
390 if (l < LX && n < LX * LX)
391 b = in[mfma_cube_idx<LX, AXIS>(l, n)];
392 acc = mma_t::mma(a, b, acc);
393 }
394 const T dvals[4] = { acc[0], acc[1], acc[2], acc[3] };
395#pragma unroll
396 for (int r = 0; r < 4; ++r) {
397 const int m = mma_t::out_row(g, r); // output coordinate on AXIS
398 if (m < LX && n < LX * LX) {
399 const int idx = mfma_cube_idx<LX, AXIS>(m, n);
400 if (ACCUM) out[idx] += dvals[r];
401 else out[idx] = dvals[r];
402 }
403 }
404 }
405 }
406}
407
408/*
409 * Double-precision batched matrix-core contraction using v_mfma_f64_4x4x4f64.
410 *
411 * Same contract as mfma_contract() -- out(m,n) (+)= sum_l D(m,l) in(l,n), with
412 * D(l,m) when TRANSPOSE -- but tiled with 4x4x4 MFMA tiles and the
413 * instruction's four blocks assigned to four consecutive 4-column N-subtiles.
414 * GEMM M = LX, N = LX*LX, K = LX, tiled as MT = ceil(LX/4) M-tiles,
415 * KSTEPS = ceil(LX/4) K-steps and NGROUPS = ceil(LX^2/16) column groups (each
416 * group = 4 blocks x 4 columns); partial M/N/K masked with zeros.
417 *
418 * Versus the 16x16x4 tile, the 4-wide M granularity fills M = LX < 16 exactly
419 * (LX = 8 runs the matrix core at 100% M-utilisation instead of 50%), at the
420 * cost of 4x as many, 1/4-sized MFMA issues that feed the same f64 matrix
421 * pipeline. Double precision only: the f32 4x4 instruction has K = 1, so there
422 * is no single-precision counterpart -- single precision keeps the 16x16x4 tile
423 * (see mfma_contract_sel below).
424 *
425 * Wave of 64 lanes = 4 blocks x 16 lanes. v_mfma_f64_4x4x4f64 lane layout,
426 * with lo = lane%4, gemm = (lane/4)%4 and kq = lane/16:
427 * A[i][k] : lane holds A[i = lo ][k = kq]
428 * B[k][j] : lane holds B[k = kq ][j = lo]
429 * D[i][j] : lane holds D[i = kq ][j = lo] (scalar f64 accumulator)
430 * and the four independent 4x4x4 blocks are selected by 'gemm', not by
431 * lane/16.
432 *
433 * This was MEASURED on gfx90a, not assumed: a one-hot read-out of which lane
434 * receives which element (128 launches, no assumptions) produced exactly this
435 * mapping. The previous version had 'gemm' and 'kq' interchanged -- it used
436 * lane/16 as the block selector and (lane/4)%4 as the contraction index -- and
437 * every one of 72 probe configurations disagreed with a CPU reference.
438 *
439 * It had been believed validated because the Ax-helm fluid solver converged
440 * with it. It does: a wrong but symmetric operator makes CG converge happily
441 * to the solution of a different system. Convergence is not correctness, and
442 * only a diff against a reference settles a layout. Any future correction
443 * stays localised to the four index expressions (m_a, l, n, m_d).
444 *
445 * NWF cooperating wavefronts (wf = 0..NWF-1) stripe the NGROUPS column groups
446 * among themselves -- wavefront wf handles ng = wf, wf+NWF, ... NWF = 1,
447 * wf = 0 (the defaults) reproduces the single-wavefront contraction.
448 */
449template< const int LX, const int AXIS,
450 const bool TRANSPOSE, const bool ACCUM, const int NWF = 1 >
453 const double * __restrict__ dmat,
454 const double * __restrict__ in,
455 const int lane, const int wf = 0) {
456 const int lo = lane & 3; // 0..3 : A row, B column, D column
457 const int gemm = (lane >> 2) & 3; // 0..3 : which of the four 4x4x4 blocks
458 const int kq = lane >> 4; // 0..3 : contraction index within a step
459 const int MT = (LX + 3) / 4;
460 const int KSTEPS = (LX + 3) / 4;
461 const int NGROUPS = (LX * LX + 15) / 16;
462 const int NPASS = (NGROUPS + NWF - 1) / NWF; // groups handled by this wave
463
464#pragma unroll
465 for (int mt = 0; mt < MT; ++mt) {
466 const int m_a = mt * 4 + lo; // A row (input lane layout: i = lo)
467 const int m_d = mt * 4 + kq; // D row (output lane layout: i = kq)
468#pragma unroll
469 for (int gp = 0; gp < NPASS; ++gp) {
470 const int ng = wf + gp * NWF; // this wavefront's column group
471 if (ng < NGROUPS) {
472 /* the four blocks take four consecutive 4-column N-subtiles */
473 const int n = ng * 16 + gemm * 4 + lo; // column (N) for B in / D out
474 double acc = 0.0;
475#pragma unroll
476 for (int ks = 0; ks < KSTEPS; ++ks) {
477 const int l = ks * 4 + kq; // contraction index (k = kq for A and B)
478 double a = 0.0;
479 if (m_a < LX && l < LX)
480 a = TRANSPOSE ? dmat[l + m_a * LX] // D(l,m) = D^T(m,l)
481 : dmat[m_a + l * LX]; // D(m,l)
482 double b = 0.0;
483 if (l < LX && n < LX * LX)
484 b = in[mfma_cube_idx<LX, AXIS>(l, n)];
486 }
487 if (m_d < LX && n < LX * LX) {
488 const int idx = mfma_cube_idx<LX, AXIS>(m_d, n);
489 if (ACCUM) out[idx] += acc;
490 else out[idx] = acc;
491 }
492 }
493 }
494 }
495}
496
497/*
498 * Precision-dispatched tensor contraction: double precision uses the batched
499 * 4x4x4 matrix core (full M-utilisation for M = LX < 16), single precision the
500 * 16x16x4 tile (no f32 4x4x4 equivalent). Lets one call site cover both.
501 */
502template< typename T, const int LX, const int AXIS,
503 const bool TRANSPOSE, const bool ACCUM, const int NWF = 1 >
504struct mfma_contract_sel {
506 static void run(T * __restrict__ out, const T * __restrict__ dmat,
507 const T * __restrict__ in, const int lane,
508 const int wf = 0) {
510 }
511};
512
513template< const int LX, const int AXIS, const bool TRANSPOSE, const bool ACCUM,
514 const int NWF >
517 static void run(double * __restrict__ out, const double * __restrict__ dmat,
518 const double * __restrict__ in, const int lane,
519 const int wf = 0) {
520#ifdef MFMA_F64_USE_16X16
521 /*
522 * Opt-out fallback: the original 16x16x4 tile (M-utilisation 50% at LX=8,
523 * 75% at LX=12). Kept as an escape hatch behind -DMFMA_F64_USE_16X16; the
524 * batched 4x4x4 path below is the hardware-validated default.
525 */
527 lane, wf);
528#else
529 /*
530 * Default f64 path: batched 4x4x4 matrix-core tile, full M-utilisation.
531 * Hardware-validated -- the ax_helm fluid solver converges on gfx90a/
532 * gfx942. Still multi-wavefront via NWF.
533 */
535#endif
536 }
537};
538
539#endif // __gfx90a__ || __gfx942__
540#endif // __MATH_MFMA_KERNEL_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)
__global__ void T *__restrict__ T *__restrict__ const T *__restrict__ const T *__restrict__ v
double real
#define HIP_CHECK(err)
Definition check.h:8
static __global__ void hip_mfma_arch_probe(int *flag)
static bool mfma_lx_supported()
#define NEKO_MFMA_CANDIDATES
static bool hip_have_mfma()
static int neko_mfma_env()
static int neko_mfma_sweep()