Matrix multiplication kernels modulo n
- Cost
- vector multiply-adds (u32, u52 in single mode), two (u52), three (k52) or about six (fp50) per product, plus reductions;
nmod_mat_mul_blascosts one to twelve floating-point products.
The blocked kernels described below are those behind nmod_mat_mul for moduli ; the floating-point method for larger moduli is described after them. Throughout, is , is , and the entries are residues in . The kernels have the same structure as floating-point gemm kernels (Floating-point matrix multiplication (gemm)); what differs is how the products are represented and when they are reduced. The bounds that make delayed reduction possible are summarised in Bounds that make delayed reduction exact.
Common structure#
The four kernels nmod_mat_mul_u32, nmod_mat_mul_u52, nmod_mat_mul_k52 and nmod_mat_mul_fp50 are instances of one template, src/nmod_mat/mul_blocked_templ.h, organised as in GotoBLAS and BLIS [GvdG2008; VZvdG2015]:
- is computed in blocks of steps of the inner dimension, rows of and columns of . Each block of and is packed into a contiguous buffer, in the order in which the micro-kernel reads it, after converting the entries to the kernel's representation.
- The micro-kernel keeps an tile of in vector registers as accumulators of lanes. Each of the steps broadcasts one entry of per row and multiplies it by vectors of .
- At the end of each block of steps, the accumulators are reduced to residues in and stored in , after adding the previous contents of for all blocks but the first. itself thus carries the partial sums from one block to the next, and no wide temporary copy of is needed.
- With several threads, is split along its longer side into blocks of whole tiles, one per thread, each computed by the serial code; at most one thread is used per products . Every thread packs the shared operand itself.
The cost is multiply-accumulate steps of the kernel and reductions. Since a reduction is a dozen vector operations, it costs a few percent of the total.
Integer kernel for #
nmod_mat_mul_u32 lifts each entry to the signed 32-bit integer with , and multiplies with the widening -bit instructions available on all SIMD instruction sets (vpmuldq on x86-64, smlal on AArch64). A product is then at most in absolute value, and the accumulators are signed 64-bit lanes.
When an accumulator could overflow, it is folded: with , write with (so is the arithmetic right shift of by 32) and replace by . This costs a shift, a mask, one widening multiplication and an addition.
Proposition 1. If , then . A second fold of gives a value of absolute value at most .
Proof. and , so . For the second fold, by the same decomposition.
The kernel uses one fold if , which happens when is small, as for or close to , and two folds otherwise; let be the corresponding bound. An accumulator starts each run from a fold or from an entry of , so it stays below in absolute value for
steps, after which it is folded again. This cadence is 256, so that no fold is needed inside a block, for up to about ; it is 128 at 29 bits, 32 at 30 bits, 8 for and 2 for close to . At the end of a block, is reduced exactly: the quotient is computed in double precision, and , which lies in , is corrected by one addition or subtraction of . The kernel is correct for every with no case analysis; it slows down only near , where the folds become frequent.
The same kernel serves _nmod_mat_mul_u32 on arrays of 32-bit entries, such as those of the nmod32 contexts of gr.
IFMA kernel for #
The AVX512-IFMA instructions vpmadd52luq and vpmadd52huq multiply the low 52 bits of two 64-bit lanes and add the low, respectively high, 52 bits of the 104-bit product to an accumulator. For entries this is the exact product , with . Two accumulators per lane, for the low and high halves, can absorb such halves without overflowing 64 bits, so a block of steps needs no reduction at all, and the entries need no lifting.
For the products are below , the high halves are zero, and nmod_mat_mul_u52 uses a second micro-kernel with only the low accumulator: one instruction per product and twice as many rows per tile. In this mode it is faster than nmod_mat_mul_u32, which needs a multiplication and an addition per product; in the two-accumulator mode it is slower than nmod_mat_mul_u32 up to about 30 bits, which is why nmod_mat_mul uses it only from 31 bits on (FLINT_NMOD_MAT_MUL_U52_MIN_BITS).
At the end of a block, the two sums represent , which is reduced as follows, with :
- , by a Barrett step through double precision;
- , up to one multiple of , by Shoup multiplication with the precomputed quotient , the two 104-bit products coming from IFMA instructions;
- , by a second Barrett step.
The double-precision quotients are off by less than one in each Barrett step, so one correction on each side suffices.
Two-limb Karatsuba kernel for #
Without IFMA, nmod_mat_mul_k52 reaches 52-bit moduli with 32-bit multiplications. Each entry is lifted to and split as
and a product is . The middle coefficient is obtained by Karatsuba's trick, so three accumulators receive , and : three widening multiply-adds per product instead of four. Each of the three partial products is at most in absolute value, so 256 of them sum to less than in a signed 64-bit lane: no reduction is needed inside a block.
At the end of a block, each sum is first brought to : with , the integers and are exact as doubles, and with is reduced with the floating-point modular multiplication described next. The three residues are then combined as with two more floating-point modular multiplications, and the result is reduced to . The floating-point reduction is what limits the kernel to .
Floating-point kernel for #
nmod_mat_mul_fp50 stores the lifted entries as doubles and reduces every product as it is formed, with the floating-point modular multiplication of fft_small [HLQ2016]:
where and are computed exactly with fused multiply-adds. For the result satisfies and . The values are summed in a double accumulator, which is exact while it stays below . Starting from , this holds for steps, after which the accumulator is brought back to by subtracting . The cadence is at least 256, so that no fold is needed inside a block, for , and it is 6 for close to .
The kernel costs about six floating-point operations per product, against two integer operations for nmod_mat_mul_u32 and three for nmod_mat_mul_k52, but it keeps a single accumulator per entry of the tile where nmod_mat_mul_k52 needs three. On x86-64 this makes it the faster of the two up to ; on AArch64, where a widening multiply-add is a single instruction, nmod_mat_mul_k52 is faster.
Floating-point gemm and Chinese remaindering#
nmod_mat_mul_blas uses flint_sgemm or flint_dgemm, which call an external BLAS when FLINT is configured with one and FLINT's own kernels (Floating-point matrix multiplication (gemm)) otherwise. The entries are lifted to and converted to floating point, the product is computed, and each entry of the result is converted back to an integer and reduced modulo . With :
- if , one
sgemmis exact; - if , one
dgemmis exact; - otherwise the method is multimodular.
In the multimodular case, the entries of over the integers, with the original representatives in , lie in . FLINT chooses pairwise coprime integers , starting from and decreasing, until . With entries lifted to , the product modulo is exact in double precision, since . At most 12 moduli are allowed; otherwise, for an extremely large , the function returns 0. Typical counts are 4 moduli for 32-bit , 6 for 52-bit and 7 for 64-bit at .
Input: residues of an integer , for (sorted increasingly).
Output: .
- Compute the digits and, for , , using precomputed inverses.
- Return , accumulated in two words and reduced once.
This is Garner's algorithm [Gar1959], see also [GG2003, §5.6]. Since with exactly, the result is correct. The conversions and the reconstruction are distributed over the thread pool; before each gemm, the worker threads are returned to the pool so that FLINT's own gemm can use them.
nmod_mat_mul uses this method for moduli below only when one dgemm suffices and the dimension is above FLINT_NMOD_MAT_MUL_BLAS_1PASS_CUTOFF, where a tuned BLAS can beat nmod_mat_mul_u32. For moduli above , where no SIMD kernel exists, it uses it from dimension 450 (with one thread), where it beats classical multiplication combined with Strassen–Winograd.
Thin matrices#
The kernels pad the last tile of to rows (4 to 14) and columns (8 to 24), which wastes most of the work when has very few rows or very few columns. nmod_mat_mul therefore uses two other methods for such shapes when a vector code path exists for .
- Few rows (at most 4, or 8 with IFMA): the product is computed as a sum of the rows of multiplied by the entries of , reading each row of once for all rows of . The accumulators stay in vector registers (or, for very wide , in the level 1 cache). With IFMA, low and high 52-bit halves are accumulated and reduced every 511 rows; with AVX2 or AVX-512 and , products are split at bit 56 as in Splitting at bit 56 and folded every 255 rows; otherwise, for , the floating-point modular multiplication is used with a fold every four rows. For the larger moduli (with AVX2 or AVX-512; above with IFMA) the entries are split into two limbs of at most 32 bits whose products are accumulated in three to six vector lanes per entry of , as in Split limbs (AVX2, AVX-512), carried into each other every few rows (for , every rows or so, the bit length of ) or never (32-bit limbs, larger ), and each entry is reduced once from up to three words at the end; when takes more than 512 KB (half of a level 2 cache of 1 MB, a compile-time parameter), the rows go 16 at a time and the accumulators are kept in the level 2 cache in between.
nmod_mat_muluses it when has at least 6 columns. This is also the algorithm ofnmod_mat_nmod_vec_mul(for wide enough). - Few columns (2 to 4, or 2 to 8 with IFMA): the columns of are transposed once, and each row of is multiplied with all of them as simultaneous dot products that share the loads of the row, with the IFMA dot product kernels of
nmod_vec(IFMA kernels (AVX-512)) or the floating-point modular multiplication.
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
- [HLQ2016] J. van der Hoeven, G. Lecerf and G. Quintin, "Modular SIMD arithmetic in Mathemagix", ACM Transactions on Mathematical Software 43:1 (2016), article 5, https://doi.org/10.1145/2876503
- [Gar1959] H. L. Garner. The residue number system. IRE Transactions on Electronic Computers EC-8:2 (1959), 140–147. https://doi.org/10.1109/TEC.1959.5219515
- [GG2003] J. von zur Gathen and J. Gerhard, Modern Computer Algebra, second edition, Cambridge University Press (2003)
The FLINT team, “Matrix multiplication kernels modulo n,” FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev, 2026. https://flintlib.org/doc/algorithms/linear-algebra/modular-matrix-kernels.html (generated 2026-11-02).
@misc{flint-doc-algorithms-linear-algebra-modular-matrix-kernels,
author = {{The FLINT team}},
title = {{Matrix multiplication kernels modulo n}},
howpublished = {FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev},
year = {2026},
url = {https://flintlib.org/doc/algorithms/linear-algebra/modular-matrix-kernels.html},
note = {Generated 2026-11-02}
}