FLINT3.7.0-dev
Algorithm

Floating-point matrix multiplication (gemm)

Cost
2mkn2mkn floating-point operations; packing copies O(mk⌈n/nc⌉+kn)O(mk \lceil n/n_c \rceil + kn) entries.

One of the ways FLINT multiplies matrices over Z/nZ\mathbb{Z}/n\mathbb{Z} and Z\mathbb{Z} 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:

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 AA and BB are integers, and they are bounded so that no rounding ever occurs.

Theorem 1. Let AA and BB have integer entries and suppose that for all i,ji, j

∑l∣Ail∣ ∣Blj∣≤253.\sum_{l} |A_{il}| \, |B_{lj}| \le 2^{53}.

Then any algorithm that computes each CijC_{ij} by additions, multiplications and fused multiply-adds of doubles, starting from the entries of AA, BB and zeros, and combining products AilBljA_{il} B_{lj} for distinct ll, returns the exact integer Cij=∑lAilBljC_{ij} = \sum_l A_{il} B_{lj}. The same holds for floats with 2242^{24} in place of 2532^{53}.

Proof. Every intermediate value is a sum of a subset of the products AilBljA_{il} B_{lj}, which is an integer of absolute value at most ∑l∣Ail∣∣Blj∣≤253\sum_l |A_{il}||B_{lj}| \le 2^{53}. 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 kmax⁡∣Ail∣max⁡∣Blj∣≤253k \max |A_{il}| \max |B_{lj}| \le 2^{53}. For entries reduced modulo pp and lifted to the symmetric range ∣x∣≤⌊p/2⌋|x| \le \lfloor p/2 \rfloor, this becomes k⌊p/2⌋2≤253k \lfloor p/2 \rfloor^2 \le 2^{53}, which is four times weaker than k(p−1)2≤253k (p - 1)^2 \le 2^{53} for entries in [0,p)[0, p).

Structure#

The kernels compute C=ABC = AB for row-major AA (m×km \times k), BB (k×nk \times n) and CC (m×nm \times n) 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 mr×nrm_r \times n_r tile of CC, held in 2mr2 m_r vector registers, with nr=2vn_r = 2v where vv is the number of lanes per vector:

Algorithm 1: Micro-kernel

Input: a packed mr×kcm_r \times k_c block A~\tilde A of AA, a packed kc×nrk_c \times n_r block B~\tilde B of BB, and an mr×nrm_r \times n_r tile of CC (or zero).

Output: the tile plus A~B~\tilde A \tilde B.

  1. Load the tile into accumulators cr,0,cr,1c_{r,0}, c_{r,1} for 0≤r<mr0 \le r < m_r, or set them to zero for the first block of kk.
  2. For l=0,…,kc−1l = 0, \ldots, k_c - 1:

    1. load the two vectors b0,b1b_0, b_1 of row ll of B~\tilde B;
    2. for r=0,…,mr−1r = 0, \ldots, m_r - 1: broadcast A~rl\tilde A_{rl} to a vector aa, then set cr,0←ab0+cr,0c_{r,0} \gets a b_0 + c_{r,0} and cr,1←ab1+cr,1c_{r,1} \gets a b_1 + c_{r,1} with fused multiply-adds.
  3. Store the accumulators.

Each step of the loop performs 2mr2 m_r independent fused multiply-adds on two loaded vectors and mrm_r broadcasts, enough to hide the latency of the multiply-add units. The tile sizes chosen in gemm.c are:

BackendDouble: vector, mr×nrm_r \times n_rFloat: vector, mr×nrm_r \times n_rRegisters used
AVX-512vec8dz, 8×168 \times 16vec16fz, 8×328 \times 3218 of 32
AVX2vec4d, 6×86 \times 8vec8f, 6×166 \times 1614 of 16
NEONvec2d, 8×48 \times 4vec4f, 8×88 \times 818 of 32
64-bit, no AVX2 or NEONvec2d, 6×46 \times 4vec4f, 6×86 \times 814 of 16
Forced generic, GNU vectors with AVXvec4d, 4×84 \times 8vec8f, 4×164 \times 1610
Forced generic, otherwisevec2d, 4×44 \times 4vec4f, 4×84 \times 810
Strict ISO Cvec4d, 4×84 \times 8vec4f, 4×84 \times 8

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:

