NumericAL
A header-only C++20 lexer built on REAL
Loading...
Searching...
No Matches
gemm.hpp
Go to the documentation of this file.
1
18#ifndef NUMERICAL_GEMM_HPP
19#define NUMERICAL_GEMM_HPP
20
21#include <algorithm>
22#include <array>
23#include <bit>
24#include <cmath>
25#include <atomic>
26#include <complex>
27#include <cstddef>
28#include <cstdint>
29#include <cstring>
30#include <memory>
31#include <type_traits>
32#include <utility>
33#include <vector>
34
35#if defined(__GNUC__) && defined(__aarch64__)
36# include <arm_neon.h>
37# define NUMERICAL_NEON_COMPILED // the arm64 fused tile is compiled in (tested with #ifdef only)
38#endif
39
40#include "parallel.hpp"
41#include "amx.hpp"
42#include "buffer.hpp"
43#include "simd.hpp"
44#include "x86.hpp"
45
46// A function kept out of its callers: inlined, it spent GCC's growth budget for the whole translation unit, and the
47// products, the Cholesky factorisation and the LU solve lost their own inlining (one arm64 thread, GCC 14: a product of
48// order 8 0.29 → 0.33 µs, a Cholesky factorisation of order 17 0.77 → 0.87 µs, a solve of order 512 0.47 → 0.54 ms).
49#if defined(_MSC_VER) && !defined(__clang__)
50# define NUMERICAL_OUT_OF_LINE __declspec(noinline)
51#else
52# define NUMERICAL_OUT_OF_LINE [[gnu::noinline]] // NOLINT(cppcoreguidelines-macro-usage): an attribute, which no function can stand for
53#endif
54
55#if defined(__clang__)
56// A block's depth loop kept scalar: vectorised, Clang sums its terms in order but splits each multiply-add (which
57// -ffp-contract=on lets it fuse, not makes it) into a product and a sum, the last terms fused: one element rounded
58// two ways, where the packed kernels fuse every term.
59# define NUMERICAL_SCALAR_DEPTH _Pragma("clang loop vectorize(disable) interleave(disable)")
60#else
61# define NUMERICAL_SCALAR_DEPTH
62#endif
63
64#if defined(__GNUC__)
65// A block's loops unrolled whole, so that each of its sums stays in a register: GCC at -O2 does not unroll them,
66// and kept the sums in memory (a product of order 17 on one arm64 core: 2.0 µs, against 0.31 under Clang).
67# define NUMERICAL_UNROLL_BLOCK _Pragma("GCC unroll 32")
68#else
69# define NUMERICAL_UNROLL_BLOCK
70#endif
71
72namespace numerical {
73
79 enum class gemm_path : std::uint8_t
80 {
81 portable = 0,
82 neon = 1,
83 avx2 = 2,
84 avx512 = 3,
85 amx = 4,
86 };
87} // namespace numerical
88
89namespace numerical::detail {
90
92#ifdef NUMERICAL_NEON_COMPILED
93 inline constexpr bool neon_compiled {true};
94#else
95 inline constexpr bool neon_compiled {false};
96#endif
97
99 [[nodiscard]] inline bool gemm_path_usable(gemm_path path) noexcept
100 {
101 switch (path) {
102#ifdef NUMERICAL_X86_COMPILED
103 case gemm_path::avx2: return cpu_features().avx2_fma;
104 case gemm_path::avx512: return cpu_features().avx512f;
105#endif
106 case gemm_path::neon: return neon_compiled;
107 case gemm_path::amx: return amx_usable();
108 default: return path == gemm_path::portable;
109 }
110 }
111
114 {
115 for (auto p {static_cast<int>(most)}; p > 0; --p) {
116 if (gemm_path_usable(static_cast<gemm_path>(p))) {
117 return static_cast<gemm_path>(p);
118 }
119 }
120 return gemm_path::portable;
121 }
122
125 [[nodiscard]] inline std::atomic<gemm_path>& gemm_path_cap() noexcept
126 {
127 static std::atomic<gemm_path> cap {gemm_path::amx};
128 return cap;
129 }
130
133 [[nodiscard]] inline std::atomic<std::uint8_t>& gemm_path_found() noexcept
134 {
135 static std::atomic<std::uint8_t> found {0xFF};
136 return found;
137 }
138
146 {
147 const unsigned cap {static_cast<unsigned>(gemm_path_cap().load(std::memory_order_relaxed))};
148 const unsigned found {gemm_path_found().load(std::memory_order_relaxed)};
149 if (found >> 3U == cap) {
150 return static_cast<gemm_path>(found & 7U);
151 }
152 const gemm_path path {best_gemm_path(static_cast<gemm_path>(cap))};
153 gemm_path_found().store(static_cast<std::uint8_t>((cap << 3U) | static_cast<unsigned>(path)), std::memory_order_relaxed);
154 return path;
155 }
156
163 inline void subtract_product(double* y,
164 const double& f,
165 const double* x,
166 std::size_t count)
167 {
168 const double factor {f}; // read once: it may be an element of y's array
169#ifdef NUMERICAL_X86_COMPILED
171 if (path == gemm_path::avx512) {
172 avx512_scaled_add(y, x, -factor, count);
173 return;
174 }
175 if (path == gemm_path::avx2) {
176 avx2_scaled_add(y, x, -factor, count);
177 return;
178 }
179#endif
180 subtract_product<double>(y, factor, x, count);
181 }
182
184 inline void subtract_product(double* y,
185 const double* x,
186 const double& f,
187 std::size_t count)
188 {
189 subtract_product(y, f, x, count);
190 }
191
194 inline void subtract_products(double* y,
195 const double* f,
196 const double* x,
197 std::size_t ldx,
198 std::size_t terms,
199 std::size_t count)
200 {
201#ifdef NUMERICAL_X86_COMPILED
203 if (path == gemm_path::avx512) {
204 avx512_subtract_products(y, f, x, ldx, terms, count);
205 return;
206 }
207 if (path == gemm_path::avx2) {
208 avx2_subtract_products(y, f, x, ldx, terms, count);
209 return;
210 }
211#endif
212 subtract_products<double>(y, f, x, ldx, terms, count);
213 }
214
217 inline void subtract_rows(double* y,
218 std::size_t ldy,
219 const double* f,
220 std::size_t ldf,
221 const double* x,
222 std::size_t ldx,
223 std::size_t terms,
224 std::size_t count)
225 {
226#ifdef NUMERICAL_X86_COMPILED
228 if (path == gemm_path::avx512) {
229 avx512_subtract_rows(y, ldy, f, ldf, x, ldx, terms, count);
230 return;
231 }
232 if (path == gemm_path::avx2) {
233 avx2_subtract_rows(y, ldy, f, ldf, x, ldx, terms, count);
234 return;
235 }
236#endif
237 subtract_rows<double>(y, ldy, f, ldf, x, ldx, terms, count);
238 }
239
246 inline void subtract_products(std::complex<double>* y,
247 const std::complex<double>* f,
248 const std::complex<double>* x,
249 std::size_t ldx,
250 std::size_t terms,
251 std::size_t count)
252 {
253#ifdef NUMERICAL_X86_COMPILED
254 // A complex number's parts are an array of two (the standard's layout guarantee): addressed as doubles.
257 auto* const into {reinterpret_cast<double*>(y)}; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the layout guarantee above
258 const auto* const scales {reinterpret_cast<const double*>(f)}; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the layout guarantee above
259 const auto* const rows {reinterpret_cast<const double*>(x)}; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the layout guarantee above
260 if (path == gemm_path::avx512) {
262 }
263 else {
265 }
266 return;
267 }
268#endif
270 }
271
274 inline void subtract_product(std::complex<double>* y,
275 const std::complex<double>& f,
276 const std::complex<double>* x,
277 std::size_t count)
278 {
279 const std::complex<double> factor {f}; // read once: it may be an element of y's array
280#ifdef NUMERICAL_X86_COMPILED
283 subtract_products(y, &factor, x, 0, 1, count);
284 return;
285 }
286#endif
287 subtract_product<std::complex<double>>(y, factor, x, count);
288 }
289
291 inline void subtract_product(std::complex<double>* y,
292 const std::complex<double>* x,
293 const std::complex<double>& f,
294 std::size_t count)
295 {
296 const std::complex<double> factor {f}; // read once: it may be an element of y's array
297#ifdef NUMERICAL_X86_COMPILED
300 subtract_products(y, &factor, x, 0, 1, count);
301 return;
302 }
303#endif
304 subtract_product<std::complex<double>>(y, x, factor, count);
305 }
306
308 inline void add_product(double* y,
309 const double& f,
310 const double* x,
311 std::size_t count)
312 {
313 const double factor {f}; // read once: it may be an element of y's array
314#ifdef NUMERICAL_X86_COMPILED
316 if (path == gemm_path::avx512) {
317 avx512_scaled_add(y, x, factor, count);
318 return;
319 }
320 if (path == gemm_path::avx2) {
321 avx2_scaled_add(y, x, factor, count);
322 return;
323 }
324#endif
325 add_product<double>(y, factor, x, count);
326 }
327
330 inline void add_product(std::complex<float>* y,
331 const std::complex<float>& f,
332 const std::complex<float>* x,
333 std::size_t count)
334 {
335 const std::complex<float> factor {f}; // read once: it may be an element of y's array
336#ifdef NUMERICAL_X86_COMPILED
339 for (std::size_t i {avx2_scaled_add_c32(y, x, factor, count)}; i < count; ++i) {
340 complex_scaled_add(y[i], factor, x[i]);
341 }
342 return;
343 }
344#endif
345 add_product<std::complex<float>>(y, factor, x, count);
346 }
347
349 inline void add_product(std::complex<double>* y,
350 const std::complex<double>& f,
351 const std::complex<double>* x,
352 std::size_t count)
353 {
354 const std::complex<double> factor {f}; // read once: it may be an element of y's array
355#ifdef NUMERICAL_X86_COMPILED
358 for (std::size_t i {avx2_scaled_add_c64(y, x, factor, count)}; i < count; ++i) {
359 complex_scaled_add(y[i], factor, x[i]);
360 }
361 return;
362 }
363#endif
364 add_product<std::complex<double>>(y, factor, x, count);
365 }
366
369 [[nodiscard]] inline double dot_product(const double* x,
370 const double* y,
371 std::size_t count)
372 {
373#ifdef NUMERICAL_X86_COMPILED
375 if (path == gemm_path::avx512) {
376 return avx512_dot(x, y, count);
377 }
378 if (path == gemm_path::avx2) {
379 return avx2_dot(x, y, count);
380 }
381#endif
382 return dot_product<double>(x, y, count);
383 }
384
388 template <split_complex C>
390 const C* y,
391 std::size_t count)
392 {
393#ifdef NUMERICAL_X86_COMPILED
395 return avx2_complex_dot(x, y, count);
396 }
397#endif
398 return dot_product<C>(x, y, count); // the native vectors' (the overloads below come here)
399 }
400
402 [[nodiscard]] inline std::complex<double> dot_product(const std::complex<double>* x,
403 const std::complex<double>* y,
404 std::size_t count)
405 {
406 return complex_dot_product(x, y, count);
407 }
408
410 [[nodiscard]] inline std::complex<float> dot_product(const std::complex<float>* x,
411 const std::complex<float>* y,
412 std::size_t count)
413 {
414 return complex_dot_product(x, y, count);
415 }
416
418 template <split_complex C>
419 void complex_row_dots(const C* m,
420 std::size_t ld,
421 std::size_t rows,
422 const C* x,
423 std::size_t count,
424 C* out)
425 {
426 for (std::size_t r {0}; r < rows; ++r) {
427 out[r] = complex_dot_product(m + (r * ld), x, count);
428 }
429 }
430
432 inline void row_dots(const std::complex<double>* m,
433 std::size_t ld,
434 std::size_t rows,
435 const std::complex<double>* x,
436 std::size_t count,
437 std::complex<double>* out)
438 {
439 complex_row_dots(m, ld, rows, x, count, out);
440 }
441
443 inline void row_dots(const std::complex<float>* m,
444 std::size_t ld,
445 std::size_t rows,
446 const std::complex<float>* x,
447 std::size_t count,
448 std::complex<float>* out)
449 {
450 complex_row_dots(m, ld, rows, x, count, out);
451 }
452
455 inline void row_dots(const double* m,
456 std::size_t ld,
457 std::size_t rows,
458 const double* x,
459 std::size_t count,
460 double* out)
461 {
462#ifdef NUMERICAL_X86_COMPILED
464 for (std::size_t r {0}; r < rows; ++r) {
465 out[r] = dot_product(m + (r * ld), x, count);
466 }
467 return;
468 }
469#endif
470 row_dots<double>(m, ld, rows, x, count, out);
471 }
472
474 inline constexpr std::size_t gemm_mr {4};
475
478 template <typename T>
479 inline constexpr std::size_t gemm_nr {sizeof(T) >= 16 ? std::size_t {6} : std::size_t {96} / sizeof(T)};
480
482 inline constexpr std::size_t gemm_kc {256};
483
485 inline constexpr std::size_t gemm_mc {64};
486
488 inline constexpr std::size_t gemm_slivers_per_task {4};
489
492 inline constexpr std::size_t tasks_per_thread {4};
493
497 inline constexpr std::size_t gemm_kernel_speedup {16};
498
500 template <typename T>
501 inline constexpr std::size_t gemm_nc {gemm_nr<T> * 128};
502
510#ifdef NUMERICAL_X86_COMPILED
511 inline constexpr std::size_t gemm_packed_threshold {std::size_t {1} << 11U};
512#else
513 inline constexpr std::size_t gemm_packed_threshold {std::size_t {1} << 13U};
514#endif
515
519 inline constexpr std::size_t gemm_min_depth {8};
520
521 template <typename T>
522 using gemm_tile = std::array<T, gemm_mr * gemm_nr<T>>;
523
532 template <typename T>
533 void gemm_kernel(std::size_t kc,
534 const T* ap,
535 const T* bp,
536 std::size_t ldb,
537 gemm_tile<T>& tile)
538 {
539 constexpr std::size_t nr {gemm_nr<T>};
540 for (std::size_t p {0}; p < kc; ++p) {
541 for (std::size_t i {0}; i < gemm_mr; ++i) {
542 const T ai {ap[(p * gemm_mr) + i]};
543 for (std::size_t j {0}; j < nr; ++j) {
544 tile[(i * nr) + j] += ai * bp[(p * ldb) + j];
545 }
546 }
547 }
548 }
549
550#if defined(__GNUC__)
555 template <typename T>
556 requires std::is_same_v<T, float> || std::is_same_v<T, double>
557 void gemm_kernel(std::size_t kc,
558 const T* ap,
559 const T* bp,
560 std::size_t ldb,
561 gemm_tile<T>& tile)
562 {
563 using lane = typename native_vector<T>::type;
564 constexpr std::size_t nr {gemm_nr<T>};
565 constexpr std::size_t lanes {nr * sizeof(T) / native_vector_bytes};
566 // The tile moves in and out one vector at a time: a single copy of the whole array would leave it in
567 // memory under GCC, which then keeps no accumulator in a register (7.4 GFLOP/s instead of 11.8 on one
568 // x86-64 core).
569 std::array<lane, gemm_mr * lanes> acc {};
570# pragma GCC unroll 32
571 for (std::size_t q {0}; q < acc.size(); ++q) {
572 lane v;
573 std::memcpy(&v, tile.data() + (q * (native_vector_bytes / sizeof(T))), sizeof(v));
574 acc[q] = v;
575 }
576 // Unrolled, so that every accumulator has a fixed index and stays in a register: GCC at -O2 does not
577 // unroll on its own, and then reads and writes the tile through memory.
578 for (std::size_t p {0}; p < kc; ++p) {
579 std::array<lane, lanes> b {};
580 std::memcpy(b.data(), bp + (p * ldb), sizeof(b));
581# pragma GCC unroll 8
582 for (std::size_t i {0}; i < gemm_mr; ++i) {
583 const T ai {ap[(p * gemm_mr) + i]};
584# pragma GCC unroll 8
585 for (std::size_t j {0}; j < lanes; ++j) {
586 acc[(i * lanes) + j] += ai * b[j];
587 }
588 }
589 }
590# pragma GCC unroll 32
591 for (std::size_t q {0}; q < acc.size(); ++q) {
592 const lane v {acc[q]};
593 std::memcpy(tile.data() + (q * (native_vector_bytes / sizeof(T))), &v, sizeof(v));
594 }
595 }
596
597#endif
598
599#ifdef NUMERICAL_NEON_COMPILED
607 inline void neon_kernel(std::size_t kc,
608 const double* ap,
609 const double* bp,
610 std::size_t ldb,
611 gemm_tile<double>& tile) noexcept
612 {
613 static_assert(gemm_mr == 4 && gemm_nr<double> == 12, "two rows of A a vector, twelve columns six vectors");
614 constexpr std::size_t lanes {6};
615 std::array<float64x2_t, 4 * lanes> acc {};
617 for (std::size_t q {0}; q < acc.size(); ++q) {
618 acc[q] = vld1q_f64(tile.data() + (2 * q));
619 }
620 for (std::size_t p {0}; p < kc; ++p) {
621 const float64x2_t top {vld1q_f64(ap + (p * gemm_mr))};
622 const float64x2_t bottom {vld1q_f64(ap + (p * gemm_mr) + 2)};
624 for (std::size_t j {0}; j < lanes; ++j) {
625 const float64x2_t b {vld1q_f64(bp + (p * ldb) + (2 * j))};
626 acc[j] = vfmaq_laneq_f64(acc[j], b, top, 0);
627 acc[lanes + j] = vfmaq_laneq_f64(acc[lanes + j], b, top, 1);
628 acc[(2 * lanes) + j] = vfmaq_laneq_f64(acc[(2 * lanes) + j], b, bottom, 0);
629 acc[(3 * lanes) + j] = vfmaq_laneq_f64(acc[(3 * lanes) + j], b, bottom, 1);
630 }
631 }
633 for (std::size_t q {0}; q < acc.size(); ++q) {
634 vst1q_f64(tile.data() + (2 * q), acc[q]);
635 }
636 }
637
640 inline void neon_kernel(std::size_t kc,
641 const float* ap,
642 const float* bp,
643 std::size_t ldb,
644 gemm_tile<float>& tile) noexcept
645 {
646 static_assert(gemm_mr == 4 && gemm_nr<float> == 24, "four rows of A a vector, 24 columns six vectors");
647 constexpr std::size_t lanes {6};
648 std::array<float32x4_t, 4 * lanes> acc {};
650 for (std::size_t q {0}; q < acc.size(); ++q) {
651 acc[q] = vld1q_f32(tile.data() + (4 * q));
652 }
653 for (std::size_t p {0}; p < kc; ++p) {
654 const float32x4_t column {vld1q_f32(ap + (p * gemm_mr))};
656 for (std::size_t j {0}; j < lanes; ++j) {
657 const float32x4_t b {vld1q_f32(bp + (p * ldb) + (4 * j))};
658 acc[j] = vfmaq_laneq_f32(acc[j], b, column, 0);
659 acc[lanes + j] = vfmaq_laneq_f32(acc[lanes + j], b, column, 1);
660 acc[(2 * lanes) + j] = vfmaq_laneq_f32(acc[(2 * lanes) + j], b, column, 2);
661 acc[(3 * lanes) + j] = vfmaq_laneq_f32(acc[(3 * lanes) + j], b, column, 3);
662 }
663 }
665 for (std::size_t q {0}; q < acc.size(); ++q) {
666 vst1q_f32(tile.data() + (4 * q), acc[q]);
667 }
668 }
669
670#endif
671
676 template <std::size_t Mr,
677 typename T>
678 void gemm_pack_a(const T* a,
679 std::size_t lda,
680 std::size_t rows,
681 std::size_t kc,
682 T* out)
683 {
684 for (std::size_t ir {0}; ir < rows; ir += Mr) {
685 const std::size_t mr {std::min(Mr, rows - ir)};
686 T* sliver {out + (ir * kc)};
687 if (mr == Mr) { // a whole sliver, its loop free of the padding's test (M1 Pro, order 64 for the AMX: 1.21 → 0.96 µs)
688 for (std::size_t p {0}; p < kc; ++p) {
689 for (std::size_t i {0}; i < Mr; ++i) {
690 sliver[(p * Mr) + i] = a[((ir + i) * lda) + p];
691 }
692 }
693 continue;
694 }
695 for (std::size_t p {0}; p < kc; ++p) {
696 for (std::size_t i {0}; i < Mr; ++i) {
697 sliver[(p * Mr) + i] = i < mr ? a[((ir + i) * lda) + p] : T {};
698 }
699 }
700 }
701 }
702
707 template <std::size_t Nr,
708 typename T>
709 void gemm_pack_b(const T* b,
710 std::size_t ldb,
711 std::size_t kc,
712 std::size_t cols,
713 T* out)
714 {
715 for (std::size_t jr {0}; jr < cols; jr += Nr) {
716 const std::size_t width {std::min(Nr, cols - jr)};
717 T* sliver {out + (jr * kc)};
718 for (std::size_t p {0}; p < kc; ++p) {
719 for (std::size_t j {0}; j < Nr; ++j) {
720 sliver[(p * Nr) + j] = j < width ? b[(p * ldb) + jr + j] : T {};
721 }
722 }
723 }
724 }
725
730 template <std::size_t Nr,
731 typename T>
732 void gemm_pack_bt(const T* bt,
733 std::size_t ldbt,
734 std::size_t kc,
735 std::size_t cols,
736 T* out)
737 {
738 for (std::size_t jr {0}; jr < cols; jr += Nr) {
739 const std::size_t width {std::min(Nr, cols - jr)};
740 T* sliver {out + (jr * kc)};
741 for (std::size_t j {0}; j < Nr; ++j) {
742 const T* const row {bt + ((jr + j) * ldbt)};
743 for (std::size_t p {0}; p < kc; ++p) {
744 sliver[(p * Nr) + j] = j < width ? row[p] : T {};
745 }
746 }
747 }
748 }
749
751 template <typename T>
752 [[nodiscard]] const T* rows_in_place(const T* x,
753 std::size_t ld,
754 std::size_t& ld_out) noexcept
755 {
756 ld_out = ld;
757 return x;
758 }
759
762 template <typename T>
763 [[nodiscard]] const T* rows_from_transpose(const T* x,
764 std::size_t ld,
765 std::size_t rows,
766 std::size_t cols,
768 std::size_t& ld_out)
769 {
770 scratch.resize(rows * cols);
771 for (std::size_t j {0}; j < cols; ++j) {
772 for (std::size_t i {0}; i < rows; ++i) {
773 scratch[(i * cols) + j] = x[(j * ld) + i];
774 }
775 }
776 ld_out = cols;
777 return scratch.data();
778 }
779
781 struct b_rows
782 {
784 template <std::size_t Nr,
785 typename T>
786 static void pack(const T* b,
787 std::size_t ldb,
788 std::size_t p0,
789 std::size_t j0,
790 std::size_t kc,
791 std::size_t cols,
792 T* out)
793 {
794 gemm_pack_b<Nr>(b + (p0 * ldb) + j0, ldb, kc, cols, out);
795 }
796
798 template <typename Kernel>
799 [[nodiscard]] static constexpr bool in_place(std::size_t ldb) noexcept
800 {
801 return Kernel::reads_b_in_place(ldb);
802 }
803
805 template <typename T>
806 [[nodiscard]] static const T* rows_of(const T* x,
807 std::size_t ld,
808 std::size_t /*rows*/,
809 std::size_t /*cols*/,
810 buffer<T>& /*scratch*/,
811 std::size_t& ld_out)
812 {
813 return rows_in_place(x, ld, ld_out);
814 }
815
817 template <typename T>
818 [[nodiscard]] static const T& at(const T* b,
819 std::size_t ldb,
820 std::size_t p,
821 std::size_t j) noexcept
822 {
823 return b[(p * ldb) + j];
824 }
825 };
826
829 {
832 template <std::size_t Nr,
833 typename T>
834 static void pack(const T* bt,
835 std::size_t ldbt,
836 std::size_t p0,
837 std::size_t j0,
838 std::size_t kc,
839 std::size_t cols,
840 T* out)
841 {
842 gemm_pack_bt<Nr>(bt + (j0 * ldbt) + p0, ldbt, kc, cols, out);
843 }
844
846 template <typename Kernel>
847 [[nodiscard]] static constexpr bool in_place(std::size_t /*ldbt*/) noexcept
848 {
849 return false;
850 }
851
854 template <typename T>
855 [[nodiscard]] static const T* rows_of(const T* x,
856 std::size_t ld,
857 std::size_t rows,
858 std::size_t cols,
860 std::size_t& ld_out)
861 {
862 return rows_from_transpose(x, ld, rows, cols, scratch, ld_out);
863 }
864
866 template <typename T>
867 [[nodiscard]] static const T& at(const T* bt,
868 std::size_t ldbt,
869 std::size_t p,
870 std::size_t j) noexcept
871 {
872 return bt[(j * ldbt) + p];
873 }
874 };
875
881 template <std::size_t Mr,
882 typename T>
883 void gemm_pack_at(const T* at,
884 std::size_t ldat,
885 std::size_t rows,
886 std::size_t kc,
887 T* out)
888 {
889 for (std::size_t ir {0}; ir < rows; ir += Mr) {
890 const std::size_t mr {std::min(Mr, rows - ir)};
891 T* sliver {out + (ir * kc)};
892 for (std::size_t p {0}; p < kc; ++p) {
893 const T* const column {at + (p * ldat) + ir};
894 for (std::size_t i {0}; i < Mr; ++i) {
895 sliver[(p * Mr) + i] = i < mr ? column[i] : T {};
896 }
897 }
898 }
899 }
900
902 struct a_rows
903 {
905 template <typename Kernel,
906 typename T>
907 static void pack(const T* a,
908 std::size_t lda,
909 std::size_t i0,
910 std::size_t p0,
911 std::size_t rows,
912 std::size_t kc,
913 T* out)
914 {
915 Kernel::pack_a(a + (i0 * lda) + p0, lda, rows, kc, out);
916 }
917
919 template <typename T>
920 [[nodiscard]] static const T* rows_of(const T* a,
921 std::size_t lda,
922 std::size_t /*rows*/,
923 std::size_t /*cols*/,
924 buffer<T>& /*scratch*/,
925 std::size_t& ld_out)
926 {
927 return rows_in_place(a, lda, ld_out);
928 }
929
931 template <typename T>
932 [[nodiscard]] static const T& at(const T* a,
933 std::size_t lda,
934 std::size_t i,
935 std::size_t p) noexcept
936 {
937 return a[(i * lda) + p];
938 }
939 };
940
943 {
946 template <typename Kernel,
947 typename T>
948 static void pack(const T* at,
949 std::size_t ldat,
950 std::size_t i0,
951 std::size_t p0,
952 std::size_t rows,
953 std::size_t kc,
954 T* out)
955 {
956 gemm_pack_at<Kernel::mr>(at + (p0 * ldat) + i0, ldat, rows, kc, out);
957 }
958
960 template <typename T>
961 [[nodiscard]] static const T* rows_of(const T* at,
962 std::size_t ldat,
963 std::size_t rows,
964 std::size_t cols,
966 std::size_t& ld_out)
967 {
968 return rows_from_transpose(at, ldat, rows, cols, scratch, ld_out);
969 }
970
972 template <typename T>
973 [[nodiscard]] static const T& at(const T* at_,
974 std::size_t ldat,
975 std::size_t i,
976 std::size_t p) noexcept
977 {
978 return at_[(p * ldat) + i];
979 }
980 };
981
986 template <typename T>
988 {
989 static constexpr std::size_t mr {gemm_mr};
990 static constexpr std::size_t nr {gemm_nr<T>};
991 static constexpr std::size_t depth {gemm_kc};
992 static constexpr std::size_t block_rows {gemm_mc};
997 static constexpr std::size_t b_in_place_rows {mr};
998 static constexpr std::size_t speedup {gemm_kernel_speedup};
999 static constexpr std::size_t serial_below {1};
1000
1002 struct session
1003 {};
1004
1007 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
1008 {
1009 return ldb <= nr;
1010 }
1011
1013 static void pack_a(const T* a,
1014 std::size_t lda,
1015 std::size_t rows,
1016 std::size_t kc,
1017 T* out)
1018 {
1019 gemm_pack_a<mr>(a, lda, rows, kc, out);
1020 }
1021
1023 static void tile(std::size_t kc,
1024 const T* ap,
1025 const T* bp,
1026 std::size_t ldb,
1027 T* c,
1028 std::size_t ldc,
1029 std::size_t rows,
1030 std::size_t cols)
1031 {
1033 accumulators.fill(T {});
1034 for (std::size_t i {0}; i < rows; ++i) {
1035 std::copy_n(c + (i * ldc), cols, accumulators.data() + (i * nr));
1036 }
1038 for (std::size_t i {0}; i < rows; ++i) {
1039 std::copy_n(accumulators.data() + (i * nr), cols, c + (i * ldc));
1040 }
1041 }
1042 };
1043
1044#ifdef NUMERICAL_NEON_COMPILED
1046 template <typename T>
1047 struct neon_tile_kernel : vector_tile_kernel<T>
1048 {
1052 NUMERICAL_ALWAYS_INLINE static void tile(std::size_t kc,
1053 const T* ap,
1054 const T* bp,
1055 std::size_t ldb,
1056 T* c,
1057 std::size_t ldc,
1058 std::size_t rows,
1059 std::size_t cols) noexcept
1060 {
1061 constexpr std::size_t nr {vector_tile_kernel<T>::nr};
1063 for (std::size_t i {0}; i < rows; ++i) {
1064 std::copy_n(c + (i * ldc), cols, accumulators.data() + (i * nr));
1065 }
1067 for (std::size_t i {0}; i < rows; ++i) {
1068 std::copy_n(accumulators.data() + (i * nr), cols, c + (i * ldc));
1069 }
1070 }
1071 };
1072#endif
1073
1074#ifdef NUMERICAL_AMX_COMPILED
1077 struct amx_tile_kernel
1078 {
1079 static constexpr std::size_t mr {amx_mr};
1080 static constexpr std::size_t nr {amx_nr};
1083 static constexpr std::size_t depth {512};
1084 static constexpr std::size_t block_rows {gemm_mc};
1087 static constexpr std::size_t b_in_place_rows {block_rows};
1088 static constexpr std::size_t speedup {gemm_kernel_speedup};
1093 static constexpr std::size_t serial_below {std::size_t {1} << 24};
1094 using session = amx_session;
1095
1097 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
1098 {
1099 return ldb <= amx_in_place_stride;
1100 }
1101
1103 static void pack_a(const double* a,
1104 std::size_t lda,
1105 std::size_t rows,
1106 std::size_t kc,
1107 double* out) noexcept
1108 {
1109 amx_pack_a(a, lda, rows, kc, out);
1110 }
1111
1113 static void tile(std::size_t kc,
1114 const double* ap,
1115 const double* bp,
1116 std::size_t ldb,
1117 double* c,
1118 std::size_t ldc,
1119 std::size_t rows,
1120 std::size_t cols) noexcept
1121 {
1122 amx_tile_f64(kc, ap, bp, ldb, c, ldc, rows, cols);
1123 }
1124 };
1125
1127 struct amx_tile_kernel_f32
1128 {
1129 static constexpr std::size_t mr {amx_f32_side};
1130 static constexpr std::size_t nr {amx_f32_side};
1131 static constexpr std::size_t depth {512};
1132 static constexpr std::size_t block_rows {gemm_mc};
1133 static constexpr std::size_t b_in_place_rows {block_rows};
1134 static constexpr std::size_t speedup {gemm_kernel_speedup};
1135 static constexpr std::size_t serial_below {std::size_t {1} << 24};
1136 using session = amx_session;
1137
1138 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
1139 {
1140 return ldb <= 2 * amx_in_place_stride;
1141 }
1142
1143 static void pack_a(const float* a,
1144 std::size_t lda,
1145 std::size_t rows,
1146 std::size_t kc,
1147 float* out) noexcept
1148 {
1149 amx_pack_a_f32(a, lda, rows, kc, out);
1150 }
1151
1152 static void tile(std::size_t kc,
1153 const float* ap,
1154 const float* bp,
1155 std::size_t ldb,
1156 float* c,
1157 std::size_t ldc,
1158 std::size_t rows,
1159 std::size_t cols) noexcept
1160 {
1161 amx_tile_f32(kc, ap, bp, ldb, c, ldc, rows, cols);
1162 }
1163 };
1164#endif
1165
1171#ifdef NUMERICAL_X86_COMPILED
1172 inline constexpr std::size_t narrow_least_depth {8};
1173#else
1174 inline constexpr std::size_t narrow_least_depth {2};
1175#endif
1176
1178 inline constexpr std::size_t narrow_columns {8};
1179
1185 template <typename T>
1187 T a,
1188 T b) noexcept
1189 {
1190#if defined(__GNUC__) && !defined(__clang__)
1191 const T product {a * b};
1192 return sum + product;
1193#else
1194 return sum + (a * b);
1195#endif
1196 }
1197
1200 template <typename R>
1201 [[nodiscard]] inline std::complex<R> rounded_apart_add(std::complex<R> sum,
1202 std::complex<R> a,
1203 std::complex<R> b) noexcept
1204 {
1205 return sum + operator_times(a, b);
1206 }
1207
1211 inline constexpr std::size_t narrow_group {4};
1212
1216 enum class term_rounding : unsigned char
1217 {
1218 compiler,
1219 fused,
1220 x86_fused,
1221 };
1222
1224 template <term_rounding Mode>
1225 using rounding_is = std::integral_constant<term_rounding, Mode>;
1226
1228 template <typename T>
1230 T sum,
1231 T a,
1232 T b) noexcept
1233 {
1234 return rounded_apart_add(sum, a, b);
1235 }
1236
1238 template <typename T>
1240 T sum,
1241 T a,
1242 T b) noexcept
1243 {
1244 return std::fma(a, b, sum);
1245 }
1246
1247#ifdef NUMERICAL_X86_COMPILED
1250 template <std::size_t R, std::size_t N, typename T>
1252 const T* a,
1253 std::size_t lda,
1254 const T* b,
1255 std::size_t ldb,
1256 T* c,
1257 std::size_t ldc) noexcept
1258 {
1259 std::array<T, R * N> sums {};
1261 for (std::size_t e {0}; e < R * N; ++e) {
1262 sums[e] = c[((e / N) * ldc) + (e % N)];
1263 }
1264 for (std::size_t p {0}; p < k; ++p) {
1266 for (std::size_t r {0}; r < R; ++r) {
1267 const T ap {a[(r * lda) + p]};
1269 for (std::size_t j {0}; j < N; ++j) {
1270 sums[(r * N) + j] = std::fma(ap, b[(p * ldb) + j], sums[(r * N) + j]);
1271 }
1272 }
1273 }
1275 for (std::size_t e {0}; e < R * N; ++e) {
1276 c[((e / N) * ldc) + (e % N)] = sums[e];
1277 }
1278 }
1279
1281 template <typename T>
1282 NUMERICAL_TARGET("avx2,fma") void plain_row_x86(std::size_t n,
1283 std::size_t k,
1284 const T* a,
1285 const T* b,
1286 std::size_t ldb,
1287 T* c) noexcept
1288 {
1289 for (std::size_t p {0}; p < k; ++p) {
1290 const T ap {a[p]};
1291 for (std::size_t j {0}; j < n; ++j) {
1292 c[j] = std::fma(ap, b[(p * ldb) + j], c[j]);
1293 }
1294 }
1295 }
1296
1297#endif
1298
1303 template <std::size_t R, std::size_t N, term_rounding Mode, typename T>
1305 const T* a,
1306 std::size_t lda,
1307 const T* b,
1308 std::size_t ldb,
1309 T* c,
1310 std::size_t ldc) noexcept
1311 {
1312#ifdef NUMERICAL_X86_COMPILED
1313 if constexpr (Mode == term_rounding::x86_fused) {
1314 narrow_block_x86<R, N>(k, a, lda, b, ldb, c, ldc);
1315 }
1316 else
1317#endif
1318 {
1319 std::array<T, R * N> sums {};
1321 for (std::size_t e {0}; e < R * N; ++e) {
1322 sums[e] = c[((e / N) * ldc) + (e % N)];
1323 }
1325 for (std::size_t p {0}; p < k; ++p) {
1327 for (std::size_t r {0}; r < R; ++r) {
1328 const T ap {a[(r * lda) + p]};
1330 for (std::size_t j {0}; j < N; ++j) {
1331 sums[(r * N) + j] = rounded_term(rounding_is<Mode> {}, sums[(r * N) + j], ap, b[(p * ldb) + j]);
1332 }
1333 }
1334 }
1336 for (std::size_t e {0}; e < R * N; ++e) {
1337 c[((e / N) * ldc) + (e % N)] = sums[e];
1338 }
1339 }
1340 }
1341
1343 template <std::size_t R, term_rounding Mode, typename T>
1344 void narrow_rows(std::size_t n,
1345 std::size_t k,
1346 const T* a,
1347 std::size_t lda,
1348 const T* b,
1349 std::size_t ldb,
1350 T* c,
1351 std::size_t ldc) noexcept
1352 {
1353 static_assert(narrow_columns == 8, "the switch below names each narrower last block");
1354 std::size_t j {0};
1355 for (; j + narrow_columns <= n; j += narrow_columns) {
1356 narrow_block<R, narrow_columns, Mode>(k, a, lda, b + j, ldb, c + j, ldc);
1357 }
1358 switch (n - j) {
1359 case 1: narrow_block<R, 1, Mode>(k, a, lda, b + j, ldb, c + j, ldc); break;
1360 case 2: narrow_block<R, 2, Mode>(k, a, lda, b + j, ldb, c + j, ldc); break;
1361 case 3: narrow_block<R, 3, Mode>(k, a, lda, b + j, ldb, c + j, ldc); break;
1362 case 4: narrow_block<R, 4, Mode>(k, a, lda, b + j, ldb, c + j, ldc); break;
1363 case 5: narrow_block<R, 5, Mode>(k, a, lda, b + j, ldb, c + j, ldc); break;
1364 case 6: narrow_block<R, 6, Mode>(k, a, lda, b + j, ldb, c + j, ldc); break;
1365 case 7: narrow_block<R, 7, Mode>(k, a, lda, b + j, ldb, c + j, ldc); break;
1366 default: break; // a whole number of blocks
1367 }
1368 }
1369
1380 template <term_rounding Mode, typename T>
1381 void gemm_rows_rounded(std::size_t m,
1382 std::size_t n,
1383 std::size_t k,
1384 const T* a,
1385 std::size_t lda,
1386 const T* b,
1387 std::size_t ldb,
1388 T* c,
1389 std::size_t ldc)
1390 {
1391 if (k >= (n <= narrow_columns ? std::size_t {2} : narrow_least_depth)) {
1392 parallel_for((m + narrow_group - 1) / narrow_group, narrow_group * k * n / vector_speedup, [&](std::size_t g) {
1393 const std::size_t first {g * narrow_group};
1394 if (first + narrow_group <= m) {
1395 narrow_rows<narrow_group, Mode>(n, k, a + (first * lda), lda, b, ldb, c + (first * ldc), ldc);
1396 return;
1397 }
1398 for (std::size_t i {first}; i < m; ++i) {
1399 narrow_rows<1, Mode>(n, k, a + (i * lda), lda, b, ldb, c + (i * ldc), ldc);
1400 }
1401 });
1402 return;
1403 }
1404 parallel_for(m, k * n / vector_speedup, [&](std::size_t i) {
1405#ifdef NUMERICAL_X86_COMPILED
1406 if constexpr (Mode == term_rounding::x86_fused) {
1407 plain_row_x86(n, k, a + (i * lda), b, ldb, c + (i * ldc));
1408 }
1409 else
1410#endif
1411 {
1412 for (std::size_t p {0}; p < k; ++p) {
1413 const T aip {a[(i * lda) + p]};
1414 for (std::size_t j {0}; j < n; ++j) {
1415 c[(i * ldc) + j] = rounded_term(rounding_is<Mode> {}, c[(i * ldc) + j], aip, b[(p * ldb) + j]);
1416 }
1417 }
1418 }
1419 });
1420 }
1421
1430 [[nodiscard]] inline bool rows_fuse() noexcept
1431 {
1432#ifdef NUMERICAL_NEON_COMPILED
1433 return gemm_path_cap().load(std::memory_order_relaxed) != gemm_path::portable;
1434#else
1435 return false;
1436#endif
1437 }
1438
1444 template <typename T>
1445 void gemm_rows(std::size_t m,
1446 std::size_t n,
1447 std::size_t k,
1448 const T* a,
1449 std::size_t lda,
1450 const T* b,
1451 std::size_t ldb,
1452 T* c,
1453 std::size_t ldc)
1454 {
1455#ifdef NUMERICAL_X86_COMPILED
1456 if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float>) {
1459 gemm_rows_rounded<term_rounding::x86_fused>(m, n, k, a, lda, b, ldb, c, ldc);
1460 return;
1461 }
1462 }
1463#endif
1464 if constexpr (std::is_floating_point_v<T>) {
1465 if (rows_fuse()) {
1466 gemm_rows_rounded<term_rounding::fused>(m, n, k, a, lda, b, ldb, c, ldc);
1467 return;
1468 }
1469 }
1470 gemm_rows_rounded<term_rounding::compiler>(m, n, k, a, lda, b, ldb, c, ldc);
1471 }
1472
1474 inline constexpr std::size_t packed_line {64};
1475
1478 template <typename T>
1479 [[nodiscard]] T* line_aligned(T* at) noexcept
1480 {
1481 const std::size_t miss {static_cast<std::size_t>(std::bit_cast<std::uintptr_t>(at) % packed_line)};
1482 return at + (((packed_line - miss) % packed_line) / sizeof(T));
1483 }
1484
1495 template <typename T>
1497 {
1498 public:
1499
1501 static constexpr std::size_t kept_limit {(std::size_t {32} << 20U) / sizeof(T)};
1502
1505 std::size_t b_count)
1506 : storage_ {grown(std::exchange(kept(), buffer<T> {}), a_count + padding + b_count + padding)},
1507 a_ {aligned(storage_.data())},
1508 b_ {aligned(storage_.data() + a_count + padding)}
1509 {}
1510
1515
1517 {
1518 kept() = storage_.size() <= kept_limit ? std::move(storage_) : buffer<T> {};
1519 }
1520
1523 {
1524 return a_;
1525 }
1526
1529 {
1530 return b_;
1531 }
1532
1535 {
1536 thread_local buffer<T> storage;
1537 return storage;
1538 }
1539
1540 private:
1541
1542 static constexpr std::size_t padding {(packed_line / sizeof(T)) + 1};
1543
1546 std::size_t count)
1547 {
1548 storage.resize(std::max(storage.size(), count), T {});
1549 return storage;
1550 }
1551
1553 [[nodiscard]] static T* aligned(T* at) noexcept
1554 {
1555 return line_aligned(at);
1556 }
1557
1561 };
1562
1565 template <typename Kernel>
1566 [[nodiscard]] constexpr bool gemm_shares(std::size_t m,
1567 std::size_t n,
1568 std::size_t k) noexcept
1569 {
1570 return m * n * k >= Kernel::serial_below;
1571 }
1572
1576 template <typename Kernel>
1577 inline constexpr bool prefetches_next_row_of_c {requires(const double* c, std::size_t ldc) {
1578 Kernel::prefetch_rows(c, ldc);
1579 }};
1580
1582 template <typename Kernel,
1583 typename T>
1584 void prefetch_next_row(const T* /*c*/,
1585 std::size_t /*ldc*/) noexcept
1586 {
1587 }
1588
1590 template <typename Kernel,
1591 typename T>
1592 requires prefetches_next_row_of_c<Kernel>
1593 void prefetch_next_row(const T* c,
1594 std::size_t ldc) noexcept
1595 {
1596 Kernel::prefetch_rows(c, ldc);
1597 }
1598
1600 template <typename Kernel>
1601 [[nodiscard]] constexpr std::size_t task_rows_cap(std::size_t /*kc*/) noexcept
1602 {
1603 return Kernel::block_rows;
1604 }
1605
1607 template <typename Kernel>
1608 requires requires(std::size_t kc)
1609 {
1610 Kernel::task_rows_for(kc);
1611 }
1612
1613 [[nodiscard]] constexpr std::size_t task_rows_cap(std::size_t kc) noexcept
1614 {
1615 return Kernel::task_rows_for(kc);
1616 }
1617
1627 template <typename Kernel,
1628 typename T,
1629 typename B = b_rows,
1630 typename A = a_rows>
1631 void gemm_packed(std::size_t m,
1632 std::size_t n,
1633 std::size_t k,
1634 const T* a,
1635 std::size_t lda,
1636 const T* b,
1637 std::size_t ldb,
1638 T* c,
1639 std::size_t ldc,
1640 const T* a_packed = nullptr)
1641 {
1642 constexpr std::size_t mr {Kernel::mr};
1643 constexpr std::size_t nr {Kernel::nr};
1644 constexpr std::size_t nc_max {nr * 128};
1645 const std::size_t a_rows {((m + mr) - 1) / mr * mr};
1646 const std::size_t b_cols {((std::min(nc_max, n) + nr) - 1) / nr * nr};
1647 const packing_storage<T> packed {a_packed != nullptr ? 0 : a_rows * std::min(Kernel::depth, k), b_cols * std::min(Kernel::depth, k)};
1648 const std::size_t a_blocks {a_packed != nullptr ? 0 : ((m + Kernel::block_rows) - 1) / Kernel::block_rows};
1649 const T* const a_tiles {a_packed != nullptr ? a_packed : packed.a()};
1650 const std::size_t shared {gemm_shares<Kernel>(m, n, k) ? 1U : 0U}; // a weight of zero starts no thread
1651 // B's whole slivers unpacked: rows close enough, or few enough rows of A whatever B's stride
1652 // (Kernel::b_in_place_rows), where the kernel reads B in place at all.
1653 const bool in_place {B::template in_place<Kernel>(ldb) || (m <= Kernel::b_in_place_rows && B::template in_place<Kernel>(1))};
1654 for (std::size_t jc {0}; jc < n; jc += nc_max) {
1655 const std::size_t nc {std::min(nc_max, n - jc)};
1656 const std::size_t slivers {((nc + nr) - 1) / nr};
1657 for (std::size_t pc {0}; pc < k; pc += Kernel::depth) {
1658 const std::size_t kc {std::min(Kernel::depth, k - pc)};
1659 const std::size_t groups {((slivers + gemm_slivers_per_task) - 1) / gemm_slivers_per_task};
1660 // One pass packs both: B's groups of slivers first, then A's row blocks.
1661 // The packing weighed as the tile's work is (\p Kernel's speedup): a small product then packs in parallel too
1662 // (eight AVX2 cores: order 128 40 → 30 µs, 192 92 → 62).
1663 parallel_for(groups + a_blocks, shared * Kernel::block_rows * kc * gemm_kernel_speedup / Kernel::speedup, [&](std::size_t piece) {
1664 if (piece < groups) {
1665 const std::size_t jr {piece * gemm_slivers_per_task * nr};
1666 const std::size_t width {std::min(gemm_slivers_per_task * nr, nc - jr)};
1667 const std::size_t whole {in_place ? width - (width % nr) : 0}; // read in place
1668 if (whole < width) { // a partial sliver is packed even so: its outside zero
1669 B::template pack<nr>(b, ldb, pc, jc + jr + whole, kc, width - whole, packed.b() + ((jr + whole) * kc));
1670 }
1671 }
1672 else {
1673 const std::size_t ic {(piece - groups) * Kernel::block_rows};
1674 A::template pack<Kernel>(a, lda, ic, pc, std::min(Kernel::block_rows, m - ic), kc, packed.a() + (ic * kc));
1675 }
1676 });
1677 // A task covers task_rows rows: a whole block, or fewer whole tiles when blocks would leave a thread fewer
1678 // than tasks_per_thread tasks (order 256 on twenty x86-64 threads: twelve tasks took 116 µs, 44 took 104).
1679 const std::size_t tiles {((m + mr) - 1) / mr};
1680 const std::size_t row_tasks {(((tasks_per_thread * max_threads()) + groups) - 1) / groups};
1681 const std::size_t task_rows {std::clamp(((tiles + row_tasks) - 1) / row_tasks, std::size_t {1}, task_rows_cap<Kernel>(kc) / mr) * mr};
1682 const std::size_t task_count {((m + task_rows) - 1) / task_rows};
1683 parallel_for(task_count * groups, shared * task_rows * gemm_slivers_per_task * nr * kc / Kernel::speedup, [&](std::size_t task) {
1684 const std::size_t ic {(task / groups) * task_rows};
1685 const std::size_t first {(task % groups) * gemm_slivers_per_task};
1686 const std::size_t last {std::min(first + gemm_slivers_per_task, slivers)};
1687 const std::size_t ic_end {std::min(ic + task_rows, m)};
1688 [[maybe_unused]] const typename Kernel::session on {};
1689 // A sliver (the operand each step broadcasts) stays in the L1 cache across the task's B slivers,
1690 // which stream from the L2, as BLIS orders its loops: B sliver outer, its 32 KiB filled the L1
1691 // (a product of order 2048 on one AVX-512 core: 72 GFLOP/s, 77 in this order; AVX2 and AMX gain too).
1692 for (std::size_t ir {ic}; ir < ic_end; ir += mr) {
1693 for (std::size_t s {first}; s < last; ++s) {
1694 const std::size_t jr {s * nr};
1695 const bool direct {in_place && jr + nr <= nc};
1696 if (ir + mr < ic_end) { // the next row's tile of C, while this one computes
1697 prefetch_next_row<Kernel>(c + ((ir + mr) * ldc) + jc + jr, ldc);
1698 }
1699 Kernel::tile(kc, a_tiles + (ir * kc), direct ? b + (pc * ldb) + jc + jr : packed.b() + (jr * kc),
1700 direct ? ldb : nr, c + (ir * ldc) + jc + jr, ldc, std::min(mr, m - ir), std::min(nr, nc - jr));
1701 }
1702 }
1703 });
1704 }
1705 }
1706 }
1707
1716 template <typename Kernel,
1717 typename T,
1718 typename B = b_rows,
1719 typename A = a_rows>
1720 void gemm_packed_triangle(std::size_t n,
1721 std::size_t k,
1722 const T* a,
1723 std::size_t lda,
1724 const T* b,
1725 std::size_t ldb,
1726 T* c,
1727 std::size_t ldc,
1728 bool lower)
1729 {
1730 constexpr std::size_t mr {Kernel::mr};
1731 constexpr std::size_t nr {Kernel::nr};
1732 constexpr std::size_t nc_max {nr * 128};
1733 const std::size_t a_rows {((n + mr) - 1) / mr * mr};
1734 const std::size_t b_cols {((std::min(nc_max, n) + nr) - 1) / nr * nr};
1735 const packing_storage<T> packed {a_rows * std::min(Kernel::depth, k), b_cols * std::min(Kernel::depth, k)};
1736 const std::size_t a_blocks {((n + Kernel::block_rows) - 1) / Kernel::block_rows};
1737 const std::size_t shared {gemm_shares<Kernel>(n, n / 2, k) ? 1U : 0U};
1738 const bool in_place {B::template in_place<Kernel>(ldb)};
1739 // Whether rows [r0, r1) and columns [j0, j1) of C meet the triangle, and whether they lie wholly in it.
1740 const auto meets = [lower](std::size_t r0, std::size_t r1, std::size_t j0, std::size_t j1) { return lower ? j0 < r1 : r0 < j1; };
1741 const auto inside = [lower](std::size_t r0, std::size_t r1, std::size_t j0, std::size_t j1) { return lower ? j1 <= r0 + 1 : r1 <= j0 + 1; };
1742 for (std::size_t jc {0}; jc < n; jc += nc_max) {
1743 const std::size_t nc {std::min(nc_max, n - jc)};
1744 const std::size_t slivers {((nc + nr) - 1) / nr};
1745 for (std::size_t pc {0}; pc < k; pc += Kernel::depth) {
1746 const std::size_t kc {std::min(Kernel::depth, k - pc)};
1747 const std::size_t groups {((slivers + gemm_slivers_per_task) - 1) / gemm_slivers_per_task};
1748 parallel_for(groups + a_blocks, shared * Kernel::block_rows * kc * gemm_kernel_speedup / Kernel::speedup, [&](std::size_t piece) {
1749 if (piece < groups) {
1750 const std::size_t jr {piece * gemm_slivers_per_task * nr};
1751 const std::size_t width {std::min(gemm_slivers_per_task * nr, nc - jr)};
1752 const std::size_t whole {in_place ? width - (width % nr) : 0}; // read in place
1753 if (whole < width) { // a partial sliver is packed even so: its outside zero
1754 B::template pack<nr>(b, ldb, pc, jc + jr + whole, kc, width - whole, packed.b() + ((jr + whole) * kc));
1755 }
1756 return;
1757 }
1758 const std::size_t ic {(piece - groups) * Kernel::block_rows};
1759 const std::size_t rows {std::min(Kernel::block_rows, n - ic)};
1760 if (meets(ic, ic + rows, jc, jc + nc)) {
1761 A::template pack<Kernel>(a, lda, ic, pc, rows, kc, packed.a() + (ic * kc));
1762 }
1763 });
1764 const std::size_t tiles {((n + mr) - 1) / mr};
1765 const std::size_t row_tasks {(((tasks_per_thread * max_threads()) + groups) - 1) / groups};
1766 const std::size_t task_rows {std::clamp(((tiles + row_tasks) - 1) / row_tasks, std::size_t {1}, task_rows_cap<Kernel>(kc) / mr) * mr};
1767 const std::size_t task_count {((n + task_rows) - 1) / task_rows};
1768 parallel_for(task_count * groups, shared * task_rows * gemm_slivers_per_task * nr * kc / (2 * Kernel::speedup), [&](std::size_t task) {
1769 const std::size_t ic {(task / groups) * task_rows};
1770 const std::size_t first {(task % groups) * gemm_slivers_per_task};
1771 const std::size_t last {std::min(first + gemm_slivers_per_task, slivers)};
1772 const std::size_t ic_end {std::min(ic + task_rows, n)};
1773 [[maybe_unused]] const typename Kernel::session on {};
1774 for (std::size_t ir {ic}; ir < ic_end; ir += mr) {
1775 const std::size_t rows {std::min(mr, n - ir)};
1776 for (std::size_t s {first}; s < last; ++s) {
1777 const std::size_t jr {s * nr};
1778 const std::size_t j0 {jc + jr};
1779 const std::size_t cols {std::min(nr, nc - jr)};
1780 if (!meets(ir, ir + rows, j0, j0 + cols)) {
1781 continue;
1782 }
1783 const bool direct {in_place && jr + nr <= nc};
1784 const T* const bp {direct ? b + (pc * ldb) + j0 : packed.b() + (jr * kc)};
1785 const std::size_t ldbp {direct ? ldb : nr};
1786 T* const block {c + (ir * ldc) + j0};
1787 if (inside(ir, ir + rows, j0, j0 + cols)) {
1788 Kernel::tile(kc, packed.a() + (ir * kc), bp, ldbp, block, ldc, rows, cols);
1789 continue;
1790 }
1791 std::array<T, mr * nr> copy {};
1792 for (std::size_t i {0}; i < rows; ++i) {
1793 std::copy_n(block + (i * ldc), cols, copy.data() + (i * nr));
1794 }
1795 Kernel::tile(kc, packed.a() + (ir * kc), bp, ldbp, copy.data(), nr, rows, cols);
1796 for (std::size_t i {0}; i < rows; ++i) {
1797 for (std::size_t j {0}; j < cols; ++j) {
1798 if (lower ? j0 + j <= ir + i : j0 + j >= ir + i) {
1799 block[(i * ldc) + j] = copy[(i * nr) + j];
1800 }
1801 }
1802 }
1803 }
1804 }
1805 });
1806 }
1807 }
1808 }
1809
1810#ifdef NUMERICAL_X86_COMPILED
1812 struct avx512_tile_kernel
1813 {
1814 static constexpr std::size_t mr {avx512_mr};
1815 static constexpr std::size_t nr {avx512_nr};
1816 static constexpr std::size_t depth {gemm_kc};
1817 static constexpr std::size_t block_rows {96};
1818 static constexpr std::size_t b_in_place_rows {mr};
1821 static constexpr std::size_t speedup {gemm_kernel_speedup / 4};
1822 static constexpr std::size_t serial_below {1};
1823
1825 struct session
1826 {};
1827
1830 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
1831 {
1832 return ldb <= nr;
1833 }
1834
1836 static void pack_a(const double* a,
1837 std::size_t lda,
1838 std::size_t rows,
1839 std::size_t kc,
1840 double* out)
1841 {
1842 gemm_pack_a<mr>(a, lda, rows, kc, out);
1843 }
1844
1846 static void tile(std::size_t kc,
1847 const double* ap,
1848 const double* bp,
1849 std::size_t ldb,
1850 double* c,
1851 std::size_t ldc,
1852 std::size_t rows,
1853 std::size_t cols) noexcept
1854 {
1855 avx512_tile_f64(kc, ap, bp, ldb, c, ldc, rows, cols);
1856 }
1857 };
1858
1860 struct avx2_tile_kernel
1861 {
1862 static constexpr std::size_t mr {avx2_mr};
1863 static constexpr std::size_t nr {avx2_nr};
1864 static constexpr std::size_t depth {gemm_kc};
1865 static constexpr std::size_t block_rows {72};
1866 static constexpr std::size_t b_in_place_rows {mr};
1867
1873 [[nodiscard]] static constexpr std::size_t task_rows_for(std::size_t kc) noexcept
1874 {
1875 return kc <= 128 ? 36 : block_rows;
1876 }
1877
1883 static void prefetch_rows(const double* c,
1884 std::size_t ldc) noexcept
1885 {
1886 for (std::size_t r {0}; r < mr; ++r) {
1887 _mm_prefetch(reinterpret_cast<const char*>(c + (r * ldc)), _MM_HINT_T0); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the intrinsic takes a byte address
1888 }
1889 }
1890
1893 static constexpr std::size_t speedup {gemm_kernel_speedup / 4};
1894 static constexpr std::size_t serial_below {1};
1895
1897 struct session
1898 {};
1899
1902 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
1903 {
1904 return ldb <= nr;
1905 }
1906
1908 static void pack_a(const double* a,
1909 std::size_t lda,
1910 std::size_t rows,
1911 std::size_t kc,
1912 double* out)
1913 {
1914 gemm_pack_a<mr>(a, lda, rows, kc, out);
1915 }
1916
1918 static void tile(std::size_t kc,
1919 const double* ap,
1920 const double* bp,
1921 std::size_t ldb,
1922 double* c,
1923 std::size_t ldc,
1924 std::size_t rows,
1925 std::size_t cols) noexcept
1926 {
1927 avx2_tile_f64(kc, ap, bp, ldb, c, ldc, rows, cols);
1928 }
1929 };
1931 struct avx512_tile_kernel_f32
1932 {
1933 static constexpr std::size_t mr {avx512_mr};
1934 static constexpr std::size_t nr {avx512_nr_f32};
1935 static constexpr std::size_t depth {gemm_kc};
1936 static constexpr std::size_t block_rows {96};
1937 static constexpr std::size_t b_in_place_rows {mr};
1938 static constexpr std::size_t speedup {gemm_kernel_speedup / 4};
1939 static constexpr std::size_t serial_below {1};
1940
1941 struct session
1942 {};
1943
1944 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
1945 {
1946 return ldb <= nr;
1947 }
1948
1949 static void pack_a(const float* a,
1950 std::size_t lda,
1951 std::size_t rows,
1952 std::size_t kc,
1953 float* out)
1954 {
1955 gemm_pack_a<mr>(a, lda, rows, kc, out);
1956 }
1957
1958 static void tile(std::size_t kc,
1959 const float* ap,
1960 const float* bp,
1961 std::size_t ldb,
1962 float* c,
1963 std::size_t ldc,
1964 std::size_t rows,
1965 std::size_t cols) noexcept
1966 {
1967 avx512_tile_f32(kc, ap, bp, ldb, c, ldc, rows, cols);
1968 }
1969 };
1970
1972 struct avx2_tile_kernel_f32
1973 {
1974 static constexpr std::size_t mr {avx2_mr};
1975 static constexpr std::size_t nr {avx2_nr_f32};
1976 static constexpr std::size_t depth {gemm_kc};
1977 static constexpr std::size_t block_rows {72};
1978 static constexpr std::size_t b_in_place_rows {mr};
1979 static constexpr std::size_t speedup {gemm_kernel_speedup / 4};
1980 static constexpr std::size_t serial_below {1};
1981
1982 [[nodiscard]] static constexpr std::size_t task_rows_for(std::size_t kc) noexcept
1983 {
1984 return kc <= 128 ? 36 : block_rows;
1985 }
1986
1987 struct session
1988 {};
1989
1990 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
1991 {
1992 return ldb <= nr;
1993 }
1994
1995 static void pack_a(const float* a,
1996 std::size_t lda,
1997 std::size_t rows,
1998 std::size_t kc,
1999 float* out)
2000 {
2001 gemm_pack_a<mr>(a, lda, rows, kc, out);
2002 }
2003
2004 static void tile(std::size_t kc,
2005 const float* ap,
2006 const float* bp,
2007 std::size_t ldb,
2008 float* c,
2009 std::size_t ldc,
2010 std::size_t rows,
2011 std::size_t cols) noexcept
2012 {
2013 avx2_tile_f32(kc, ap, bp, ldb, c, ldc, rows, cols);
2014 }
2015 };
2016
2018 struct avx512_complex_tile_kernel
2019 {
2020 static constexpr std::size_t mr {avx512_complex_mr};
2021 static constexpr std::size_t nr {avx512_complex_nr};
2022 static constexpr std::size_t depth {gemm_kc / 2};
2023 static constexpr std::size_t block_rows {96};
2024 static constexpr std::size_t b_in_place_rows {mr};
2025 static constexpr std::size_t speedup {gemm_kernel_speedup / 16};
2026 static constexpr std::size_t serial_below {1};
2027
2029 struct session
2030 {};
2031
2034 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
2035 {
2036 return ldb <= nr;
2037 }
2038
2040 static void pack_a(const std::complex<double>* a,
2041 std::size_t lda,
2042 std::size_t rows,
2043 std::size_t kc,
2044 std::complex<double>* out)
2045 {
2046 gemm_pack_a<mr>(a, lda, rows, kc, out);
2047 }
2048
2050 static void tile(std::size_t kc,
2051 const std::complex<double>* ap,
2052 const std::complex<double>* bp,
2053 std::size_t ldb,
2054 std::complex<double>* c,
2055 std::size_t ldc,
2056 std::size_t rows,
2057 std::size_t cols) noexcept
2058 {
2059 avx512_tile_c64(kc, ap, bp, ldb, c, ldc, rows, cols);
2060 }
2061 };
2062
2064 struct avx2_complex_tile_kernel
2065 {
2066 static constexpr std::size_t mr {avx2_complex_mr};
2067 static constexpr std::size_t nr {avx2_complex_nr};
2068 static constexpr std::size_t depth {gemm_kc / 2};
2069 static constexpr std::size_t block_rows {72};
2070 static constexpr std::size_t b_in_place_rows {mr};
2071 static constexpr std::size_t speedup {gemm_kernel_speedup / 16};
2072 static constexpr std::size_t serial_below {1};
2073
2075 struct session
2076 {};
2077
2080 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
2081 {
2082 return ldb <= nr;
2083 }
2084
2086 static void pack_a(const std::complex<double>* a,
2087 std::size_t lda,
2088 std::size_t rows,
2089 std::size_t kc,
2090 std::complex<double>* out)
2091 {
2092 gemm_pack_a<mr>(a, lda, rows, kc, out);
2093 }
2094
2096 static void tile(std::size_t kc,
2097 const std::complex<double>* ap,
2098 const std::complex<double>* bp,
2099 std::size_t ldb,
2100 std::complex<double>* c,
2101 std::size_t ldc,
2102 std::size_t rows,
2103 std::size_t cols) noexcept
2104 {
2105 avx2_tile_c64(kc, ap, bp, ldb, c, ldc, rows, cols);
2106 }
2107 };
2109 struct avx512_complex_tile_kernel_f32
2110 {
2111 static constexpr std::size_t mr {avx512_complex_mr};
2112 static constexpr std::size_t nr {avx512_complex_nr_f32};
2113 static constexpr std::size_t depth {gemm_kc}; // an element is two `float`, the size of a `double`
2114 static constexpr std::size_t block_rows {96};
2115 static constexpr std::size_t b_in_place_rows {mr};
2116 static constexpr std::size_t speedup {gemm_kernel_speedup / 16};
2117 static constexpr std::size_t serial_below {1};
2118
2119 struct session
2120 {};
2121
2122 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
2123 {
2124 return ldb <= nr;
2125 }
2126
2127 static void pack_a(const std::complex<float>* a,
2128 std::size_t lda,
2129 std::size_t rows,
2130 std::size_t kc,
2131 std::complex<float>* out)
2132 {
2133 gemm_pack_a<mr>(a, lda, rows, kc, out);
2134 }
2135
2136 static void tile(std::size_t kc,
2137 const std::complex<float>* ap,
2138 const std::complex<float>* bp,
2139 std::size_t ldb,
2140 std::complex<float>* c,
2141 std::size_t ldc,
2142 std::size_t rows,
2143 std::size_t cols) noexcept
2144 {
2145 avx512_tile_c32(kc, ap, bp, ldb, c, ldc, rows, cols);
2146 }
2147 };
2148
2150 struct avx2_complex_tile_kernel_f32
2151 {
2152 static constexpr std::size_t mr {avx2_complex_mr};
2153 static constexpr std::size_t nr {avx2_complex_nr_f32};
2154 static constexpr std::size_t depth {gemm_kc}; // an element is two `float`, the size of a `double`
2155 static constexpr std::size_t block_rows {72};
2156 static constexpr std::size_t b_in_place_rows {mr};
2157 static constexpr std::size_t speedup {gemm_kernel_speedup / 16};
2158 static constexpr std::size_t serial_below {1};
2159
2160 struct session
2161 {};
2162
2163 static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
2164 {
2165 return ldb <= nr;
2166 }
2167
2168 static void pack_a(const std::complex<float>* a,
2169 std::size_t lda,
2170 std::size_t rows,
2171 std::size_t kc,
2172 std::complex<float>* out)
2173 {
2174 gemm_pack_a<mr>(a, lda, rows, kc, out);
2175 }
2176
2177 static void tile(std::size_t kc,
2178 const std::complex<float>* ap,
2179 const std::complex<float>* bp,
2180 std::size_t ldb,
2181 std::complex<float>* c,
2182 std::size_t ldc,
2183 std::size_t rows,
2184 std::size_t cols) noexcept
2185 {
2186 avx2_tile_c32(kc, ap, bp, ldb, c, ldc, rows, cols);
2187 }
2188 };
2189
2190#endif
2191
2193 template <typename T>
2194 [[nodiscard]] bool gemm_elsewhere(std::size_t m,
2195 std::size_t n,
2196 std::size_t k,
2197 const T* a,
2198 std::size_t lda,
2199 const T* b,
2200 std::size_t ldb,
2201 T* c,
2202 std::size_t ldc)
2203 {
2204 (void)m, (void)n, (void)k, (void)a, (void)lda, (void)b, (void)ldb, (void)c, (void)ldc;
2205 return false;
2206 }
2207
2211 template <typename B,
2212 typename A = a_rows>
2213 [[nodiscard]] bool gemm_double_elsewhere(std::size_t m,
2214 std::size_t n,
2215 std::size_t k,
2216 const double* a,
2217 std::size_t lda,
2218 const double* b,
2219 std::size_t ldb,
2220 double* c,
2221 std::size_t ldc)
2222 {
2223 const gemm_path path {gemm_path_current()};
2224#ifdef NUMERICAL_AMX_COMPILED
2225 if (path == gemm_path::amx && amx_suits(m, n, k)) { // else the NEON tile
2226 gemm_packed<amx_tile_kernel, double, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2227 return true;
2228 }
2229#endif
2230#ifdef NUMERICAL_NEON_COMPILED
2231 if (path == gemm_path::neon || path == gemm_path::amx) {
2232 gemm_packed<neon_tile_kernel<double>, double, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2233 return true;
2234 }
2235#endif
2236#ifdef NUMERICAL_X86_COMPILED
2237 if (path == gemm_path::avx512) {
2238 gemm_packed<avx512_tile_kernel, double, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2239 return true;
2240 }
2241 if (path == gemm_path::avx2) {
2242 gemm_packed<avx2_tile_kernel, double, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2243 return true;
2244 }
2245#endif
2246 (void)path;
2247 (void)m, (void)n, (void)k, (void)a, (void)lda, (void)b, (void)ldb, (void)c, (void)ldc;
2248 return false;
2249 }
2250
2255 template <typename A,
2256 typename B>
2257 void gemm_triangle(std::size_t n,
2258 std::size_t k,
2259 const double* a,
2260 std::size_t lda,
2261 const double* b,
2262 std::size_t ldb,
2263 double* c,
2264 std::size_t ldc,
2265 bool lower)
2266 {
2267 const gemm_path path {gemm_path_current()};
2268#ifdef NUMERICAL_AMX_COMPILED
2269 if (path == gemm_path::amx && amx_suits(n, n, k)) {
2270 gemm_packed_triangle<amx_tile_kernel, double, B, A>(n, k, a, lda, b, ldb, c, ldc, lower);
2271 return;
2272 }
2273#endif
2274#ifdef NUMERICAL_NEON_COMPILED
2275 if (path == gemm_path::neon || path == gemm_path::amx) {
2276 gemm_packed_triangle<neon_tile_kernel<double>, double, B, A>(n, k, a, lda, b, ldb, c, ldc, lower);
2277 return;
2278 }
2279#endif
2280#ifdef NUMERICAL_X86_COMPILED
2281 if (path == gemm_path::avx512) {
2282 gemm_packed_triangle<avx512_tile_kernel, double, B, A>(n, k, a, lda, b, ldb, c, ldc, lower);
2283 return;
2284 }
2285 if (path == gemm_path::avx2) {
2286 gemm_packed_triangle<avx2_tile_kernel, double, B, A>(n, k, a, lda, b, ldb, c, ldc, lower);
2287 return;
2288 }
2289#endif
2290 (void)path;
2291 gemm_packed_triangle<vector_tile_kernel<double>, double, B, A>(n, k, a, lda, b, ldb, c, ldc, lower);
2292 }
2293
2295 [[nodiscard]] inline bool gemm_elsewhere(std::size_t m,
2296 std::size_t n,
2297 std::size_t k,
2298 const double* a,
2299 std::size_t lda,
2300 const double* b,
2301 std::size_t ldb,
2302 double* c,
2303 std::size_t ldc)
2304 {
2305 return gemm_double_elsewhere<b_rows>(m, n, k, a, lda, b, ldb, c, ldc);
2306 }
2307
2311 template <typename B,
2312 typename A = a_rows>
2313 [[nodiscard]] bool gemm_float_elsewhere(std::size_t m,
2314 std::size_t n,
2315 std::size_t k,
2316 const float* a,
2317 std::size_t lda,
2318 const float* b,
2319 std::size_t ldb,
2320 float* c,
2321 std::size_t ldc)
2322 {
2323#ifdef NUMERICAL_NEON_COMPILED
2324 const gemm_path path {gemm_path_current()};
2325# ifdef NUMERICAL_AMX_COMPILED
2326 if (path == gemm_path::amx && amx_suits_f32(m, n, k)) {
2327 gemm_packed<amx_tile_kernel_f32, float, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2328 return true;
2329 }
2330# endif
2331 if (path == gemm_path::neon || path == gemm_path::amx) {
2332 gemm_packed<neon_tile_kernel<float>, float, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2333 return true;
2334 }
2335#endif
2336#ifdef NUMERICAL_X86_COMPILED
2337 const gemm_path x86_path {gemm_path_current()};
2338 if (x86_path == gemm_path::avx512) {
2339 gemm_packed<avx512_tile_kernel_f32, float, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2340 return true;
2341 }
2342 if (x86_path == gemm_path::avx2) {
2343 gemm_packed<avx2_tile_kernel_f32, float, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2344 return true;
2345 }
2346#endif
2347 (void)m, (void)n, (void)k, (void)a, (void)lda, (void)b, (void)ldb, (void)c, (void)ldc;
2348 return false;
2349 }
2350
2352 [[nodiscard]] inline bool gemm_elsewhere(std::size_t m,
2353 std::size_t n,
2354 std::size_t k,
2355 const float* a,
2356 std::size_t lda,
2357 const float* b,
2358 std::size_t ldb,
2359 float* c,
2360 std::size_t ldc)
2361 {
2362 return gemm_float_elsewhere<b_rows>(m, n, k, a, lda, b, ldb, c, ldc);
2363 }
2364
2365 template <typename T>
2366 void gemm(std::size_t m,
2367 std::size_t n,
2368 std::size_t k,
2369 const T* a,
2370 std::size_t lda,
2371 const T* b,
2372 std::size_t ldb,
2373 T* c,
2374 std::size_t ldc);
2375
2378 inline constexpr std::size_t split_columns {512};
2379
2381 inline constexpr std::size_t split_depth {256};
2382
2385 inline constexpr std::size_t split_kept {std::size_t {1} << 18};
2386
2391 inline constexpr std::size_t split_parts_rows {64};
2392
2400 [[nodiscard]] inline bool split_by_parts(std::size_t m) noexcept
2401 {
2402 [[maybe_unused]] const gemm_path path {gemm_path_current()};
2403#ifdef NUMERICAL_AMX_COMPILED
2404 if (path == gemm_path::amx) {
2405 return m <= split_parts_rows;
2406 }
2407#endif
2408 return m <= gemm_mr;
2409 }
2410
2414 template <typename R>
2416 {
2420 };
2421
2426 template <typename R>
2428 {
2429 thread_local split_operands<R> kept;
2430 return kept;
2431 }
2432
2440 template <split_complex Z,
2441 typename B = b_rows,
2442 typename A = a_rows>
2443 void split_parts_block(std::size_t m,
2444 std::size_t nb,
2445 std::size_t kb,
2446 std::size_t pc,
2447 std::size_t jc,
2448 const Z* a,
2449 std::size_t lda,
2450 const Z* b,
2451 std::size_t ldb,
2452 typename Z::value_type* ar,
2453 typename Z::value_type* br,
2454 typename Z::value_type* cr)
2455 {
2456 using R = typename Z::value_type;
2457 parallel_for(m, kb, [&](std::size_t i) {
2458 R* const row {ar + (i * 3 * kb)};
2459 for (std::size_t p {0}; p < kb; ++p) {
2460 const Z value {A::at(a, lda, i, pc + p)};
2461 row[p] = value.real();
2462 row[kb + p] = -value.imag();
2463 row[(2 * kb) + p] = value.imag();
2464 }
2465 });
2466 parallel_for(kb, nb, [&](std::size_t p) {
2467 R* const row {br + (p * 2 * nb)};
2468 for (std::size_t j {0}; j < nb; ++j) {
2469 const Z value {B::at(b, ldb, pc + p, jc + j)};
2470 row[j] = value.real();
2471 row[nb + j] = value.imag();
2472 }
2473 });
2474 gemm(m, nb, kb, ar, 3 * kb, br, 2 * nb, cr, 2 * nb); // Re C += Re A·Re B
2475 gemm(m, nb, kb, ar + kb, 3 * kb, br + nb, 2 * nb, cr, 2 * nb); // Re C += −Im A·Im B
2476 gemm(m, nb, kb, ar, 3 * kb, br + nb, 2 * nb, cr + nb, 2 * nb); // Im C += Re A·Im B
2477 gemm(m, nb, kb, ar + (2 * kb), 3 * kb, br, 2 * nb, cr + nb, 2 * nb); // Im C += Im A·Re B
2478 }
2479
2485 template <split_complex Z,
2486 typename B = b_rows,
2487 typename A = a_rows>
2488 void split_expanded_block(std::size_t m,
2489 std::size_t nb,
2490 std::size_t kb,
2491 std::size_t pc,
2492 std::size_t jc,
2493 const Z* a,
2494 std::size_t lda,
2495 const Z* b,
2496 std::size_t ldb,
2497 typename Z::value_type* ar,
2498 typename Z::value_type* br,
2499 typename Z::value_type* cr)
2500 {
2501 using R = typename Z::value_type;
2502 parallel_for(m, kb, [&](std::size_t i) {
2503 for (std::size_t p {0}; p < kb; ++p) {
2504 const Z value {A::at(a, lda, i, pc + p)};
2505 ar[(i * 2 * kb) + p] = value.real();
2506 ar[(i * 2 * kb) + kb + p] = -value.imag();
2507 }
2508 });
2509 parallel_for(kb, 2 * nb, [&](std::size_t p) {
2510 R* const top {br + (p * 2 * nb)};
2511 R* const bottom {br + ((kb + p) * 2 * nb)};
2512 for (std::size_t j {0}; j < nb; ++j) {
2513 const Z value {B::at(b, ldb, pc + p, jc + j)};
2514 top[j] = value.real();
2515 top[nb + j] = value.imag();
2516 bottom[j] = value.imag();
2517 bottom[nb + j] = -value.real();
2518 }
2519 });
2520 gemm(m, 2 * nb, 2 * kb, ar, 2 * kb, br, 2 * nb, cr, 2 * nb);
2521 }
2522
2531 template <split_complex Z,
2532 typename B = b_rows,
2533 typename A = a_rows>
2534 void gemm_split(std::size_t m,
2535 std::size_t n,
2536 std::size_t k,
2537 const Z* a,
2538 std::size_t lda,
2539 const Z* b,
2540 std::size_t ldb,
2541 Z* c,
2542 std::size_t ldc)
2543 {
2544 using R = typename Z::value_type;
2545 const bool parts {split_by_parts(m)};
2546 const std::size_t widest {std::min(n, split_columns)};
2547 const std::size_t deepest {std::min(k, split_depth)};
2548 split_operands<R>& kept {split_storage<R>()};
2549 kept.a.resize(std::max(kept.a.size(), m * (parts ? 3 : 2) * deepest)); // grown only, written before they are read
2550 kept.b.resize(std::max(kept.b.size(), (parts ? 2 : 4) * deepest * widest));
2551 kept.c.resize(std::max(kept.c.size(), m * 2 * widest));
2552 R* const ar {kept.a.data()};
2553 R* const br {kept.b.data()};
2554 R* const cr {kept.c.data()};
2555 for (std::size_t jc {0}; jc < n; jc += split_columns) {
2556 const std::size_t nb {std::min(split_columns, n - jc)};
2557 parallel_for(m, nb, [&](std::size_t i) {
2558 for (std::size_t j {0}; j < nb; ++j) {
2559 cr[(i * 2 * nb) + j] = c[(i * ldc) + jc + j].real();
2560 cr[(i * 2 * nb) + nb + j] = c[(i * ldc) + jc + j].imag();
2561 }
2562 });
2563 for (std::size_t pc {0}; pc < k; pc += split_depth) {
2564 const std::size_t kb {std::min(split_depth, k - pc)};
2565 if (parts) {
2566 split_parts_block<Z, B, A>(m, nb, kb, pc, jc, a, lda, b, ldb, ar, br, cr);
2567 }
2568 else {
2569 split_expanded_block<Z, B, A>(m, nb, kb, pc, jc, a, lda, b, ldb, ar, br, cr);
2570 }
2571 }
2572 parallel_for(m, nb, [&](std::size_t i) {
2573 for (std::size_t j {0}; j < nb; ++j) {
2574 c[(i * ldc) + jc + j] = Z {cr[(i * 2 * nb) + j], cr[(i * 2 * nb) + nb + j]};
2575 }
2576 });
2577 }
2578 for (buffer<R>* operand : {&kept.a, &kept.b, &kept.c}) {
2579 if (operand->size() > split_kept) {
2580 *operand = buffer<R> {};
2581 }
2582 }
2583 }
2584
2589 inline constexpr std::size_t plain_loop_least_width {64};
2590
2600 [[nodiscard]] inline bool plain_loop_pays(std::size_t m,
2601 std::size_t n,
2602 std::size_t k) noexcept
2603 {
2604 [[maybe_unused]] const gemm_path path {gemm_path_current()};
2605#ifdef NUMERICAL_NEON_COMPILED
2606 if (path == gemm_path::neon) {
2607 return k <= 16 || (m <= 64 && k <= 64);
2608 }
2609#endif
2610#ifdef NUMERICAL_X86_COMPILED
2611 if (path == gemm_path::avx2 || path == gemm_path::avx512) {
2612 return n >= plain_loop_least_width && ((m <= 16 && k <= 64) || (m <= 32 && k <= 32));
2613 }
2614#endif
2615 (void)m, (void)n, (void)k;
2616 return false;
2617 }
2618
2620 template <typename T>
2621 [[nodiscard]] bool plain_loop_suits(std::size_t /*m*/,
2622 std::size_t /*n*/,
2623 std::size_t /*k*/) noexcept
2624 {
2625 return false;
2626 }
2627
2629 template <>
2630 [[nodiscard]] inline bool plain_loop_suits<double>(std::size_t m,
2631 std::size_t n,
2632 std::size_t k) noexcept
2633 {
2634 return plain_loop_pays(m, n, k);
2635 }
2636
2639 template <typename T>
2640 void gemm_large(std::size_t m,
2641 std::size_t n,
2642 std::size_t k,
2643 const T* a,
2644 std::size_t lda,
2645 const T* b,
2646 std::size_t ldb,
2647 T* c,
2648 std::size_t ldc)
2649 {
2650 if (plain_loop_suits<T>(m, n, k)) { // the plain loop rounds as the tiles do: the same bits either way
2651 gemm_rows(m, n, k, a, lda, b, ldb, c, ldc);
2652 return;
2653 }
2654 if (!gemm_elsewhere(m, n, k, a, lda, b, ldb, c, ldc)) {
2655 gemm_packed<vector_tile_kernel<T>>(m, n, k, a, lda, b, ldb, c, ldc);
2656 }
2657 }
2658
2659#ifdef NUMERICAL_X86_COMPILED
2661 template <split_complex Z,
2662 typename Double,
2663 typename Float>
2664 using x86_complex_kernel = std::conditional_t<std::is_same_v<Z, std::complex<double>>, Double, Float>;
2665#endif
2666
2670 template <typename B,
2671 typename A,
2672 split_complex T>
2673 void gemm_large_complex(std::size_t m,
2674 std::size_t n,
2675 std::size_t k,
2676 const T* a,
2677 std::size_t lda,
2678 const T* b,
2679 std::size_t ldb,
2680 T* c,
2681 std::size_t ldc)
2682 {
2683 const gemm_path path {gemm_path_current()};
2684 if (path == gemm_path::portable) {
2685 gemm_packed<vector_tile_kernel<T>, T, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2686 return;
2687 }
2688#ifdef NUMERICAL_X86_COMPILED
2689 if (path == gemm_path::avx512) { // a complex tile of its own on the x86-64 units
2690 gemm_packed<x86_complex_kernel<T, avx512_complex_tile_kernel, avx512_complex_tile_kernel_f32>, T, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2691 return;
2692 }
2693 if (path == gemm_path::avx2) {
2694 gemm_packed<x86_complex_kernel<T, avx2_complex_tile_kernel, avx2_complex_tile_kernel_f32>, T, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2695 return;
2696 }
2697#endif
2698 gemm_split<T, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2699 }
2700
2702 template <split_complex T>
2703 void gemm_large(std::size_t m,
2704 std::size_t n,
2705 std::size_t k,
2706 const T* a,
2707 std::size_t lda,
2708 const T* b,
2709 std::size_t ldb,
2710 T* c,
2711 std::size_t ldc)
2712 {
2713 gemm_large_complex<b_rows, a_rows>(m, n, k, a, lda, b, ldb, c, ldc);
2714 }
2715
2739 template <typename T>
2740 void gemm(std::size_t m,
2741 std::size_t n,
2742 std::size_t k,
2743 const T* a,
2744 std::size_t lda,
2745 const T* b,
2746 std::size_t ldb,
2747 T* c,
2748 std::size_t ldc)
2749 {
2750 if constexpr (split_complex<T>) {
2751 if (gemm_path_current() != gemm_path::portable) {
2752 if (m != 0 && n != 0 && k != 0) {
2753 gemm_large(m, n, k, a, lda, b, ldb, c, ldc);
2754 }
2755 return;
2756 }
2757 }
2758 if (k < gemm_min_depth || m * n < ((gemm_packed_threshold + k) - 1) / k) {
2759 gemm_rows(m, n, k, a, lda, b, ldb, c, ldc);
2760 return;
2761 }
2762 gemm_large(m, n, k, a, lda, b, ldb, c, ldc);
2763 }
2764
2766 template <typename B,
2767 typename A>
2768 void gemm_large_layouts(std::size_t m,
2769 std::size_t n,
2770 std::size_t k,
2771 const double* a,
2772 std::size_t lda,
2773 const double* b,
2774 std::size_t ldb,
2775 double* c,
2776 std::size_t ldc)
2777 {
2778 if (!gemm_double_elsewhere<B, A>(m, n, k, a, lda, b, ldb, c, ldc)) {
2779 gemm_packed<vector_tile_kernel<double>, double, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2780 }
2781 }
2782
2784 template <typename B,
2785 typename A>
2786 void gemm_large_layouts(std::size_t m,
2787 std::size_t n,
2788 std::size_t k,
2789 const float* a,
2790 std::size_t lda,
2791 const float* b,
2792 std::size_t ldb,
2793 float* c,
2794 std::size_t ldc)
2795 {
2796 if (!gemm_float_elsewhere<B, A>(m, n, k, a, lda, b, ldb, c, ldc)) {
2797 gemm_packed<vector_tile_kernel<float>, float, B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2798 }
2799 }
2800
2802 template <typename B,
2803 typename A,
2804 split_complex T>
2805 void gemm_large_layouts(std::size_t m,
2806 std::size_t n,
2807 std::size_t k,
2808 const T* a,
2809 std::size_t lda,
2810 const T* b,
2811 std::size_t ldb,
2812 T* c,
2813 std::size_t ldc)
2814 {
2815 gemm_large_complex<B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2816 }
2817
2826 template <typename A,
2827 typename B,
2828 typename T>
2829 void gemm_layouts(std::size_t m,
2830 std::size_t n,
2831 std::size_t k,
2832 const T* a,
2833 std::size_t lda,
2834 const T* b,
2835 std::size_t ldb,
2836 T* c,
2837 std::size_t ldc)
2838 {
2839 if constexpr (split_complex<T>) {
2840 if (gemm_path_current() != gemm_path::portable) {
2841 if (m != 0 && n != 0 && k != 0) {
2842 gemm_large_complex<B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2843 }
2844 return;
2845 }
2846 }
2847 if (k < gemm_min_depth || m * n < ((gemm_packed_threshold + k) - 1) / k) {
2848 buffer<T> a_rows_copy;
2849 buffer<T> b_rows_copy;
2850 std::size_t a_ld {};
2851 std::size_t b_ld {};
2852 const T* const left {A::rows_of(a, lda, m, k, a_rows_copy, a_ld)};
2853 const T* const right {B::rows_of(b, ldb, k, n, b_rows_copy, b_ld)};
2854 gemm_rows(m, n, k, left, a_ld, right, b_ld, c, ldc);
2855 return;
2856 }
2857 gemm_large_layouts<B, A>(m, n, k, a, lda, b, ldb, c, ldc);
2858 }
2859
2861 template <typename T>
2862 void gemm_bt(std::size_t m,
2863 std::size_t n,
2864 std::size_t k,
2865 const T* a,
2866 std::size_t lda,
2867 const T* bt,
2868 std::size_t ldbt,
2869 T* c,
2870 std::size_t ldc)
2871 {
2872 gemm_layouts<a_rows, b_transposed>(m, n, k, a, lda, bt, ldbt, c, ldc);
2873 }
2874
2876 template <typename T>
2877 void gemm_at(std::size_t m,
2878 std::size_t n,
2879 std::size_t k,
2880 const T* at,
2881 std::size_t ldat,
2882 const T* b,
2883 std::size_t ldb,
2884 T* c,
2885 std::size_t ldc)
2886 {
2887 gemm_layouts<a_transposed, b_rows>(m, n, k, at, ldat, b, ldb, c, ldc);
2888 }
2889
2891 template <typename T>
2892 void gemm_atbt(std::size_t m,
2893 std::size_t n,
2894 std::size_t k,
2895 const T* at,
2896 std::size_t ldat,
2897 const T* bt,
2898 std::size_t ldbt,
2899 T* c,
2900 std::size_t ldc)
2901 {
2902 gemm_layouts<a_transposed, b_transposed>(m, n, k, at, ldat, bt, ldbt, c, ldc);
2903 }
2904
2906 enum class left_kernel : unsigned char
2907 {
2908 plain,
2909 vector,
2910 neon,
2911 amx,
2912 avx512,
2913 avx2
2914 };
2915
2918 template <typename T>
2920 {
2922 std::size_t m;
2923 std::size_t k;
2924 const T* a;
2925 std::size_t lda;
2926 const T* packed;
2927 std::size_t granule;
2928 };
2929
2931 template <typename T>
2932 [[nodiscard]] packed_left<T> read_in_place(std::size_t m,
2933 std::size_t k,
2934 const T* a,
2935 std::size_t lda) noexcept
2936 {
2937 return packed_left<T> {.kernel = left_kernel::plain, .m = m, .k = k, .a = a, .lda = lda, .packed = nullptr, .granule = 1};
2938 }
2939
2944 template <typename T>
2945 [[nodiscard]] packed_left<T> left_rows(const packed_left<T>& left,
2946 std::size_t r0,
2947 std::size_t rows) noexcept
2948 {
2949 packed_left<T> part {left};
2950 part.m = rows;
2951 part.a = left.a + (r0 * left.lda);
2952 part.packed = left.packed == nullptr ? nullptr : left.packed + (r0 * left.k);
2953 return part;
2954 }
2955
2958 template <typename T>
2959 [[nodiscard]] left_kernel left_kernel_for(std::size_t /*m*/,
2960 std::size_t /*n*/,
2961 std::size_t /*k*/) noexcept
2962 {
2963 return left_kernel::plain;
2964 }
2965
2968 template <>
2969 [[nodiscard]] inline left_kernel left_kernel_for<double>(std::size_t m,
2970 std::size_t n,
2971 std::size_t k) noexcept
2972 {
2973 if (k < gemm_min_depth || m * n < ((gemm_packed_threshold + k) - 1) / k || k > gemm_kc) {
2974 return left_kernel::plain;
2975 }
2976 const gemm_path path {gemm_path_current()};
2977#ifdef NUMERICAL_AMX_COMPILED
2978 if (path == gemm_path::amx && amx_suits(m, n, k)) {
2979 return left_kernel::amx;
2980 }
2981#endif
2982#ifdef NUMERICAL_NEON_COMPILED
2983 if (path == gemm_path::neon || path == gemm_path::amx) {
2984 return left_kernel::neon;
2985 }
2986#endif
2987#ifdef NUMERICAL_X86_COMPILED
2988 if (path == gemm_path::avx512) {
2989 return left_kernel::avx512;
2990 }
2991 if (path == gemm_path::avx2) {
2992 return left_kernel::avx2;
2993 }
2994#endif
2995 (void)path;
2996 return left_kernel::vector;
2997 }
2998
3000 template <typename Kernel,
3001 typename T>
3002 [[nodiscard]] const T* pack_left_for(std::size_t m,
3003 std::size_t k,
3004 const T* a,
3005 std::size_t lda,
3006 buffer<T>& storage)
3007 {
3008 const std::size_t a_rows {((m + Kernel::mr) - 1) / Kernel::mr * Kernel::mr};
3009 storage.resize(std::max(storage.size(), (a_rows * k) + (packed_line / sizeof(T)) + 1));
3010 T* const out {line_aligned(storage.data())};
3011 const std::size_t blocks {((m + Kernel::block_rows) - 1) / Kernel::block_rows};
3012 parallel_for(blocks, Kernel::block_rows * k * gemm_kernel_speedup / Kernel::speedup, [&](std::size_t block) {
3013 const std::size_t ic {block * Kernel::block_rows};
3014 Kernel::pack_a(a + (ic * lda), lda, std::min(Kernel::block_rows, m - ic), k, out + (ic * k));
3015 });
3016 return out;
3017 }
3018
3025 template <typename T>
3026 [[nodiscard]] packed_left<T> pack_left(std::size_t m,
3027 std::size_t /*n*/,
3028 std::size_t k,
3029 const T* a,
3030 std::size_t lda,
3031 buffer<T>& /*storage*/)
3032 {
3033 return read_in_place(m, k, a, lda);
3034 }
3035
3037 template <>
3038 [[nodiscard]] inline packed_left<double> pack_left<double>(std::size_t m,
3039 std::size_t n,
3040 std::size_t k,
3041 const double* a,
3042 std::size_t lda,
3043 buffer<double>& storage)
3044 {
3045 packed_left<double> left {.kernel = left_kernel_for<double>(m, n, k), .m = m, .k = k, .a = a, .lda = lda, .packed = nullptr, .granule = 1};
3046#ifdef NUMERICAL_AMX_COMPILED
3047 if (left.kernel == left_kernel::amx) {
3048 left.packed = pack_left_for<amx_tile_kernel>(m, k, a, lda, storage);
3049 left.granule = amx_tile_kernel::mr;
3050 }
3051#endif
3052#ifdef NUMERICAL_NEON_COMPILED
3053 if (left.kernel == left_kernel::neon) {
3054 left.packed = pack_left_for<neon_tile_kernel<double>>(m, k, a, lda, storage);
3055 left.granule = neon_tile_kernel<double>::mr;
3056 }
3057#endif
3058#ifdef NUMERICAL_X86_COMPILED
3059 if (left.kernel == left_kernel::avx512) {
3060 left.packed = pack_left_for<avx512_tile_kernel>(m, k, a, lda, storage);
3061 left.granule = avx512_tile_kernel::mr;
3062 }
3063 if (left.kernel == left_kernel::avx2) {
3064 left.packed = pack_left_for<avx2_tile_kernel>(m, k, a, lda, storage);
3065 left.granule = avx2_tile_kernel::mr;
3066 }
3067#endif
3068 if (left.kernel == left_kernel::vector) {
3069 left.packed = pack_left_for<vector_tile_kernel<double>>(m, k, a, lda, storage);
3070 left.granule = vector_tile_kernel<double>::mr;
3071 }
3072 return left;
3073 }
3074
3079 template <typename T>
3080 void gemm_with(const packed_left<T>& left,
3081 std::size_t n,
3082 const T* b,
3083 std::size_t ldb,
3084 T* c,
3085 std::size_t ldc)
3086 {
3087 gemm(left.m, n, left.k, left.a, left.lda, b, ldb, c, ldc);
3088 }
3089
3095 template <>
3096 inline void gemm_with<double>(const packed_left<double>& left,
3097 std::size_t n,
3098 const double* b,
3099 std::size_t ldb,
3100 double* c,
3101 std::size_t ldc)
3102 {
3103 if (left.kernel == left_kernel::plain || left_kernel_for<double>(left.m, n, left.k) != left.kernel) {
3104 gemm(left.m, n, left.k, left.a, left.lda, b, ldb, c, ldc);
3105 return;
3106 }
3107#ifdef NUMERICAL_AMX_COMPILED
3108 if (left.kernel == left_kernel::amx) {
3109 gemm_packed<amx_tile_kernel>(left.m, n, left.k, left.a, left.lda, b, ldb, c, ldc, left.packed);
3110 return;
3111 }
3112#endif
3113#ifdef NUMERICAL_NEON_COMPILED
3114 if (left.kernel == left_kernel::neon) {
3115 gemm_packed<neon_tile_kernel<double>>(left.m, n, left.k, left.a, left.lda, b, ldb, c, ldc, left.packed);
3116 return;
3117 }
3118#endif
3119#ifdef NUMERICAL_X86_COMPILED
3120 if (left.kernel == left_kernel::avx512) {
3121 gemm_packed<avx512_tile_kernel>(left.m, n, left.k, left.a, left.lda, b, ldb, c, ldc, left.packed);
3122 return;
3123 }
3124 if (left.kernel == left_kernel::avx2) {
3125 gemm_packed<avx2_tile_kernel>(left.m, n, left.k, left.a, left.lda, b, ldb, c, ldc, left.packed);
3126 return;
3127 }
3128#endif
3129 gemm_packed<vector_tile_kernel<double>>(left.m, n, left.k, left.a, left.lda, b, ldb, c, ldc, left.packed);
3130 }
3131
3133 inline constexpr std::size_t dot_chunk {std::size_t {1} << 14U};
3134
3141 template <typename T>
3142 [[nodiscard]] T chunked_dot(const T* a,
3143 const T* b,
3144 std::size_t count)
3145 {
3146 const std::size_t chunks {(count + dot_chunk - 1) / dot_chunk};
3147 if (chunks <= 1) {
3148 return dot_product(a, b, count);
3149 }
3150 buffer<T> partial(chunks);
3151 parallel_for(chunks, dot_chunk, [&](std::size_t c) {
3152 const std::size_t first {c * dot_chunk};
3153 partial[c] = dot_product(a + first, b + first, std::min(dot_chunk, count - first));
3154 });
3155 T total {};
3156 for (const T& sum : partial) {
3157 total += sum;
3158 }
3159 return total;
3160 }
3161
3163 template <typename T>
3164 inline constexpr std::size_t multiply_add_weight {1};
3165
3167 template <>
3168 inline constexpr std::size_t multiply_add_weight<std::complex<double>> {4};
3169
3171 template <>
3172 inline constexpr std::size_t multiply_add_weight<std::complex<float>> {4};
3173
3175 inline constexpr std::size_t row_group {64};
3176
3177#ifdef NUMERICAL_AMX_COMPILED
3180 inline constexpr std::size_t amx_dot_least {std::size_t {1} << 15U};
3181
3185 inline constexpr std::size_t amx_dot_most {std::size_t {1} << 18U};
3186#endif
3187
3195 template <typename T>
3196 void matrix_times_vector(const T* a,
3197 std::size_t rows,
3198 std::size_t cols,
3199 const T* x,
3200 T* y)
3201 {
3202#ifdef NUMERICAL_AMX_COMPILED
3203 if constexpr (std::is_same_v<T, double>) {
3204 const std::size_t elements {rows * cols};
3205 if (cols <= dot_chunk && elements >= amx_dot_least && elements < amx_dot_most && gemm_path_current() == gemm_path::amx) {
3206 const amx_session on {}; // on the caller alone
3207 amx_dot_rows_f64(a, cols, rows, x, cols, y);
3208 return;
3209 }
3210 }
3211#endif
3212 if (cols > dot_chunk) { // rows longer than a chunk: chunked_dot's partial sums
3213 parallel_for(rows, cols, [&](std::size_t r) { y[r] = chunked_dot(a + (r * cols), x, cols); });
3214 return;
3215 }
3216 // A complex multiply-add weighs four real ones. Groups of fewer rows where the matrix has too few for each thread
3217 // to take some (189 rows of 64 were three groups: six AVX2 cores' L3 read by one, a complex matrix of 189 × 206
3218 // times a vector 24 µs); each row its own dot product whatever the grouping, so the same bits.
3219 constexpr std::size_t weight {multiply_add_weight<T>};
3220 const std::size_t group {std::clamp<std::size_t>(rows / (4 * max_threads()), 8, row_group)};
3221 parallel_for((rows + group - 1) / group, group * cols * weight, [&](std::size_t g) {
3222 const std::size_t first {g * group};
3223 row_dots(a + (first * cols), cols, std::min(group, rows - first), x, cols, y + first);
3224 });
3225 }
3226} // namespace numerical::detail
3227
3228namespace numerical {
3229
3237 {
3238 detail::gemm_path_cap().store(most, std::memory_order_relaxed);
3239 return detail::best_gemm_path(most); // probes only the paths at or below the cap
3240 }
3241
3244 {
3246 }
3247} // namespace numerical
3248
3249#endif // NUMERICAL_GEMM_HPP
The matrix coprocessor of Apple's M-series chips (AMX), for the double matrix product.
numerical::buffer, the storage of the library's vectors, matrices and tensors: a std::vector whose el...
The storage of a product's packed operands, kept by the calling thread from one product to the next.
Definition gemm.hpp:1497
packing_storage(std::size_t a_count, std::size_t b_count)
Room for a_count packed elements of A and b_count of B.
Definition gemm.hpp:1504
static buffer< T > grown(buffer< T > storage, std::size_t count)
storage with at least count elements (it only grows, so a reuse writes nothing).
Definition gemm.hpp:1545
static buffer< T > & kept() noexcept
The storage this thread keeps between products.
Definition gemm.hpp:1534
T * b() const noexcept
B's packed elements.
Definition gemm.hpp:1528
static T * aligned(T *at) noexcept
line_aligned.
Definition gemm.hpp:1553
static constexpr std::size_t padding
Elements enough to reach a line.
Definition gemm.hpp:1542
packing_storage(packing_storage &&)=delete
static constexpr std::size_t kept_limit
The most elements a thread keeps between products (32 MiB).
Definition gemm.hpp:1501
T * a() const noexcept
A's packed elements.
Definition gemm.hpp:1522
packing_storage & operator=(packing_storage &&)=delete
packing_storage(const packing_storage &)=delete
packing_storage & operator=(const packing_storage &)=delete
A dense vector of T with value semantics.
Definition vector.hpp:86
A complex number of float or double, which gemm_split multiplies through the real kernel and the kern...
Definition simd.hpp:64
#define NUMERICAL_SCALAR_DEPTH
Definition gemm.hpp:61
#define NUMERICAL_UNROLL_BLOCK
Definition gemm.hpp:69
#define NUMERICAL_OUT_OF_LINE
Definition gemm.hpp:52
constexpr std::size_t gemm_packed_threshold
Below this many multiply-adds the plain loop (gemm_rows, by blocks of rows held in registers) runs in...
Definition gemm.hpp:513
bool gemm_path_usable(gemm_path path) noexcept
Whether path can run here.
Definition gemm.hpp:99
C complex_dot_product(const C *x, const C *y, std::size_t count)
Σ x[i] · y[i] of complex numbers: on the x86-64 units with AVX2 (the AVX-512 path too) their fused ch...
Definition gemm.hpp:389
void gemm_packed(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc, const T *a_packed=nullptr)
The packed product with the tile Kernel computes: for each block of B's columns and each panel of the...
Definition gemm.hpp:1631
constexpr std::size_t split_columns
The columns of B and C, and the depth, of one real product of gemm_split: its real operands stay a fe...
Definition gemm.hpp:2378
void gemm_pack_b(const T *b, std::size_t ldb, std::size_t kc, std::size_t cols, T *out)
Copies kc × cols of B (from row-major, leading dimension ldb) into slivers of Nr columns,...
Definition gemm.hpp:709
bool plain_loop_pays(std::size_t m, std::size_t n, std::size_t k) noexcept
Whether a double product of m rows, n columns and depth k, past gemm_rows's threshold,...
Definition gemm.hpp:2600
const T * pack_left_for(std::size_t m, std::size_t k, const T *a, std::size_t lda, buffer< T > &storage)
A packed for Kernel into storage, in parallel by its blocks of rows, as gemm_packed packs it.
Definition gemm.hpp:3002
void gemm_pack_a(const T *a, std::size_t lda, std::size_t rows, std::size_t kc, T *out)
Copies rows × kc of A (from row-major, leading dimension lda) into slivers of Mr rows,...
Definition gemm.hpp:678
void gemm_triangle(std::size_t n, std::size_t k, const double *a, std::size_t lda, const double *b, std::size_t ldb, double *c, std::size_t ldc, bool lower)
The triangle of the double product C = A·B (gemm_packed_triangle) on the tile of the path in use,...
Definition gemm.hpp:2257
bool split_by_parts(std::size_t m) noexcept
Whether gemm_split runs a product of m rows of A by four real products of each block's parts (split_p...
Definition gemm.hpp:2400
void narrow_block(std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc) noexcept
C += A·B for R rows and N columns of C, each sum in ascending order of the depth, as the plain loop's...
Definition gemm.hpp:1304
constexpr bool prefetches_next_row_of_c
Whether Kernel has the C tile of the next row of tiles prefetched while a tile computes (Kernel::pref...
Definition gemm.hpp:1577
void subtract_rows(double *y, std::size_t ldy, const double *f, std::size_t ldf, const double *x, std::size_t ldx, std::size_t terms, std::size_t count)
subtract_rows for double, the units chosen as subtract_products chooses them: each element the value ...
Definition gemm.hpp:217
left_kernel
The tile a product's left operand is packed for (numerical::detail::pack_left): plain where it is not...
Definition gemm.hpp:2907
@ plain
gemm_rows, or a scalar whose products are not shared: A is read in place.
void gemm_pack_at(const T *at, std::size_t ldat, std::size_t rows, std::size_t kc, T *out)
The slivers of Mr rows of A (rows × kc), column by column, from its transpose at (kc × rows row-major...
Definition gemm.hpp:883
void gemm_rows_rounded(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc)
C += A·B by the plain i-k-j loop, parallel over the rows of C. Past a depth of 1 (narrow_least_depth)...
Definition gemm.hpp:1381
T chunked_dot(const T *a, const T *b, std::size_t count)
Σ a[i] · b[i] for a plain dot product: chunks of dot_chunk elements in parallel, each by dot_product ...
Definition gemm.hpp:3142
bool amx_usable() noexcept
Whether this chip's AMX can be used: known family and a passing self-test, decided once.
Definition amx.hpp:902
left_kernel left_kernel_for(std::size_t, std::size_t, std::size_t) noexcept
The tile gemm runs a product of m × n by depth k on, for a left operand packed once for many products...
Definition gemm.hpp:2959
constexpr std::size_t gemm_kernel_speedup
The multiply-adds the register tile does in the time of one in work_per_thread's dependent chain (abo...
Definition gemm.hpp:497
void exchange(buffer< T > &a, std::size_t n, std::size_t i, std::size_t j)
Exchanges rows i and j, then columns i and j, of the row-major n × n a: a similarity.
Definition eigen.hpp:106
void gemm_large_complex(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc)
A complex product past the plain loop: on the vector tile on the portable path, whole complex product...
Definition gemm.hpp:2673
T operator_times(const T &a, const T &b)
a · b, as std::operator* computes it, in line. libc++'s complex product (the four products and their ...
Definition simd.hpp:772
constexpr std::size_t vector_speedup
The multiply-adds a vectorised inner loop does in the time of one in work_per_thread's dependent chai...
Definition parallel.hpp:544
void gemm_at(std::size_t m, std::size_t n, std::size_t k, const T *at, std::size_t ldat, const T *b, std::size_t ldb, T *c, std::size_t ldc)
gemm_layouts with A given as Aᵀ (at, k × m, ldat apart).
Definition gemm.hpp:2877
constexpr std::size_t gemm_mr
The rows of C one register tile covers.
Definition gemm.hpp:474
constexpr std::size_t narrow_group
The rows of C one block of gemm_rows takes together: each element a chain of multiply-adds that waits...
Definition gemm.hpp:1211
void add_product(double *y, const double &f, const double *x, std::size_t count)
y[i] += f · x[i] for double, as subtract_product.
Definition gemm.hpp:308
gemm_path best_gemm_path(gemm_path most) noexcept
The most capable path that can run here, most at the highest.
Definition gemm.hpp:113
void row_dots(const std::complex< double > *m, std::size_t ld, std::size_t rows, const std::complex< double > *x, std::size_t count, std::complex< double > *out)
row_dots for std::complex<double>, through complex_row_dots.
Definition gemm.hpp:432
void subtract_products(double *y, const double *f, const double *x, std::size_t ldx, std::size_t terms, std::size_t count)
y[i] -= Σₜ f[t] · x[(t · ldx) + i] for double, the units chosen as subtract_product chooses them,...
Definition gemm.hpp:194
constexpr std::size_t dot_chunk
The elements of one partial sum of chunked_dot: fixed, so the sum does not depend on the threads.
Definition gemm.hpp:3133
void gemm_with(const packed_left< T > &left, std::size_t n, const T *b, std::size_t ldb, T *c, std::size_t ldc)
C += A·B for the packed left (pack_left) and the k × n b, as gemm computes it, A read in place here,...
Definition gemm.hpp:3080
bool gemm_elsewhere(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc)
A product a faster path than the vector one takes, if any: none for this scalar.
Definition gemm.hpp:2194
void gemm_bt(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *bt, std::size_t ldbt, T *c, std::size_t ldc)
gemm_layouts with B given as Bᵀ (bt, n × k, ldbt apart).
Definition gemm.hpp:2862
const T * rows_from_transpose(const T *x, std::size_t ld, std::size_t rows, std::size_t cols, buffer< T > &scratch, std::size_t &ld_out)
A row-major rows × cols operand for the plain loop, copied into scratch from its transpose x (cols × ...
Definition gemm.hpp:763
void gemm_split(std::size_t m, std::size_t n, std::size_t k, const Z *a, std::size_t lda, const Z *b, std::size_t ldb, Z *c, std::size_t ldc)
C += A·B for complex numbers through the real kernel, by blocks of split_columns columns and split_de...
Definition gemm.hpp:2534
void subtract_product(double *y, const double &f, const double *x, std::size_t count)
y[i] −= f · x[i] for double on the widest x86-64 units the matrix product's kernel may use (gemm_path...
Definition gemm.hpp:163
constexpr std::size_t plain_loop_least_width
The fewest columns of a product plain_loop_pays sends to the plain loop on the x86-64 tiles: there an...
Definition gemm.hpp:2589
packed_left< T > pack_left(std::size_t m, std::size_t, std::size_t k, const T *a, std::size_t lda, buffer< T > &)
The m × k a packed once for products with it of about n columns each (gemm_with), in storage (grown a...
Definition gemm.hpp:3026
constexpr std::size_t split_kept
The most elements an operand of gemm_split keeps from one product to the next on a thread: 2 MiB of d...
Definition gemm.hpp:2385
T rounded_apart_add(T sum, T a, T b) noexcept
sum + a·b as the vector tile rounds it (vector_tile_kernel), so that the plain loop's elements are it...
Definition gemm.hpp:1186
bool plain_loop_suits(std::size_t, std::size_t, std::size_t) noexcept
plain_loop_pays for a product of T: double's alone, the one it was measured for.
Definition gemm.hpp:2621
constexpr std::size_t tasks_per_thread
The tasks a product's compute pass hands each thread at least, while its rows allow: a slower core th...
Definition gemm.hpp:492
constexpr std::size_t split_depth
The depth of one real product of gemm_split (split_columns).
Definition gemm.hpp:2381
T * line_aligned(T *at) noexcept
The first element of at's run that starts a cache line (or at's nearest element towards one,...
Definition gemm.hpp:1479
std::atomic< std::uint8_t > & gemm_path_found() noexcept
The path gemm_path_current found last, packed with the cap it was found under (cap · 8 + path); 0xFF ...
Definition gemm.hpp:133
constexpr bool gemm_shares(std::size_t m, std::size_t n, std::size_t k) noexcept
Whether the product's threads pay: below Kernel's serial_below multiply-adds, the product runs on the...
Definition gemm.hpp:1566
constexpr bool neon_compiled
Whether the NEON path's tile is compiled in: on every arm64 target GCC or Clang builds.
Definition gemm.hpp:95
void parallel_for(std::size_t count, std::size_t work_per_index, const Body &body)
Runs body(i) for every i in [0, count).
Definition parallel.hpp:580
void gemm_large(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc)
A product past the plain loop: on the AMX or x86-64 tiles where gemm_elsewhere takes it,...
Definition gemm.hpp:2640
constexpr std::size_t gemm_nr
The columns of C one register tile covers: 96 bytes of elements (12 double, 24 float),...
Definition gemm.hpp:479
constexpr std::size_t gemm_mc
The rows of one task: an A block (gemm_mc × gemm_kc) stays in the L2 cache.
Definition gemm.hpp:485
void split_expanded_block(std::size_t m, std::size_t nb, std::size_t kb, std::size_t pc, std::size_t jc, const Z *a, std::size_t lda, const Z *b, std::size_t ldb, typename Z::value_type *ar, typename Z::value_type *br, typename Z::value_type *cr)
One block of gemm_split (split_parts_block's) by one real product of its expanded operands,...
Definition gemm.hpp:2488
gemm_path gemm_path_current() noexcept
The path the next double product takes: the most capable one under the cap that runs here (each path'...
Definition gemm.hpp:145
void gemm_large_layouts(std::size_t m, std::size_t n, std::size_t k, const double *a, std::size_t lda, const double *b, std::size_t ldb, double *c, std::size_t ldc)
A double product past the plain loop, B and A laid out as B and A say (gemm_layouts).
Definition gemm.hpp:2768
std::integral_constant< term_rounding, Mode > rounding_is
The rounding mode Mode as a type, which picks rounded_term's overload.
Definition gemm.hpp:1225
constexpr std::size_t packed_line
The cache line a packed operand starts.
Definition gemm.hpp:1474
constexpr std::size_t narrow_columns
The columns of C one block of gemm_rows keeps in registers over the whole depth.
Definition gemm.hpp:1178
const T * rows_in_place(const T *x, std::size_t ld, std::size_t &ld_out) noexcept
A row-major rows × cols operand for the plain loop, x itself (ld apart).
Definition gemm.hpp:752
T rounded_term(rounding_is< term_rounding::compiler >, T sum, T a, T b) noexcept
sum + a·b as the compiler rounds it (rounded_apart_add).
Definition gemm.hpp:1229
std::array< T, gemm_mr *gemm_nr< T > > gemm_tile
Definition gemm.hpp:522
constexpr std::size_t narrow_least_depth
The least depth at which rows of C wider than one block (narrow_columns) run by blocks in gemm_rows; ...
Definition gemm.hpp:1174
packed_left< T > left_rows(const packed_left< T > &left, std::size_t r0, std::size_t rows) noexcept
The rows [r0, r0 + rows) of left, r0 a multiple of left.granule: their slivers in its packing,...
Definition gemm.hpp:2945
constexpr std::size_t gemm_kc
The depth of one panel: a B sliver (gemm_kc × gemm_nr) stays in the L1 cache.
Definition gemm.hpp:482
void gemm_pack_bt(const T *bt, std::size_t ldbt, std::size_t kc, std::size_t cols, T *out)
gemm_pack_b of B = Bᵀᵀ, from the row-major bt (cols × kc, leading dimension ldbt): each column of a s...
Definition gemm.hpp:732
double dot_product(const double *x, const double *y, std::size_t count)
Σ x[i] · y[i] for double, as subtract_product chooses its units (in another order of summation there,...
Definition gemm.hpp:369
void complex_scaled_add(C &y, const C &f, const C &x)
y += f · x of one complex number, its part products rounded apart as libc++'s operator computes them ...
Definition simd.hpp:941
void split_parts_block(std::size_t m, std::size_t nb, std::size_t kb, std::size_t pc, std::size_t jc, const Z *a, std::size_t lda, const Z *b, std::size_t ldb, typename Z::value_type *ar, typename Z::value_type *br, typename Z::value_type *cr)
One block of gemm_split ([Re C | Im C] at cr, m × nb each, += A·B, A m × kb and B kb × nb) by four re...
Definition gemm.hpp:2443
constexpr std::size_t row_group
The rows a thread takes of a matrix times a vector (matrix_times_vector).
Definition gemm.hpp:3175
void narrow_rows(std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc) noexcept
C += A·B for R whole rows of C of n columns: blocks of narrow_columns, the last narrower.
Definition gemm.hpp:1344
constexpr std::size_t split_parts_rows
The most rows of A for which gemm_split runs four real products of a block's parts on the AMX path (s...
Definition gemm.hpp:2391
constexpr std::size_t task_rows_cap(std::size_t) noexcept
The most rows of a task of Kernel over a panel of depth kc: its block of rows.
Definition gemm.hpp:1601
packed_left< T > read_in_place(std::size_t m, std::size_t k, const T *a, std::size_t lda) noexcept
The m × k a read in place by gemm_with, unpacked: each product packs it as gemm does.
Definition gemm.hpp:2932
constexpr std::size_t gemm_min_depth
Below this depth the plain loop runs too: each panel reads and writes its tiles of C once,...
Definition gemm.hpp:519
split_operands< R > & split_storage()
The calling thread's split_operands. A product uses them whole on one thread, and the real product it...
Definition gemm.hpp:2427
void gemm_kernel(std::size_t kc, const T *ap, const T *bp, std::size_t ldb, gemm_tile< T > &tile)
tile += Aₚ·Bₚ over one panel, for any scalar.
Definition gemm.hpp:533
void gemm_rows(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc)
C += A·B by the plain i-k-j loop (gemm_rows_rounded), each element rounded as the tile of the path in...
Definition gemm.hpp:1445
void gemm_packed_triangle(std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc, bool lower)
gemm_packed restricted to a triangle of the square C = A·B (n × n): its lower one (j ≤ i) where lower...
Definition gemm.hpp:1720
bool gemm_float_elsewhere(std::size_t m, std::size_t n, std::size_t k, const float *a, std::size_t lda, const float *b, std::size_t ldb, float *c, std::size_t ldc)
A float product on the AMX where it suits the product, on the NEON tile on arm64's fused paths,...
Definition gemm.hpp:2313
constexpr std::size_t gemm_nc
The columns of B packed at once, a whole number of slivers.
Definition gemm.hpp:501
std::atomic< gemm_path > & gemm_path_cap() noexcept
The most capable path products may take, as numerical::limit_gemm_path set it: none is probed until a...
Definition gemm.hpp:125
bool rows_fuse() noexcept
Whether the tile a double or float product past the plain loop runs on fuses each term (gemm_rows rou...
Definition gemm.hpp:1430
constexpr std::size_t gemm_slivers_per_task
The B slivers of one task.
Definition gemm.hpp:488
void prefetch_next_row(const T *, std::size_t) noexcept
Nothing, for a tile that prefetches no C (prefetches_next_row_of_c).
Definition gemm.hpp:1584
constexpr std::size_t multiply_add_weight
The real multiply-adds one multiply-add of T weighs: 1 for a real number.
Definition gemm.hpp:3164
void gemm_layouts(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc)
gemm with either operand given as its transpose (A: a_rows or a_transposed, B: b_rows or b_transposed...
Definition gemm.hpp:2829
void gemm_atbt(std::size_t m, std::size_t n, std::size_t k, const T *at, std::size_t ldat, const T *bt, std::size_t ldbt, T *c, std::size_t ldc)
gemm_layouts with both operands given as their transposes.
Definition gemm.hpp:2892
void complex_row_dots(const C *m, std::size_t ld, std::size_t rows, const C *x, std::size_t count, C *out)
row_dots of complex numbers: each row by complex_dot_product, as chunked_dot sums a row.
Definition gemm.hpp:419
term_rounding
How numerical::detail::gemm_rows rounds each term sum + a·b, as the tile of the path in use does: as ...
Definition gemm.hpp:1217
@ x86_fused
std::fma in functions built for AVX2 with FMA.
@ fused
std::fma, where every target has the instruction.
@ compiler
As the compiler rounds sum + a·b (rounded_apart_add).
bool gemm_double_elsewhere(std::size_t m, std::size_t n, std::size_t k, const double *a, std::size_t lda, const double *b, std::size_t ldb, double *c, std::size_t ldc)
A double product on the path in use (numerical::gemm_path_in_use), unless it is the portable one,...
Definition gemm.hpp:2213
void matrix_times_vector(const T *a, std::size_t rows, std::size_t cols, const T *x, T *y)
y = A x for the row-major rows × cols a: each element the dot product of its row with x,...
Definition gemm.hpp:3196
The NumericAL public API.
Definition amx.hpp:44
std::vector< T, detail::default_init_allocator< T > > buffer
The storage of vector, matrix and tensor, namely a std::vector whose buffer<T>(n) and resize(n) leave...
Definition buffer.hpp:157
std::size_t max_threads()
The threads a kernel may use: the cap if one is set, else the hardware threads (starting none).
Definition parallel.hpp:732
T sum(const tensor< T > &t)
The sum of every element, by pairwise halving (0 for an empty tensor).
gemm_path gemm_path_in_use() noexcept
The path double matrix products take.
Definition gemm.hpp:3243
gemm_path limit_gemm_path(gemm_path most) noexcept
Keeps double matrix products on most or a more portable path: the most capable path at or below it th...
Definition gemm.hpp:3236
gemm_path
The kernels a double matrix product can run on (a float one too on arm64), from the most portable....
Definition gemm.hpp:80
@ avx512
AVX-512 with fused multiply-adds.
@ portable
The compiler's vectors (16 bytes on x86-64 wheels), as the compiler rounds c + a·b (see gemm).
@ avx2
AVX2 with fused multiply-adds.
@ amx
The Apple AMX coprocessor, fused multiply-adds; the products it does not suit on the NEON path's tile...
@ neon
arm64's vectors with fused multiply-adds, for double and float.
A minimal std::thread parallel-for for the hot kernels.
The native vector of float and double the kernels hold their lanes in (GNU vector extensions,...
#define NUMERICAL_ALWAYS_INLINE
Definition simd.hpp:22
A as gemm_packed reads it by default: row-major, m × k, its rows lda apart, packed by the tile.
Definition gemm.hpp:903
static void pack(const T *a, std::size_t lda, std::size_t i0, std::size_t p0, std::size_t rows, std::size_t kc, T *out)
The rows from i0 of A, rows × kc from column p0, packed for Kernel.
Definition gemm.hpp:907
static const T & at(const T *a, std::size_t lda, std::size_t i, std::size_t p) noexcept
A's element in row i and column p.
Definition gemm.hpp:932
static const T * rows_of(const T *a, std::size_t lda, std::size_t, std::size_t, buffer< T > &, std::size_t &ld_out)
A row-major for the plain loop: itself.
Definition gemm.hpp:920
A given as Aᵀ: row-major, k × m, its rows lda apart (C = Aᵀᵀ·B, no copy of the transpose).
Definition gemm.hpp:943
static const T * rows_of(const T *at, std::size_t ldat, std::size_t rows, std::size_t cols, buffer< T > &scratch, std::size_t &ld_out)
A row-major for the plain loop, copied from at into scratch.
Definition gemm.hpp:961
static const T & at(const T *at_, std::size_t ldat, std::size_t i, std::size_t p) noexcept
A's element in row i and column p: Aᵀ's in row p and column i.
Definition gemm.hpp:973
static void pack(const T *at, std::size_t ldat, std::size_t i0, std::size_t p0, std::size_t rows, std::size_t kc, T *out)
The rows from i0 of A, rows × kc from column p0, packed as Kernel lays them out (gemm_pack_at).
Definition gemm.hpp:948
B as gemm_packed reads it by default: row-major, k × n, its rows ldb apart.
Definition gemm.hpp:782
static void pack(const T *b, std::size_t ldb, std::size_t p0, std::size_t j0, std::size_t kc, std::size_t cols, T *out)
The kc × cols block of B from row p0 and column j0 into slivers of Nr columns.
Definition gemm.hpp:786
static constexpr bool in_place(std::size_t ldb) noexcept
Whether Kernel reads B's whole slivers in place.
Definition gemm.hpp:799
static const T * rows_of(const T *x, std::size_t ld, std::size_t, std::size_t, buffer< T > &, std::size_t &ld_out)
The rows × cols operand row-major for the plain loop: x itself, ld apart.
Definition gemm.hpp:806
static const T & at(const T *b, std::size_t ldb, std::size_t p, std::size_t j) noexcept
B's element in row p and column j.
Definition gemm.hpp:818
B given as Bᵀ: row-major, n × k, its rows ldb apart (C = A·Bᵀᵀ, no copy of the transpose).
Definition gemm.hpp:829
static const T & at(const T *bt, std::size_t ldbt, std::size_t p, std::size_t j) noexcept
B's element in row p and column j: Bᵀ's in row j and column p.
Definition gemm.hpp:867
static constexpr bool in_place(std::size_t) noexcept
Never in place: a sliver's columns lie apart.
Definition gemm.hpp:847
static const T * rows_of(const T *x, std::size_t ld, std::size_t rows, std::size_t cols, buffer< T > &scratch, std::size_t &ld_out)
The rows × cols operand row-major for the plain loop, copied from its transpose x (ld apart) into scr...
Definition gemm.hpp:855
static void pack(const T *bt, std::size_t ldbt, std::size_t p0, std::size_t j0, std::size_t kc, std::size_t cols, T *out)
The kc × cols block of B from row p0 and column j0 (Bᵀ's rows from j0, its columns from p0) into sliv...
Definition gemm.hpp:834
A left operand packed once for several products by it (an LU's −L₂₁ against blocks of its U₁₂): the t...
Definition gemm.hpp:2920
std::size_t lda
A's row distance.
Definition gemm.hpp:2925
std::size_t k
A's columns, the depth.
Definition gemm.hpp:2923
const T * packed
A packed for the tile (null where plain).
Definition gemm.hpp:2926
std::size_t m
A's rows.
Definition gemm.hpp:2922
const T * a
A, row-major.
Definition gemm.hpp:2924
left_kernel kernel
The tile.
Definition gemm.hpp:2921
std::size_t granule
The rows of the tile's slivers: a block of rows from a multiple of it reads the packing.
Definition gemm.hpp:2927
The real operands of gemm_split, kept by each thread from one product to the next (split_storage): ma...
Definition gemm.hpp:2416
buffer< R > a
A's parts for a block of depth: [Re A | −Im A | Im A], or [Re A | −Im A].
Definition gemm.hpp:2417
Nothing to turn on around a task.
Definition gemm.hpp:1003
The vector path's tile: gemm_mr × gemm_nr elements of C in registers (gemm_kernel),...
Definition gemm.hpp:988
static constexpr bool reads_b_in_place(std::size_t ldb) noexcept
Whether B's rows ldb apart are read in place: no farther apart than a packed sliver's (and a B that o...
Definition gemm.hpp:1007
static constexpr std::size_t speedup
The weight of its work (gemm_kernel_speedup).
Definition gemm.hpp:998
static constexpr std::size_t b_in_place_rows
The most rows of A for which gemm_packed reads B in place at any distance between its rows (where rea...
Definition gemm.hpp:997
static constexpr std::size_t depth
The panel depth.
Definition gemm.hpp:991
static void tile(std::size_t kc, const T *ap, const T *bp, std::size_t ldb, T *c, std::size_t ldc, std::size_t rows, std::size_t cols)
C += Aₚ·Bₚ for one tile of C (rows × cols) over a panel of depth kc, B's rows ldb apart.
Definition gemm.hpp:1023
static constexpr std::size_t mr
The tile's rows.
Definition gemm.hpp:989
static constexpr std::size_t serial_below
Every product with work shares it (gemm_shares).
Definition gemm.hpp:999
static constexpr std::size_t nr
The tile's columns.
Definition gemm.hpp:990
static void pack_a(const T *a, std::size_t lda, std::size_t rows, std::size_t kc, T *out)
A's slivers for this tile (gemm_pack_a).
Definition gemm.hpp:1013
static constexpr std::size_t block_rows
Definition gemm.hpp:992
The x86-64 tiles of the double matrix product: AVX-512 and AVX2 with FMA, chosen at run time.