|
NumericAL
A header-only C++20 lexer built on REAL
|
The matrix product kernel behind matrix * matrix and the einsum matmul path.
More...
#include <algorithm>#include <array>#include <bit>#include <cmath>#include <atomic>#include <complex>#include <cstddef>#include <cstdint>#include <cstring>#include <memory>#include <type_traits>#include <utility>#include <vector>#include "parallel.hpp"#include "amx.hpp"#include "buffer.hpp"#include "simd.hpp"#include "x86.hpp"Go to the source code of this file.
Classes | |
| struct | numerical::detail::b_rows |
B as gemm_packed reads it by default: row-major, k × n, its rows ldb apart. More... | |
| struct | numerical::detail::b_transposed |
B given as Bᵀ: row-major, n × k, its rows ldb apart (C = A·Bᵀᵀ, no copy of the transpose). More... | |
| struct | numerical::detail::a_rows |
A as gemm_packed reads it by default: row-major, m × k, its rows lda apart, packed by the tile. More... | |
| struct | numerical::detail::a_transposed |
A given as Aᵀ: row-major, k × m, its rows lda apart (C = Aᵀᵀ·B, no copy of the transpose). More... | |
| struct | numerical::detail::vector_tile_kernel< T > |
The vector path's tile: gemm_mr × gemm_nr elements of C in registers (gemm_kernel), each product rounded before its sum unless the compiler contracts c + a·b into a fused multiply-add. More... | |
| struct | numerical::detail::vector_tile_kernel< T >::session |
| Nothing to turn on around a task. More... | |
| class | numerical::detail::packing_storage< T > |
| The storage of a product's packed operands, kept by the calling thread from one product to the next. More... | |
| struct | numerical::detail::split_operands< R > |
The real operands of gemm_split, kept by each thread from one product to the next (split_storage): mapping their pages anew took a third to a half of a complex product of order 64 to 256 on one x86-64 core (six AVX2 cores, all threads: order 128 0.52 → 0.17 ms), 15–20 % of one of order 256 on arm64. More... | |
| struct | numerical::detail::packed_left< T > |
A left operand packed once for several products by it (an LU's −L₂₁ against blocks of its U₁₂): the tile chosen and A packed for it (gemm_packed's layout of one panel), or A to read in place (plain). More... | |
Namespaces | |
| namespace | numerical |
| The NumericAL public API. | |
| namespace | numerical::detail |
Macros | |
| #define | NUMERICAL_OUT_OF_LINE [[gnu::noinline]] |
| #define | NUMERICAL_SCALAR_DEPTH |
| #define | NUMERICAL_UNROLL_BLOCK |
Typedefs | |
| template<typename T > | |
| using | numerical::detail::gemm_tile = std::array< T, gemm_mr *gemm_nr< T > > |
| template<term_rounding Mode> | |
| using | numerical::detail::rounding_is = std::integral_constant< term_rounding, Mode > |
The rounding mode Mode as a type, which picks rounded_term's overload. | |
Enumerations | |
| enum class | numerical::gemm_path : std::uint8_t { numerical::portable = 0 , numerical::neon = 1 , numerical::avx2 = 2 , numerical::avx512 = 3 , numerical::amx = 4 } |
The kernels a double matrix product can run on (a float one too on arm64), from the most portable. Each is used only where the processor has it: NEON on every arm64 chip, the AVX ones where cpuid reports them, the Apple AMX on an M1 to M4 chip whose self-test passes (see x86.hpp and amx.hpp). More... | |
| enum class | numerical::detail::term_rounding : unsigned char { numerical::detail::compiler , numerical::detail::fused , numerical::detail::x86_fused } |
How numerical::detail::gemm_rows rounds each term sum + a·b, as the tile of the path in use does: as the compiler rounds it (the vector tile's way), one fused multiply-add (arm64's NEON and AMX tiles), or one fused multiply-add of AVX2's unit (the x86-64 tiles), in code built for any x86-64 processor. More... | |
| enum class | numerical::detail::left_kernel : unsigned char { numerical::detail::plain , numerical::detail::vector , numerical::detail::neon , numerical::detail::amx , numerical::detail::avx512 , numerical::detail::avx2 } |
The tile a product's left operand is packed for (numerical::detail::pack_left): plain where it is not packed. More... | |
Functions | |
| bool | numerical::detail::gemm_path_usable (gemm_path path) noexcept |
Whether path can run here. | |
| gemm_path | numerical::detail::best_gemm_path (gemm_path most) noexcept |
The most capable path that can run here, most at the highest. | |
| std::atomic< gemm_path > & | numerical::detail::gemm_path_cap () noexcept |
| The most capable path products may take, as numerical::limit_gemm_path set it: none is probed until a product (or a question) reaches it, so a cap below the AMX keeps its instructions from ever running. | |
| std::atomic< std::uint8_t > & | numerical::detail::gemm_path_found () noexcept |
The path gemm_path_current found last, packed with the cap it was found under (cap · 8 + path); 0xFF before any. | |
| gemm_path | numerical::detail::gemm_path_current () noexcept |
The path the next double product takes: the most capable one under the cap that runs here (each path's probe is made once). Kept with its cap, one load, where each call walked the paths down from the cap through their probes' guards: every small step of a decomposition asks (an LU of order 100 through dgetrf_ on one AVX2 core spent 5 % of its time here). A race stores the same value twice. | |
| void | numerical::detail::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_current), each element one fused multiply-add there; elsewhere subtract_product's template. The decompositions' row updates take it: a wheel built for the baseline x86-64 holds two double per vector and fuses none. | |
| void | numerical::detail::subtract_product (double *y, const double *x, const double &f, std::size_t count) |
y[i] −= x[i] · f for double, as the other (a real product is the same either way round). | |
| void | numerical::detail::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, the elements held in registers through the terms there. | |
| void | numerical::detail::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 subtract_products gives it. | |
| void | numerical::detail::subtract_products (std::complex< double > *y, const std::complex< double > *f, const std::complex< double > *x, std::size_t ldx, std::size_t terms, std::size_t count) |
subtract_products for std::complex<double>: on AVX-512 or AVX2 with FMA where the matrix product's kernel runs there, each part of each term one fused multiply-add (Re f · x, then Im f times the swapped parts), the elements in registers through the terms; elsewhere the template, whole complex products as the compiler rounds them. | |
| void | numerical::detail::subtract_product (std::complex< double > *y, const std::complex< double > &f, const std::complex< double > *x, std::size_t count) |
y[i] −= f · x[i] for std::complex<double>: one term of subtract_products on AVX-512 or AVX2 where the matrix product's kernel runs there, else the template. | |
| void | numerical::detail::subtract_product (std::complex< double > *y, const std::complex< double > *x, const std::complex< double > &f, std::size_t count) |
y[i] −= x[i] · f for std::complex<double>, as the other (the template keeps the expression's order). | |
| void | numerical::detail::add_product (double *y, const double &f, const double *x, std::size_t count) |
y[i] += f · x[i] for double, as subtract_product. | |
| void | numerical::detail::add_product (std::complex< float > *y, const std::complex< float > &f, const std::complex< float > *x, std::size_t count) |
y[i] += f · x[i] for complex float: on the x86-64 paths by AVX2 vectors of its arithmetic (avx2_scaled_add_c32), the same bits as add_product's template, which runs elsewhere. | |
| void | numerical::detail::add_product (std::complex< double > *y, const std::complex< double > &f, const std::complex< double > *x, std::size_t count) |
add_product for complex double (avx2_scaled_add_c64 on the x86-64 paths). | |
| double | numerical::detail::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, as fixed). | |
| template<split_complex C> | |
| C | numerical::detail::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 chains (avx2_complex_dot), elsewhere the native vectors' (in another order of summation there, as fixed). | |
| std::complex< double > | numerical::detail::dot_product (const std::complex< double > *x, const std::complex< double > *y, std::size_t count) |
dot_product for std::complex<double>, through complex_dot_product. | |
| std::complex< float > | numerical::detail::dot_product (const std::complex< float > *x, const std::complex< float > *y, std::size_t count) |
dot_product for std::complex<float>, through complex_dot_product. | |
| template<split_complex C> | |
| void | numerical::detail::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. | |
| void | numerical::detail::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. | |
| void | numerical::detail::row_dots (const std::complex< float > *m, std::size_t ld, std::size_t rows, const std::complex< float > *x, std::size_t count, std::complex< float > *out) |
row_dots for std::complex<float>, through complex_row_dots. | |
| void | numerical::detail::row_dots (const double *m, std::size_t ld, std::size_t rows, const double *x, std::size_t count, double *out) |
row_dots for double: on the x86-64 units each row by their dot_product (wider than four rows of the portable vectors), elsewhere four rows at a time. | |
| template<typename T > | |
| void | numerical::detail::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. | |
| template<std::size_t Mr, typename T > | |
| void | numerical::detail::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, each stored column by column; a last partial sliver is padded with zeros. | |
| template<std::size_t Nr, typename T > | |
| void | numerical::detail::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, each stored row by row; a last partial sliver is padded with zeros. | |
| template<std::size_t Nr, typename T > | |
| void | numerical::detail::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 sliver is a row of bt, read along it; a last partial sliver is padded with zeros. | |
| template<typename T > | |
| const T * | numerical::detail::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). | |
| template<typename T > | |
| const T * | numerical::detail::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 × rows, ld apart). | |
| template<std::size_t Mr, typename T > | |
| void | numerical::detail::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, ldat apart): each column of a sliver is a run of a row of at; a last partial sliver is padded with zeros, as gemm_pack_a lays them out. | |
| template<typename T > | |
| T | numerical::detail::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 its own: under Clang's -ffp-contract=on one expression, fused in both; under GCC's, whose vector extensions fuse nothing while a scalar statement's multiply-add fuses, the product apart, a statement of its own. | |
| template<typename R > | |
| std::complex< R > | numerical::detail::rounded_apart_add (std::complex< R > sum, std::complex< R > a, std::complex< R > b) noexcept |
| rounded_apart_add of complex numbers: the product in line (operator_times, the operator's own rounding, unfused), where it was a call that took a sixth of a complex LU. | |
| template<typename T > | |
| T | numerical::detail::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). | |
| template<typename T > | |
| T | numerical::detail::rounded_term (rounding_is< term_rounding::fused >, T sum, T a, T b) noexcept |
sum + a·b as one fused multiply-add. | |
| template<std::size_t R, std::size_t N, term_rounding Mode, typename T > | |
| void | numerical::detail::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: the block held in registers, of a size the compiler knows; each term rounded as Mode says. Out of line: its unrolled sizes, inlined, spent GCC's growth budget, and the LU factorisation, which runs none of them, lost 3 to 8 % at every order (one arm64 thread, GCC 14). | |
| template<std::size_t R, term_rounding Mode, typename T > | |
| void | numerical::detail::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. | |
| template<term_rounding Mode, typename T > | |
| void | numerical::detail::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), the rows run narrow_group at a time by blocks (narrow_rows), the last rows one by one: the same sums, six to ten times as fast from order 4 to 16 (M1 Pro) as the loop of unknown width, whose sums the compiler cannot keep in registers. A depth of 1 streams as fast through that loop. | |
| bool | numerical::detail::rows_fuse () noexcept |
Whether the tile a double or float product past the plain loop runs on fuses each term (gemm_rows rounds its own so): double on arm64's NEON and AMX paths, float on the NEON tile there. The x86-64 tiles' double takes term_rounding::x86_fused; the vector tile, and so the portable path, rounds as the compiler does, as the plain loop then does. On arm64 every cap but the portable one runs the NEON or the AMX path, so the cap alone answers: one load, where the path in use took an order-4 product on one arm64 core from 10.6 to 13.3 ns (the cap, 11.5). | |
| template<typename T > | |
| void | numerical::detail::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 use rounds it, so that it is the same whatever the shape of the product it falls in: a caller that splits its columns by the threads (an LU inverse) then has the same bits on any number of them. | |
| template<typename T > | |
| T * | numerical::detail::line_aligned (T *at) noexcept |
The first element of at's run that starts a cache line (or at's nearest element towards one, for an element whose size does not divide the allocator's alignment). | |
| template<typename Kernel > | |
| constexpr bool | numerical::detail::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 caller alone. | |
| template<typename Kernel , typename T > | |
| void | numerical::detail::prefetch_next_row (const T *, std::size_t) noexcept |
| Nothing, for a tile that prefetches no C (prefetches_next_row_of_c). | |
| template<typename Kernel , typename T > requires prefetches_next_row_of_c<Kernel> | |
| void | numerical::detail::prefetch_next_row (const T *c, std::size_t ldc) noexcept |
The C tile at c prefetched by Kernel's prefetch_rows. | |
| template<typename Kernel > | |
| constexpr std::size_t | numerical::detail::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. | |
| template<typename Kernel > requires requires(std::size_t kc) { Kernel::task_rows_for(kc); } | |
| constexpr std::size_t | numerical::detail::task_rows_cap (std::size_t kc) noexcept |
task_rows_cap for a tile whose task rows depend on the panel's depth (Kernel::task_rows_for). | |
| template<typename Kernel , typename T , typename B = b_rows, typename A = a_rows> | |
| void | numerical::detail::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 depth, both operands are packed (in parallel, by the blocks the product takes), then tasks of gemm_mc rows by gemm_slivers_per_task slivers of columns run in parallel, each writing a disjoint block of C, one tile at a time. Given a_packed (A as pack_left packs it for Kernel, a depth of one panel), A is read from there and only B is packed: the same tiles, the same bits. B is B's layout (b_rows, b_transposed), A A's (a_rows, a_transposed): the packing alone differs, so the bits do not. | |
| template<typename Kernel , typename T , typename B = b_rows, typename A = a_rows> | |
| void | numerical::detail::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, else its upper one (j ≥ i); the other is neither written nor depended on. The same packing (A's row blocks with no tile in the triangle left out) and the same tiles, so each element's sum is the product's; a tile across the diagonal runs on a copy of its block of C, the triangle's part copied back. A syrk takes the triangle in one pass, its operands packed once a panel, where products by halves packed them again for every block. | |
| template<typename T > | |
| bool | numerical::detail::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. | |
| template<typename B , typename A = a_rows> | |
| bool | numerical::detail::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, B and A laid out as B and A say (b_rows, a_rows and their transposes). | |
| template<typename A , typename B > | |
| void | numerical::detail::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, A and B laid out as A and B say. | |
| bool | numerical::detail::gemm_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) |
| gemm_double_elsewhere of a row-major B. | |
| template<typename B , typename A = a_rows> | |
| bool | numerical::detail::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, on the x86-64 tiles there; elsewhere the vector tile. B and A laid out as B and A say. | |
| bool | numerical::detail::gemm_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) |
| gemm_float_elsewhere of row-major operands. | |
| template<typename T > | |
| void | numerical::detail::gemm (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, every element of C summed in ascending order of the depth. | |
| bool | numerical::detail::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_parts_block), else by one of its expanded operands (split_expanded_block): the same bits, the parts' products faster where they read B's parts in place (gemm_packed). On the AMX path up to split_parts_rows; elsewhere (the NEON path) up to one row of the NEON tile, its b_in_place_rows: M1 Pro, all threads, by 400 × 500, 1 to 4 rows 2 to 18 % faster, 6 to 32 rows 5 to 52 % slower. | |
| template<typename R > | |
| split_operands< R > & | numerical::detail::split_storage () |
| The calling thread's split_operands. A product uses them whole on one thread, and the real product it runs on them nests no complex one. | |
| template<split_complex Z, typename B = b_rows, typename A = a_rows> | |
| void | numerical::detail::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 real products of its parts: Re C += Re A·Re B, then −Im A·Im B; Im C += Re A·Im B, then Im A·Re B, each continuing its elements' chains ((−x)·(−y) is x·y exactly: the bits of −Im A·−Re B). B's parts are a copy of its size, which a product of a few rows of A reads in place. The block is A's columns and B's rows from pc, B's columns from jc, A and B laid out as A and B say. | |
| template<split_complex Z, typename B = b_rows, typename A = a_rows> | |
| void | numerical::detail::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, [Re C | Im C] += [Re A | −Im A] · [[Re B, Im B], [Im B, −Re B]]: the same chains, B copied four times its size, the product as wide and as deep as the block's two. | |
| template<split_complex Z, typename B = b_rows, typename A = a_rows> | |
| void | numerical::detail::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_depth of depth, on the real path's units and threads (a scalar complex tile has none). Within a block, each element of C takes the products of the real parts of A in ascending order of the depth, then those of the imaginary parts: by four real products of the block's parts (split_parts_block) or one of its expanded operands (split_expanded_block), as split_by_parts chooses, the same bits either way. A and B laid out as A and B say, read where they lie as the parts are copied. | |
| bool | numerical::detail::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, still runs faster in the plain loop than packed on the path in use: packing B's slivers for so few rows of A, or so short a depth, did not pay. One core, the plain loop's time over the packed product's: the NEON tile at a depth of 16 or less whatever the shape (0.5 to 0.6 up to 2000 × 2000), at 64 or less for 64 rows or fewer (0.4 to 0.9; the same under GCC 14 and on eight threads); the AVX2 tile for 16 rows or fewer to a depth of 64, or 32 rows to 32 (0.2 to 0.8, six cores too), as wide as plain_loop_least_width. The AMX's packing pays from 16 rows. | |
| template<typename T > | |
| bool | numerical::detail::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. | |
| template<> | |
| bool | numerical::detail::plain_loop_suits< double > (std::size_t m, std::size_t n, std::size_t k) noexcept |
plain_loop_pays for double. | |
| template<typename T > | |
| void | numerical::detail::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, else on the vector tile. | |
| template<typename B , typename A , split_complex T> | |
| void | numerical::detail::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 products summed in order; on the x86-64 complex tiles there; else (arm64) through the real kernel (gemm_split). B and A laid out as B and A say. | |
| template<typename B , typename A > | |
| void | numerical::detail::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). | |
| template<typename B , typename A > | |
| void | numerical::detail::gemm_large_layouts (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 past the plain loop, B and A laid out as B and A say (gemm_layouts). | |
| template<typename B , typename A , split_complex T> | |
| void | numerical::detail::gemm_large_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) |
A complex product past the plain loop, B and A laid out as B and A say (gemm_layouts). | |
| template<typename A , typename B , typename T > | |
| void | numerical::detail::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), so that a product by a transpose (a syrk's P Pᵀ, BLAS's transa and transb, a NumPy array in Fortran order) needs no copy of it. The tiles are gemm's, the operands packed (or, through gemm_split, copied into their parts) from where they lie, and so are the bits; a product gemm runs in the plain loop copies a transposed operand row-major and runs there, small as it is. | |
| template<typename T > | |
| void | numerical::detail::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). | |
| template<typename T > | |
| void | numerical::detail::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). | |
| template<typename T > | |
| void | numerical::detail::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. | |
| template<typename T > | |
| packed_left< T > | numerical::detail::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. | |
| template<typename T > | |
| packed_left< T > | numerical::detail::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, or their rows of A in place. | |
| template<typename T > | |
| left_kernel | numerical::detail::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: plain for a scalar other than double (written as an overload per scalar, as conj_of). | |
| template<> | |
| left_kernel | numerical::detail::left_kernel_for< double > (std::size_t m, std::size_t n, std::size_t k) noexcept |
left_kernel_for a double product: past gemm_rows, as gemm_elsewhere chooses, of a depth of one panel; plain otherwise. | |
| template<typename Kernel , typename T > | |
| const T * | numerical::detail::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. | |
| template<typename T > | |
| packed_left< T > | numerical::detail::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 as needed, kept by the caller from one call to the next): on the tile gemm would choose for m × n × k. A scalar other than double (here), a depth of more than one panel, or a product gemm_rows takes, leaves A to be read in place. | |
| template<> | |
| packed_left< double > | numerical::detail::pack_left< double > (std::size_t m, std::size_t n, std::size_t k, const double *a, std::size_t lda, buffer< double > &storage) |
pack_left for double, on the tile left_kernel_for chooses. | |
| template<typename T > | |
| void | numerical::detail::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, for a scalar other than double. | |
| template<> | |
| void | numerical::detail::gemm_with< double > (const packed_left< double > &left, std::size_t n, const double *b, std::size_t ldb, double *c, std::size_t ldc) |
gemm_with for double: on the tile A was packed for, A read from its packing, where gemm would choose that tile for this n; else as gemm. Either way gemm's tiles and order of summation, its bits. | |
| template<typename T > | |
| T | numerical::detail::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 (several vector accumulators, on the widest x86-64 units for double), their sums added in the chunks' order. The same on any number of threads; in the last bits, not the ascending sum, which waits on each addition before the next (ten times slower at 100 000 elements). | |
| template<typename T > | |
| void | numerical::detail::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, as chunked_dot sums it (a row of a chunk or less is one dot_product), the rows in parallel by groups (row_dots, which share each load of x). The same on any number of threads. A double matrix of amx_dot_least elements to fewer than amx_dot_most, on the AMX path, runs on the AMX (amx_dot_rows_f64): each element then its eight lanes of fused multiply-adds, added in order. | |
| gemm_path | numerical::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 that can run here. gemm_path::amx restores the best one; gemm_path::portable keeps them on the compiler's own vectors, with no instruction chosen at run time. Paths below the cap are not probed. | |
| gemm_path | numerical::gemm_path_in_use () noexcept |
The path double matrix products take. | |
Variables | |
| constexpr bool | numerical::detail::neon_compiled {false} |
| Whether the NEON path's tile is compiled in: on every arm64 target GCC or Clang builds. | |
| constexpr std::size_t | numerical::detail::gemm_mr {4} |
| The rows of C one register tile covers. | |
| template<typename T > | |
| constexpr std::size_t | numerical::detail::gemm_nr {sizeof(T) >= 16 ? std::size_t {6} : std::size_t {96} / sizeof(T)} |
The columns of C one register tile covers: 96 bytes of elements (12 double, 24 float), which with gemm_mr rows fills the vector registers of both arm64 and x86-64 with AVX. | |
| constexpr std::size_t | numerical::detail::gemm_kc {256} |
The depth of one panel: a B sliver (gemm_kc × gemm_nr) stays in the L1 cache. | |
| constexpr std::size_t | numerical::detail::gemm_mc {64} |
The rows of one task: an A block (gemm_mc × gemm_kc) stays in the L2 cache. | |
| constexpr std::size_t | numerical::detail::gemm_slivers_per_task {4} |
| The B slivers of one task. | |
| constexpr std::size_t | numerical::detail::tasks_per_thread {4} |
| The tasks a product's compute pass hands each thread at least, while its rows allow: a slower core then holds back no one. | |
| constexpr std::size_t | numerical::detail::gemm_kernel_speedup {16} |
| The multiply-adds the register tile does in the time of one in work_per_thread's dependent chain (about 20 G/s on one arm64 or x86-64 AVX2 core): a task weighs its work by it, so a product of 0.1 ms does not start eight threads. | |
| template<typename T > | |
| constexpr std::size_t | numerical::detail::gemm_nc {gemm_nr<T> * 128} |
| The columns of B packed at once, a whole number of slivers. | |
| constexpr std::size_t | numerical::detail::gemm_packed_threshold {std::size_t {1} << 13U} |
| Below this many multiply-adds the plain loop (gemm_rows, by blocks of rows held in registers) runs instead of the packed kernel, both summing in the same order. Against the packed kernel on 400 random shapes from 512 multiply-adds, the plain loop took 0.25 to 0.41 times as long below 2^13 on arm64 (M1 Pro, geometric means by octave, the slowest shape 0.91); above, it lost up to 2.5 times on shapes the AMX suits. On x86-64 the plain loop has the baseline's vectors and the packed one the AVX2 or AVX-512 units: there it took 0.43 to 0.55 times as long below 2^11, 0.86 above with shapes up to 2 times slower (six AVX2 cores, GCC 13). | |
| constexpr std::size_t | numerical::detail::gemm_min_depth {8} |
| Below this depth the plain loop runs too: each panel reads and writes its tiles of C once, which a shallow product does not amortise (1000×1000×1 ran six times slower packed, ×4 still lost 10 %). | |
| constexpr std::size_t | numerical::detail::narrow_least_depth {2} |
| The least depth at which rows of C wider than one block (narrow_columns) run by blocks in gemm_rows; one block or narrower takes any depth past 1. Against the plain loop of unknown width, on 500 random shapes below the packed kernel, the blocks took 0.11 to 0.63 times as long from a depth of 2 on arm64 (M1 Pro, geometric means by width); on x86-64, where the loop has the baseline's vectors, 0.35 to 0.90 from a depth of 8 but 1.08 to 1.31 below it past one block (six AVX2 cores, GCC 13). | |
| constexpr std::size_t | numerical::detail::narrow_columns {8} |
| The columns of C one block of gemm_rows keeps in registers over the whole depth. | |
| constexpr std::size_t | numerical::detail::narrow_group {4} |
| The rows of C one block of gemm_rows takes together: each element a chain of multiply-adds that waits on its last, four rows give the units four times the chains (M1 Pro, order 8 to 24: 1.6 to 1.9 times as fast as one row; two rows on six x86-64 AVX2 cores, the fastest there, were within 7 %). | |
| constexpr std::size_t | numerical::detail::packed_line {64} |
| The cache line a packed operand starts. | |
| template<typename Kernel > | |
| constexpr bool | numerical::detail::prefetches_next_row_of_c |
Whether Kernel has the C tile of the next row of tiles prefetched while a tile computes (Kernel::prefetch_rows): the tiles within a row walk one page's lines, which the processor's own prefetch follows, while the next row lands on other pages. | |
| constexpr std::size_t | numerical::detail::split_columns {512} |
The columns of B and C, and the depth, of one real product of gemm_split: its real operands stay a few megabytes whatever the shapes. | |
| constexpr std::size_t | numerical::detail::split_depth {256} |
The depth of one real product of gemm_split (split_columns). | |
| constexpr std::size_t | numerical::detail::split_kept {std::size_t {1} << 18} |
The most elements an operand of gemm_split keeps from one product to the next on a thread: 2 MiB of double, every operand of a product of order 512; a larger one is let go after its product. | |
| constexpr std::size_t | numerical::detail::split_parts_rows {64} |
The most rows of A for which gemm_split runs four real products of a block's parts on the AMX path (split_by_parts). M1 Pro, all threads, A of 16 to 64 rows by B of 256 to 1024: complex float 1.1 to 2.4 times faster, complex double 0.97 to 1.6; 96 to 128 rows by 512, complex double 1.2 to 1.4 times slower, where one wide product suited the AMX. | |
| constexpr std::size_t | numerical::detail::plain_loop_least_width {64} |
| The fewest columns of a product plain_loop_pays sends to the plain loop on the x86-64 tiles: there an order-24 SVD, whose two products of 24 × 24 × 24 it took, ran 9 % slower (GCC 13, the same bits), though such a product alone ran faster in the plain loop. On the NEON tile the small square products gain most (GCC 14, one core: a product of order 24 to 64 −32 to −50 %, a QR −5 to −21 %). | |
| constexpr std::size_t | numerical::detail::dot_chunk {std::size_t {1} << 14U} |
The elements of one partial sum of chunked_dot: fixed, so the sum does not depend on the threads. | |
| template<typename T > | |
| constexpr std::size_t | numerical::detail::multiply_add_weight {1} |
The real multiply-adds one multiply-add of T weighs: 1 for a real number. | |
| constexpr std::size_t | numerical::detail::row_group {64} |
| The rows a thread takes of a matrix times a vector (matrix_times_vector). | |
The matrix product kernel behind matrix * matrix and the einsum matmul path.
C += A·B over row-major buffers, in the loop structure of BLIS (Van Zee and van de Geijn, 2015): the depth is cut into panels of numerical::detail::gemm_kc, each operand's panel is copied once into a contiguous layout the inner loop reads at unit stride (A in slivers of numerical::detail::gemm_mr rows, B in slivers of numerical::detail::gemm_nr columns), and a register tile of gemm_mr × gemm_nr elements of C accumulates a whole panel's depth before it is written back. Each element of C is read into the tile at the start of a panel and written back at its end, so every element is summed in ascending order of the depth, exactly as the plain i-k-j loop does, on any number of threads: with the compiler's multiply-add contraction off, the two agree to the bit.
For float and double under GCC and Clang the tile is held in vectors of the target's native width (16 bytes, 32 with AVX), which both compilers keep in registers; elsewhere it is a plain array.
Definition in file gemm.hpp.