Algorithm 2: Blocked product

Input: AA, BB and the block sizes nc=2048n_c = 2048, kc=256k_c = 256, mc=96m_c = 96 (in elements).

Output: C=ABC = AB.

  1. For each block of ncn_c columns of BB and CC:
  2. For each block of kck_c rows of this column block of BB:

    1. pack the kc×nck_c \times n_c block of BB into column panels of width nrn_r;
    2. for each block of mcm_c rows of AA and CC:

      1. pack the mc×kcm_c \times k_c block of AA into row panels of height mrm_r;
      2. for each panel of BB (step nrn_r), and for each panel of AA (step mrm_r): run Algorithm 1 on the corresponding tile of CC, accumulating unless this is the first block of kk.

Packing copies a panel into a contiguous buffer in the order in which the micro-kernel reads it: for BB, row ll of the panel is the nrn_r entries the kernel loads at step ll, and for AA, the mrm_r entries of column ll 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 CC is copied into a local mr×nrm_r \times n_r buffer, updated and copied back, so no memory outside CC 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 AA block takes 96⋅256⋅896 \cdot 256 \cdot 8 bytes =192= 192 KiB and a kc×nrk_c \times n_r panel of BB 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 AA. The block sizes are the same on all targets.

Special shapes#

Packing costs O(mk+kn)O(mk + kn) copies per call. For small or thin matrices this is not repaid, and the serial driver dispatches as follows when k>0k > 0:

  • m≤mrm \le m_r: BB is read once, in strips of 4v4v columns, while the mm rows of a strip of CC stay in registers. Remaining columns are computed by scalar loops.
  • all of m,k,n≤96m, k, n \le 96: the whole problem fits in the level 2 cache, and a direct kernel loads AA and BB in place, sweeping strips of vv columns with mrm_r accumulators; the last n mod vn \bmod v columns of BB 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.
  • n≤nrn \le n_r: BB is packed once into a single k×nrk \times n_r panel and AA is streamed through it.
  • otherwise: Algorithm 2.

When m≤0m \le 0 or n≤0n \le 0 nothing is done, and when k≤0k \le 0 the result is set to zero.

Threading#

flint_sgemm_fallback and flint_dgemm_fallback use FLINT's thread pool. Thin products (m≤mrm \le m_r or n≤nrn \le n_r) are memory-bound and always run serially. Otherwise the number of threads is at most flint_get_num_threads() and at most 1+2mkn/1061 + 2mkn / 10^6, 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 BB, the threads pack its panels in parallel, taking panel indices from an atomic counter, wait at a barrier, then take blocks of rows of AA from a second counter, each packing its own AA block and computing the corresponding rows of CC. The row block size is m/(4T)m/(4T) for TT threads, rounded up to a multiple of mrm_r and capped at mcm_c. 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 CC are divided into TT nearly equal blocks and each thread runs the serial algorithm on its rows of AA and all of BB. Nothing is shared except read-only input, but each thread packs BB 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_blas lifts entries modulo pp to the symmetric range. If k⌊p/2⌋2<224k \lfloor p/2 \rfloor^2 < 2^{24} it uses a single flint_sgemm, and if k⌊p/2⌋2<253k \lfloor p/2 \rfloor^2 < 2^{53} a single flint_dgemm. Otherwise it chooses up to 12 pairwise coprime moduli tt below 255/k\sqrt{2^{55}/k}, so that each product modulo tt is exact by Theorem 1, computes one flint_dgemm per 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_blas uses a single flint_dgemm when the entries of AA and BB have at most aa and bb bits with a+b+⌈log⁡2(k+1)⌉≤53a + b + \lceil \log_2(k+1) \rceil \le 53. Otherwise it reduces the entries modulo primes pp with k((p−1)/2)2<253k ((p - 1)/2)^2 < 2^{53}, multiplies modulo each prime with flint_dgemm and reconstructs the result with a multi-modular algorithm.
  • nmod_mat_mul_u8 handles moduli p≤255p \le 255 with entries stored in bytes. For moduli above 16, the range of its byte-shuffle kernels, it converts entries in [0,p)[0, p) to floats and calls flint_sgemm on blocks of kmax⁡=⌊(224−1)/(p−1)2⌋k_{\max} = \lfloor (2^{24} - 1)/(p - 1)^2 \rfloor 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
Cite this page

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).