Floating-point matrix multiplication (gemm)
- Cost
- floating-point operations; packing copies entries.
One of the ways FLINT multiplies matrices over and is to convert the entries to floating point, call a gemm (general matrix multiplication) routine and convert back: a well-tuned floating-point matrix product runs close to the peak arithmetic rate of the processor, and it is exact when the entries are small enough (Exactness). Larger entries are first reduced modulo several small numbers, each product is computed by gemm, and the result is reconstructed by Chinese remaindering. This route is used only in some ranges of moduli, entry sizes and dimensions; the topic pages Multiplying matrices over Z/nZ and Multiplying matrices over the integers give the complete dispatch. In outline:
- Over ,
nmod_mat_muluses floating-pointgemminnmod_mat_mul_blasand in the byte kernels ofnmod_mat_mul_u8, which callflint_sgemmon blocks for .nmod_mat_mul_blasis chosen for only for large matrices for which onedgemmis exact (from a tuned dimension,FLINT_NMOD_MAT_MUL_BLAS_1PASS_CUTOFF), and otherwise mainly for moduli above (and on targets without the SIMD kernels) from a dimension between 100 and several hundred, depending on the modulus and the number of threads; with an external BLAS, also for large matrices with moduli of 33 to 52 bits. In the other casesnmod_mat_muluses classical multiplication with delayed reduction (Dot products modulo a word-size integer), the blocked SIMD kernelsnmod_mat_mul_u32,nmod_mat_mul_u52,nmod_mat_mul_k52andnmod_mat_mul_fp50(Matrix multiplication kernels modulo n), table lookups and bit-packing for tiny moduli (Matrix multiplication modulo small integers), and Strassen–Winograd above them (Strassen–Winograd multiplication). - Over ,
fmpz_mat_mulusesgemm(fmpz_mat_mul_blas) on 64-bit machines only for dimensions above 50 and above a limit that depends on the bit size of the output entries: none for , where onedgemmis exact, 1000 for , 1300 for , 250 for and about above. Otherwise it uses dot products in one to five machine words, Waksman's and Strassen–Winograd's algorithms, multimodular multiplication throughnmod_mat_mul, and, for entries of many thousands of bits,fmpz_mat_mul_fft_small, which transforms every entry once. - Other rings.
fmpz_mod_mat_mul,fmpq_mat_muland the block products ofarb_matreduce tofmpz_mat_mul;mpn_modmatrices (moduli of several words) use classical multiplication with delayed reduction, Waksman's algorithm or multimodular multiplication throughnmod_mat_mul(Matrix multiplication over Z/nZ for multiword n); andgr_mat_mulcalls the method of each ring, by default the classical algorithm (Multiplying matrices over generic rings). They reachgemmonly throughnmod_mat_mulandfmpz_mat_mul.
When FLINT is built with BLAS, flint_sgemm and flint_dgemm call it (unless flint_gemm_use_blas is set to 0). Otherwise they use FLINT's own kernels in src/machine_vectors/gemm.c, which are described below. Their organisation follows Goto and van de Geijn [GvdG2008] in the loop ordering of BLIS [VZvdG2015].
Exactness#
The products FLINT computes are exact: the entries of and are integers, and they are bounded so that no rounding ever occurs.
Theorem 1. Let and have integer entries and suppose that for all
Then any algorithm that computes each by additions, multiplications and fused multiply-adds of doubles, starting from the entries of , and zeros, and combining products for distinct , returns the exact integer . The same holds for floats with in place of .
Proof. Every intermediate value is a sum of a subset of the products , which is an integer of absolute value at most . Such integers are doubles, so by induction every operation has an exactly representable result and returns it without rounding.
The theorem does not depend on the order of summation, so it applies to FLINT's kernels including the zero padding in the packed buffers below, and to any implementation that evaluates the sums directly, which is what FLINT assumes of an external BLAS. A sufficient condition is . For entries reduced modulo and lifted to the symmetric range , this becomes , which is four times weaker than for entries in .
Structure#
The kernels compute for row-major (), () and () with arbitrary leading dimensions. One source file, gemm_templ.h, is instantiated for float and double, and all arithmetic goes through the vector types of machine_vectors.h. The code has no target-specific branches beyond the choice of vector type and tile height.
The micro-kernel#
The innermost routine updates an tile of , held in vector registers, with where is the number of lanes per vector:
Input: a packed block of , a packed block of , and an tile of (or zero).
Output: the tile plus .
- Load the tile into accumulators for , or set them to zero for the first block of .
For :
- load the two vectors of row of ;
- for : broadcast to a vector , then set and with fused multiply-adds.
- Store the accumulators.
Each step of the loop performs independent fused multiply-adds on two loaded vectors and broadcasts, enough to hide the latency of the multiply-add units. The tile sizes chosen in gemm.c are:
| Backend | Double: vector, | Float: vector, | Registers used |
|---|---|---|---|
| AVX-512 | vec8dz, | vec16fz, | 18 of 32 |
| AVX2 | vec4d, | vec8f, | 14 of 16 |
| NEON | vec2d, | vec4f, | 18 of 32 |
| 64-bit, no AVX2 or NEON | vec2d, | vec4f, | 14 of 16 |
| Forced generic, GNU vectors with AVX | vec4d, | vec8f, | 10 |
| Forced generic, otherwise | vec2d, | vec4f, | 10 |
| Strict ISO C | vec4d, | vec4f, |
The forced generic rows also apply to 32-bit builds, which always use a generic backend.
Blocking and packing#
Around the micro-kernel, five loops partition the matrices so that the data each level reuses stays in a level of the memory hierarchy:
Input: , and the block sizes , , (in elements).
Output: .
- For each block of columns of and :
For each block of rows of this column block of :
- pack the block of into column panels of width ;
for each block of rows of and :
- pack the block of into row panels of height ;
- for each panel of (step ), and for each panel of (step ): run Algorithm 1 on the corresponding tile of , accumulating unless this is the first block of .
Packing copies a panel into a contiguous buffer in the order in which the micro-kernel reads it: for , row of the panel is the entries the kernel loads at step , and for , the entries of column are adjacent. A panel that extends past the matrix is padded with zeros, so the micro-kernel always works on full tiles. An edge tile of is copied into a local buffer, updated and copied back, so no memory outside is touched. The packed buffers are aligned to 64 bytes, and the scratch space is a thread-local buffer that grows as needed and is kept between calls, or allocated per call without thread-local storage.
In double precision the packed block takes bytes KiB and a panel of takes 16 KiB with AVX2. In the scheme of [GvdG2008] the former stays in the level 2 cache and the latter in the level 1 cache while it is multiplied by every panel of . The block sizes are the same on all targets.
Special shapes#
Packing costs copies per call. For small or thin matrices this is not repaid, and the serial driver dispatches as follows when :
- : is read once, in strips of columns, while the rows of a strip of stay in registers. Remaining columns are computed by scalar loops.
- all of : the whole problem fits in the level 2 cache, and a direct kernel loads and in place, sweeping strips of columns with accumulators; the last columns of are copied once into a zero-padded buffer. This path is disabled for the strict ISO C backend, where it was measured to be slower.
- : is packed once into a single panel and is streamed through it.
- otherwise: Algorithm 2.
When or nothing is done, and when the result is set to zero.
Threading#
flint_sgemm_fallback and flint_dgemm_fallback use FLINT's thread pool. Thin products ( or ) are memory-bound and always run serially. Otherwise the number of threads is at most flint_get_num_threads() and at most , so that each thread gets about a million floating-point operations, and is further limited by the number of free threads in the pool. Each worker is woken once per call. There are two drivers:
- Cooperative (x86-64): all threads run the loops of Algorithm 2 together. For each block of , the threads pack its panels in parallel, taking panel indices from an atomic counter, wait at a barrier, then take blocks of rows of from a second counter, each packing its own block and computing the corresponding rows of . The row block size is for threads, rounded up to a multiple of and capped at . The barriers are sense-reversing spin barriers; except on Windows, a waiting thread yields the processor after every 2048 spins.
- Split (other targets): the rows of are divided into nearly equal blocks and each thread runs the serial algorithm on its rows of and all of . Nothing is shared except read-only input, but each thread packs itself. The source notes that this driver is slightly slower at high thread counts and that the cooperative one, which relies on C11 atomics with acquire and release ordering, has only been stress tested on x86-64.
Defining GEMM_MT_FORCE_ATOMIC or GEMM_MT_FORCE_SPLIT when compiling gemm.c overrides the choice.
Use in FLINT#
All conversions between FLINT's integer types and floating point happen outside the kernels, which compute plain floating-point products.
nmod_mat_mul_blaslifts entries modulo to the symmetric range. If it uses a singleflint_sgemm, and if a singleflint_dgemm. Otherwise it chooses up to 12 pairwise coprime moduli below , so that each product modulo is exact by Theorem 1, computes oneflint_dgemmper modulus and recombines the results by Chinese remaindering (Floating-point gemm and Chinese remaindering). Before each call it gives its worker threads back to the pool so that the kernels can use them.fmpz_mat_mul_blasuses a singleflint_dgemmwhen the entries of and have at most and bits with . Otherwise it reduces the entries modulo primes with , multiplies modulo each prime withflint_dgemmand reconstructs the result with a multi-modular algorithm.nmod_mat_mul_u8handles moduli with entries stored in bytes. For moduli above 16, the range of its byte-shuffle kernels, it converts entries in to floats and callsflint_sgemmon blocks of columns, accumulating the exact block results in 32-bit integers (Larger moduli).
Exact linear algebra by floating-point matrix multiplication, with the modular reduction delayed to the end, is the approach of the FFLAS and FFPACK libraries [DGP2008].
Referenced from#
References#
- [GvdG2008] K. Goto and R. A. van de Geijn, "Anatomy of high-performance matrix multiplication", ACM Transactions on Mathematical Software 34:3 (2008), article 12, https://doi.org/10.1145/1356052.1356053
- [VZvdG2015] F. G. Van Zee and R. A. van de Geijn, "BLIS: A framework for rapidly instantiating BLAS functionality", ACM Transactions on Mathematical Software 41:3 (2015), 1-33, https://doi.org/10.1145/2764454
- [DGP2008] J.-G. Dumas, P. Giorgi and C. Pernet, "Dense linear algebra over word-size prime fields: the FFLAS and FFPACK packages", ACM Transactions on Mathematical Software 35:3 (2008), 1-42, https://doi.org/10.1145/1391989.1391992
The FLINT team, “Floating-point matrix multiplication (gemm),” FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev, 2026. https://flintlib.org/doc/algorithms/linear-algebra/gemm.html (generated 2026-11-02).
@misc{flint-doc-algorithms-linear-algebra-gemm,
author = {{The FLINT team}},
title = {{Floating-point matrix multiplication (gemm)}},
howpublished = {FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev},
year = {2026},
url = {https://flintlib.org/doc/algorithms/linear-algebra/gemm.html},
note = {Generated 2026-11-02}
}