Repository navigation
Speed up and reduce the memory of Groebner bases over QQ - #383
Merged
Merged
Conversation
Rework the storage used while lifting Groebner bases over the rationals (msolve -g 2, export_groebner_qq): - store the modular images of each polynomial in one contiguous prime-major block instead of one malloc-ed array per term; - grow this block by max(4, nprimes/16) primes instead of 16*nthrds: the multi-modular loop is mono-threaded, so with -t N most of the allocated images were never used; - drop the per-term array of CRT images (cf_zz): each coefficient is now lifted by multi-CRT into a scratch integer and immediately rationally reconstructed; - store only integer numerators in cf_qq: once lifted, all denominators were set to 1 (the common denominator is kept in lm); - compute num * (lcm/den) instead of (num * lcm) / den so that the result is not over-allocated. Also fix the restart paths of core_groebner_qq (uninitialized dlift->S passed to gb_modpoly_realloc, double free of already lifted polynomials, leaked hm/cf_64/lead monomials, stale tracer reused when learning again), a NULL dereference when F4 detects a bad prime while applying the tracer, and a leak of trace data in duplicate_tracer. On Katsura-10 the peak heap goes from 103MB to 73MB with one thread and the peak RSS from 244MB to 70MB with -t 8; on Katsura-11 the peak RSS goes from 458MB to 308MB. Outputs are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017J51JHn4LH2YArUKp1VBjv
- free the reducer rows created by symbolic preprocessing in the final reduction step (they were leaked at every F4 run with reduce_gb set, i.e. once per prime in multi-modular computations); - when a bad prime is detected while applying the tracer, release the local meta data, the matrix and its new rows (finalize_f4 is not called on this path); - copy only the used part (eld entries) of the hash table in copy_hash_table, which is called once per prime in the application phase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017J51JHn4LH2YArUKp1VBjv
Rational reconstruction now goes through FLINT (fmpq_reconstruct_fmpz_2, half-gcd based) instead of the quadratic Euclidean algorithm whenever the bounds allow it, falling back to ratrecon/ratreconwden otherwise. The number of primes needed is also reduced and made reproducible: - three witness coefficients are reconstructed and checked per polynomial (the last nonzero one and the ones found scanning downwards from 2*len/3 and len/2) instead of one, so that a spurious witness reconstruction is detected before a full lift is attempted; - the denominator used as a guess is taken from the previous polynomial (its lifted denominator, or the one of its witnesses once verified) and combined with the first witness denominator for the next witnesses; it is no longer reset to 1 after a failed lift or a failed verification; - the full lift of a polynomial reuses the denominator accumulated so far as a guess, so that only the missing factor is reconstructed for each coefficient; - reconstruction bounds keep 16 bits of slack, which makes spurious reconstructions unlikely; - witnesses are reconstructed every rr primes, rr being the largest power of 2 <= nprimes/128, instead of a period depending on timings, so that the number of primes no longer depends on the machine load. On 31 benchmark systems (5 random seeds each, single thread) the outputs are unchanged and the number of primes decreases on 27 of them (up to 26%), while being much more stable across seeds. The average speedup (geometric mean over all runs) is 1.29, and the speedup on the total time of the benchmark suite is 1.25. The largest speedups are reached on the MinRank (minrank) family, up to 1.91, on the critical point (cp_d) family, up to 1.79, and on the dense random (random2) family, up to 1.71. The absolute time saved grows with the problem size, and on the eco and cp_d families so does the speedup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a denominator g is guessed, the value reconstructed is u * g, whose numerator is about g times the denominator still missing. The bounds were so far either balanced (N = D = sqrt(M), with M = mod_p / 2^(1 + RR_MARGIN)) or, once lstart > 0, fixed to D = M^(1/3) and N = M^(2/3) whatever the size of g. They are now set to D = sqrt(M / g) and N = M / D, and recomputed whenever the guess changes: for each witness coefficient, and during the full lift of a polynomial each time the accumulated denominator grows. Balanced bounds are kept for the reconstructions done without a guess. On 38 benchmark systems over QQ (5 random seeds each, single thread) the outputs are unchanged and the number of primes decreases on 31 of them (up to 33%), the other 7 being unchanged. Compared with the previous commit, the average speedup (geometric mean over all runs) is 1.24 and the speedup on the total time of the benchmark suite is 1.28; compared with v0.11.0 these are 1.76 and 1.78. The largest speedups over the previous commit are reached on the dense random families (random2 up to 1.57, random3 up to 1.51), on the MinRank family (up to 1.54) and on the critical point families (cp_d and crit, up to 1.48). Compared with v0.11.0, the speedups are between 1.95 and 3.00 on these families (random2 up to 3.00, and 5.48 on the small random2_6_6; MinRank up to 2.55, cp_d 2.54, crit 2.52, random3 2.41), and between 1.0 and 1.43 on the other ones (kat up to 1.43, chandra 1.39, eco 1.27, bruno 1.27, zimmermann 1.22, henrion 1.17, vor1 1.07, cyclic 1.05, noon 1.07, KLYZ22 1.04). Peak memory decreases on most systems, and is up to 4 times smaller than with v0.11.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR speeds up the computation of Groebner bases over QQ (
msolve -g 2) and reduces its memory use. Most of the gain comes from needing fewer primes and from cheaper rational reconstruction. Peak memory is lower because the modular images are stored more compactly.On 38 benchmark systems over QQ (5 random seeds each, single thread), compared with v0.11.0:
The largest speedups are on systems that need many primes: MinRank, critical points (cp_d, crit) and dense random systems (random2, random3), where the new version is 1.95 to 3.00 times faster. Systems that need few primes (eco, kat, noon, cyclic) gain 1.00 to 1.43 in time, but use 1.4 to 4 times less memory.
Within a family, the absolute time saved grows with the problem size, and so does the memory gain on most families (random2: 1.82×, 2.03× and 2.18× less memory from random2_7_7 to random2_9_9). The speedup itself grows with the size on the eco (1.13× to 1.27× from eco12 to eco14), chandra and random3 families, stays roughly constant on cp_d, and slightly decreases on MinRank and random2 (3.00× on random2_7_7, 2.61× on random2_9_9), whose smaller systems benefit most from the cheaper reconstruction.
Changes
The PR contains four commits, all on the multi-modular path (
src/msolve/lifting-gb.candsrc/msolve/duplicate.c, plus F4 fixes insrc/neogb/f4.candsrc/neogb/hash.c).1. Reduce memory used by multi-modular Groebner bases over QQ
max(4, nprimes/16)primes instead of16*nthrds, since the multi-modular loop is single-threaded and most of the images allocated for-t Nwere never used.This commit also fixes several bugs in the restart paths of
core_groebner_qq(an uninitialized size, a double free, leaks, a stale tracer), a NULL dereference when F4 detects a bad prime while applying the tracer, and a leak induplicate_tracer.2. Fix per-prime leaks in F4 and avoid touching unused hash table memory
copy_hash_table, called once per prime, now copies only the used part of the table.3. Speed up the lifting of Groebner bases over QQ
fmpq_reconstruct_fmpz_2instead of the quadratic Euclidean algorithm, with a fallback whenever the bounds do not meet FLINT's requirement2ND < m.2*len/3and fromlen/2. A spurious reconstruction is thus detected before an expensive full lift is attempted.rrprimes,rrbeing the largest power of 2<= nprimes/128. Previously the period depended on timings, so the number of primes depended on the machine load. It is now reproducible.4. Choose rational reconstruction bounds from the guessed denominator
When a denominator
gis guessed, the value reconstructed isu * g. Its numerator is aboutgtimes the denominator still missing, and the missing denominator is small when the guess is good.So far the bounds were either balanced (
N = D = sqrt(M), withM = mod_p / 2^(1+RR_MARGIN)) or fixed toD = M^(1/3)andN = M^(2/3), whatever the size ofg. They are now set toD = sqrt(M / g)andN = M / D, and recomputed whenever the guess changes. SinceN * Dstays the same, the bound on the numerator grows withginstead of being wasted on the denominator. Successful reconstructions therefore happen with fewer primes, by about a third on the systems that need many primes.Benchmarks
Setup:
msolve -g 2 -t 1 --random-seed swith seeds 1 to 5./usr/bin/time.System selection: systems over QQ taking between 1 s (with v0.11.0) and 20 min (with the new version), screened family by family in increasing size.
v0.11.0 baseline: the only other changes on master since v0.11.0 are in the term parser and the choice of sorting routine, so they don't affect these measurements.
Overall results:
Results system per system
The table is sorted by the time of the new version. Times are in seconds and peak memory in MB; "speedup" and "mem ratio" are v0.11.0 divided by new.
The changes in this PR were made with the assistance of generative AI (Claude Code).
🤖 Generated with Claude Code