Dot products modulo a word-size integer
- Cost
- One product and one to three word additions per term, plus one reduction of at most three words per dot product.
Consider a dot product modulo a word-size integer: is the modulus, are reduced entries, is the length, and
is the unreduced dot product. FLINT never reduces modulo inside the loop. It computes exactly, or a value congruent to that fits in fewer words, and reduces once at the end with NMOD_RED (one word, Barrett), NMOD2_RED2 or NMOD_RED3 (two or three words, Möller–Granlund). This is the technique of delayed modular reduction, systematically used in exact linear algebra since FFLAS [DGP2002; DGP2008]. Those libraries compute in floating-point arithmetic, which is exact while . FLINT works with integer words, so the question is how many words needs.
How many words#
Lemma 1. , and the upper bound is attained when all entries equal .
The proof is immediate.
Theorem 2 (Limb count). Let . Every dot product of length of reduced entries fits in words, that is , if and only if
Proof. By Lemma 1, the condition is , that is with . If is an integer, this says . Otherwise it says , that is .
Some consequences, for :
- Since and , three words always suffice. If , two words always suffice.
- For , one word suffices exactly when .
- The thresholds for one and two words are, for example:
| one word: | two words: | |
|---|---|---|
| 2 | 3037000500 | 13043817825332782213 |
| 8 | 1518500250 | 6521908912666391107 |
| 10 | 1358187914 | 5833372668713515885 |
| 100 | 429496730 | 1844674407370955162 |
| 1000 | 135818792 | 583337266871351589 |
| 4294968 | 18446744073709552 |
_nmod_vec_dot_params contains these thresholds as constants for (for and ). They agree with Theorem 2 in every case. For longer lengths it computes the three-word product directly, as does _nmod_vec_dot_bound_limbs.
Choice of method#
_nmod_vec_dot_params returns one of the values of dot_method_t. The values are ordered so that the number of words of can be read off the method, which _nmod_vec_dot_bound_limbs_from_params does. They are tried in this order:
_DOT0if or . The result is 0._DOT_POW2if is a power of 2 (see One word).- If :
_DOT1,_DOT2or_DOT3according to the number of words, by Theorem 2. These are handled by unrolled inline code. - If :
_DOT1if one word suffices. Otherwise, on 64-bit machines,_DOT2_SPLITif , and_DOT2_HALFin the remaining cases (always on 32-bit machines). On AArch64,_DOT2is used instead of_DOT2_HALF, and also instead of_DOT2_SPLITwhen . - If two words suffice:
_DOT_U52,_DOT_SPLIT_LIMBSor_DOT_U64if available and the length is large enough (see SIMD kernels for moduli above ), and otherwise_DOT2. - If three words are needed:
_DOT3_SPLIT_LIMBSor_DOT3_U64if available and the length is large enough, otherwise_DOT3_ACCif ( on 32-bit machines), otherwise_DOT3.
The parameters are valid for every length up to , because each method's conditions only get weaker as the length decreases. When a SIMD method is used with a length below its threshold, _nmod_vec_dot and _nmod_vec_dot_rev fall back to the scalar code of the same band (_DOT2, _DOT3_ACC or _DOT3). _nmod_vec_dot_ptr and NMOD_VEC_DOT always use this scalar code instead of the SIMD methods for .
One word#
If , the products and their sum fit in one word and are added with ordinary arithmetic (_DOT1). On 64-bit machines with AVX2, _nmod_vec_dot1 keeps four sums in the lanes of a vector register and multiplies with vpmuludq, which forms the 64-bit product of the low 32 bits of each lane. This is exact because one word only suffices for when . Each lane holds a partial sum of , so the lanes cannot overflow.
If is a power of two, divides , so reduction modulo commutes with reduction modulo . The sum can then wrap around freely and one word always suffices (_DOT_POW2). For the vectorised _nmod_vec_dot1 is used; for larger powers of two, _nmod_vec_dot_pow2 uses plain scalar code.
Two and three words#
For the products need two words. _DOT2 computes each product with umul_ppmm and adds it to a two-word sum with add_ssaaaa. It uses two independent sums for the even and odd terms, so that consecutive additions with carry do not wait for each other, and adds them at the end.
_DOT2_HALF is for when needs two words. The products fit in one word and are added to a two-word sum.
_DOT3 adds each two-word product to a three-word sum. _DOT3_ACC saves work by adding blocks of 8 products in two words and only the block sums in three words.
Proposition 3. The two-word block sums of _DOT3_ACC cannot overflow if and only if , that is for and for .
Proof. A block sum of 8 products is at most , with equality for entries . The numerical bounds are Theorem 2 with and .
The final reduction of a three-word sum , where , first reduces modulo and then performs two two-by-one divisions (NMOD_RED3).
Splitting at bit 56#
For somewhat below , the two-word sum of _DOT2_HALF can be avoided altogether. On 64-bit machines, _DOT2_SPLIT keeps as in two words and , moving the high bits of into after every 8 terms. At the end, is replaced by its residue , computed in _nmod_vec_dot_params and stored in pow2_precomp, and the single word is reduced. This needs only one-word additions, which vectorise well.
Input: reduced for , and .
Output: .
- , .
- For each full block of 8 terms: add the 8 products to ; then and .
- Add the remaining products to .
- Return , where is computed as a two-word integer: if its high word is 0 it is reduced with a one-word reduction, otherwise with a two-word reduction.
The AVX2 version runs this in four lanes, so that each lane receives 8 products per block of 32 terms. It folds and sums the lanes before the last products are added.
Theorem 4. Let and let be the largest number of products added in step 3, so for the scalar code and for the AVX2 code. Algorithm 1 computes without overflow if
The first condition is equivalent to . At , the second holds for , so it puts no practical constraint on the length.
Proof. Throughout the algorithm equals the sum of the products added so far, since step 2 only moves multiples of from to . At the start of each block , so after 8 more products . This is below if and only if , which gives the first condition. Since , it is equivalent to . The same argument covers step 3 and each lane of the AVX2 code, where at most 8 products are added between two folds.
In step 4, by the second condition, so does not overflow, and . Since ,
so the high word of the two-word integer is less than , as required by the two-word reduction (when it is 0, a one-word reduction gives the same result). Finally .
The split at 56 bits leaves room for the 8 products of a block, which determines the bound on ; a smaller split point would allow slightly larger moduli. The table in src/nmod_vec/dot.c gives the moduli bounds for split points from 40 to 63 bits (its length columns refer to an earlier version that computed in a single word).
_nmod_vec_dot_params selects _DOT2_SPLIT whenever .
Splitting each product#
On 64-bit machines with AVX2, _nmod_vec_dot2_half (but not its _rev and _ptr forms) applies a variant of the split to . Here a product can have up to 64 bits, so each product is itself split: goes to a high sum and to a low sum . The low lanes are folded every 256 terms. At the end the result is again with .
Theorem 5. With and , _nmod_vec_dot2_half with AVX2 is correct if . This holds for every when .
Proof. The invariant (sum of the products so far) holds as before. Each of the four lanes of is below after a fold and receives 64 values below per block of 256 terms, so it stays below . After the last fold, at most 63 more values go into each lane, so the horizontal sum of the lanes is below . After a final fold and the low parts of the last three products, . The high sum satisfies , and as in Theorem 4 the two-word integer is below , so it is reduced correctly with a single two-word reduction.
The value of pow2_precomp is computed for _DOT2_HALF exactly when FLINT itself was built with AVX2.
SIMD kernels for moduli above #
For the products have more than 64 bits and the vector multiply instructions of x86-64 cannot form them directly. FLINT splits the entries into smaller pieces whose products fit in vector lanes, accumulates the pieces in separate lanes, and recombines them into a two- or three-word integer before the final reduction. All three kernels compute exactly, so they serve both the two-word and the three-word band. For a general discussion of modular arithmetic with SIMD instructions, see [HLQ2016].
Split limbs (AVX2, AVX-512)#
_nmod_vec_dot_split_limbs uses vpmuludq ( bits) in 4 lanes (AVX2) or 8 lanes (AVX-512F). Let be the bit length of .
- For , each entry is written with , so and , and . Three accumulators receive the three parts. Each part of a term is at most , where and . After every terms, the high 32 bits of every lane are moved to a second accumulator. A lane is below after such a fold, so it stays below for the next terms. The second accumulators are emptied into a three-word total every 8192 folds, which keeps their lanes below . For example for and for .
- For , the entries are split into 32-bit halves and the four partial products of each term are added as modulo and . The sum of the low halves of the is then , which is exact as long as a lane receives fewer than products. The accumulators are emptied every 8192 iterations.
IFMA kernels (AVX-512)#
The AVX512-IFMA instructions vpmadd52luq and vpmadd52huq multiply the low 52 bits of each lane and add the low or the high 52 bits of the 104-bit product to an accumulator.
_nmod_vec_dot_u52handles , so that whole entries are multiplied, as innmod_mat_mul_u52. The low and high halves are kept in four independent pairs of accumulators to hide the latency of the instructions. Every 126 iterations of 32 terms the pairs are added, and the lanes summed into a two-word total . At most 508 halves below meet in a lane, and , so neither the lanes nor their sum overflow. The total is , so this kernel requires ._nmod_vec_dot_u64handles any . It splits the entries into 32-bit halves and accumulates the low and high 52-bit halves of the four partial products in eight accumulators, emptied into a three-word total every 250 iterations:
The tails of fewer than 8 terms use masked loads.
When the kernels are used#
The kernels are compiled only for , split_limbs with AVX2 and the IFMA kernels with AVX-512F, AVX-512DQ and AVX512-IFMA, and neither when FLINT_MACHINE_VECTORS_FORCE_GENERIC or FLINT_MACHINE_VECTORS_STRICT_C is defined. In the two-word band, _nmod_vec_dot_params tries _DOT_U52, then _DOT_SPLIT_LIMBS, then _DOT_U64; in the three-word band _DOT3_SPLIT_LIMBS, then _DOT3_U64. Each is subject to these parameters:
| Parameter | Defined in | Meaning |
|---|---|---|
FLINT_NMOD_VEC_DOT_U52_MIN_LEN | flint-mparam.h | least length for _DOT_U52 (32 or 40 in the shipped files) |
FLINT_NMOD_VEC_DOT_SPLIT_LIMBS_MIN_LEN | flint-mparam.h | least length for split limbs; 0 disables them (48 on Zen 4 up to 160 on Meteor Lake, 0 on Broadwell, 96 by default) |
FLINT_NMOD_VEC_DOT_SPLIT_LIMBS_MAX_BITS | flint-mparam.h | split limbs only for ; 64 or more means no limit (60 to 64 in the shipped files; 60 on IFMA machines, where _DOT_U64 is faster above) |
NMOD_VEC_DOT_U64_MIN_LEN | nmod_vec.h | least length for _DOT_U64, and only for (80) |
NMOD_VEC_DOT3_U64_MIN_LEN | nmod_vec.h | least length for _DOT3_U64 (48) |
NMOD_VEC_DOT2_SPLIT_MAX_LEN | nmod_vec.h | AArch64 only: _DOT2 instead of _DOT2_SPLIT from this length (160) |
The _ptr forms of the kernels exist but are not selected, since loading vec2[i][offset] needs gathers, which measured slower than the scalar code.
Use in FLINT#
Dot products with these parameters are the inner loop of classical polynomial multiplication (_nmod_poly_mul_classical, mullow, mulmid), power series inversion and division, classical matrix multiplication (Multiplying matrices), triangular solving, LU decomposition with delayed reduction (nmod_mat_lu_classical_delayed) and the characteristic polynomial in nmod_mat, and of the gr contexts for , whose dot product methods are called by the generic code through _gr_vec_dot (Dot products).
The same bound governs algorithms that delay reductions without forming dot products. nmod_mat_lu_classical_delayed reads the number of words from the method returned by _nmod_vec_dot_params and then updates rows with the _nmod_vec_nored functions in one, two or three words (Delayed reduction). _nmod_poly_divrem_basecase does the same with a bit count.
For moduli of several limbs, mpn_mod accumulates dot products in to limbs in the same way (delayed reduction for multiword moduli).
Referenced from#
- Choosing a method for modular arithmetic (overview)
- Division by an invariant word (algorithm)
- Delayed reduction for multiword moduli (algorithm)
- Dot products of balls (algorithm)
- Contexts, method tables and specialisation (algorithm)
- Characteristic and minimal polynomials (algorithm)
- Gaussian elimination and LU decomposition (algorithm)
- Floating-point matrix multiplication (gemm) (algorithm)
- Multiplying matrices over Z/nZ (overview)
- Matrix multiplication kernels modulo n (algorithm)
- Error terms of dot products, matrix products and polynomial products (algorithm)
- Middle products and the transposition principle (algorithm)
- Multiplying polynomials over Z/nZ (overview)
- Dot products
nmod_mat_mul_classicalnmod_mat_nmod_vec_mul,nmod_mat_nmod_vec_mul_ptr- Multiplying matrices
- Computing dot products
- nmod_vec.h (module)
_nmod_vec_dot2_split,_nmod_vec_dot2_split_rev,_nmod_vec_dot2_split_ptr_nmod_vec_dot2_half,_nmod_vec_dot2_half_rev,_nmod_vec_dot2_half_ptr- Matrix–vector products modulo p (tutorial)
References#
- [DGP2002] J.-G. Dumas, T. Gautier and C. Pernet, "Finite field linear algebra subroutines", Proceedings of the 2002 International Symposium on Symbolic and Algebraic Computation (ISSAC) (2002), 63-74, https://doi.org/10.1145/780506.780515
- [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
- [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
The FLINT team, “Dot products modulo a word-size integer,” FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev, 2026. https://flintlib.org/doc/algorithms/linear-algebra/dot-products.html (generated 2026-11-02).
@misc{flint-doc-algorithms-linear-algebra-dot-products,
author = {{The FLINT team}},
title = {{Dot products modulo a word-size integer}},
howpublished = {FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev},
year = {2026},
url = {https://flintlib.org/doc/algorithms/linear-algebra/dot-products.html},
note = {Generated 2026-11-02}
}