math: in-repo Lehmer 3072-bit modular inverse + std ceil_log_2 - #1051
math: in-repo Lehmer 3072-bit modular inverse + std ceil_log_2#1051biryukovmaxim wants to merge 13 commits into
Conversation
…c locator Branchless ilog2-based helper for the sync block-locator capacity hint; removes the consensus locator's only malachite-base import.
core::hint::cold_path, used by the modular-inverse routine added next, is stable as of 1.95.
Stack-allocated variable-time modular inverse for the MuHash element via Lehmer's extended GCD (Knuth TAOCP 4.5.2 Algorithm L): a two-word half-GCD guess peels ~62 bits per 2x2 reduction matrix, with an exact Euclidean step as the fallback. 100% safe Rust with no new dependencies; cross-checked bit-for-bit against the existing malachite mod_inverse on 3072-bit vectors. The optional lehmer-instrument feature exposes per-inverse op counters.
Criterion comparison of the in-repo Lehmer inverse against malachite (the current implementation), dashu, and crypto-bigint's safegcd, alongside the other production big-uint ops (blue-work sum, difficulty window, calc_work). Every candidate is asserted equal to the current malachite result before it is timed. dashu-int and crypto-bigint are bench-only dev-dependencies.
examples/lehmer_stats.rs reports the operation breakdown (matrices, divides, apply-limb counts) for lehmer::invert under the lehmer-instrument feature, to show where the per-inverse work goes. Inert when the feature is off.
Match the workspace MSRV: the Lints job pinned rustc 1.93.0 and the docker chef images pinned 1.91, both below 1.95.0, so cargo aborted with "requires rustc 1.95.0". rust:1.95-alpine3.23 confirmed on Docker Hub.
… deps Comment out the dashu/crypto-bigint/num-bigint/ruint candidate arms and remove the dashu-int + crypto-bigint dev-deps; drop the dashu-comparison production-pattern groups. Bench now covers only the two malachite ops replaced on this branch (mod_inverse vs lehmer, ceil_log2 vs std).
…-dep - muhash 3072-bit inverse() now calls lehmer::invert; generic Uint::mod_inverse removed - malachite dropped from production deps and the uint.rs re-export; kept only as a dev-dep cross-check oracle for the lehmer/lib tests and the candidates bench - fuzz: add math/fuzz u3072 target (lehmer::invert vs num-bigint, ran clean 61k execs); remove the mod_inv arms from u128/u192/u256 - bench.rs mod_inv arm and test oracles repointed off the removed method
| pub const fn ceil_log_2(x: u64) -> u64 { | ||
| // power of two -> floor; not a power of two -> floor + 1 | ||
| x.ilog2() as u64 + (!x.is_power_of_two()) as u64 | ||
| } |
There was a problem hiding this comment.
You mentioned branchless, note that this isn't actually branchless, as ilog2 panics if x == 0
see assembly: https://godbolt.org/z/E8GasceMx
This function is basically the same as:
pub fn ceil_log_2(x: u64) -> u32 {
assert!(x > 0);
u64::BITS - (x - 1).leading_zeros()
}There was a problem hiding this comment.
The original one also panicked, we can either panic, return Option, or make the function return 0 on a 0 input
There was a problem hiding this comment.
we have only one caller of the function, which never passes zero. returning zero sounds fine to me
| } | ||
|
|
||
| /// Limb count of the operands (3072 bits in base 2^64). | ||
| const N: usize = 48; |
There was a problem hiding this comment.
can we make this a const generic to support fuzzing with u128/u192 etc.?
There was a problem hiding this comment.
there are two places where I use exact number:
div_rem_step and reduce_mod
i think it would require to either convert the whole function to be the method of structure so we can get access to corresponding uint type or pass a closure/fn pointer to calc div_rem and mod
or we can add one more generic with concrete Uint type so we can get these function from there.
any suggestions, other ideas?
|
(pls coordinate the merge of this pr bcs i think such a change justifies a master testing period) |
|
I let claude+codex grind for a while over this, they didn't find any issues with it, but It's a lot of logic to verify, I think the following can make me more comfortable with those:
I wonder also how it fairs against using https://github.com/mhogrefe/malachite/blob/master/malachite-nz/src/natural/arithmetic/gcd/extended_gcd.rs but replacing heap allocs with stack allocs, but this leaves us with the licensing issue |
bf7af9f to
a4be7a5
Compare
- LimbArray/LehmerOps/LehmerInvert in lehmer.rs; construct_uint! impls LehmerOps, plus primitive u64/u128 impls (native-division oracles) - construct_uint! made $crate-hygienic (js_sys/wasm_bindgen/kaspa_utils/serde) so external crates can use it standalone - pre-trait fixed-48 snapshot kept in benches/support as a candidates bench arm for A/B and asm diffing; bench parity within noise - width parity tests: exhaustive u64 sweep, u128/Uint192/256/320 vs malachite
- u128/u192/u256 fuzz both value and modulus full-width against a shared num-bigint oracle in utils.rs - u128 also cross-checks the primitive u128 impl
Replace ilog2() (which panics on 0) with leading_zeros() arithmetic, guarded to return 0 when x=0. Eliminates panic case and maintains performance.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Addressing fuzzing/coverage/asserts points: 1. Generalized to smaller widthsDone in recent force-push. Lehmer is now generic via 2. Multi-day fuzzing soak with coverage analysisAll four targets ran for 3 days (~72 hours each) with
u3072 ran with Coverage reports (lehmer.rs + uint.rs + int.rs + lib.rs)The uncovered CorpusGrown corpora submitted as kaspanet/rusty-kaspa-corpus#1. 4. Promoted assertsDone. The |
Summary
Adds an in-repo, dependency-free modular inverse for the 3072-bit MuHash element, wires MuHash to use it, and replaces the consensus sync locator's
malachiteCeilingLogBase2with a smallstd-based helper. Together these removemalachitefrom both of its production call sites: it is no longer a production dependency anywhere in the workspace, remaining only as a dev-only cross-check oracle.malachitewas the workspace's only LGPL-3.0 (copyleft) dependency.What's here
kaspa_math::ceil_log_2— branchlessilog2-basedceil(log2(x)); the sync block-locator now uses it instead ofmalachite-base'sCeilingLogBase2.kaspa_math::lehmer— stack-allocated, variable-time modular inverse via Lehmer's extended GCD (Knuth TAOCP 4.5.2 Algorithm L). A two-word half-GCD guess peels ~62 bits per 2x2 reduction matrix, with an exact Euclidean step as the fallback. 100% safe Rust, no new dependencies; measured ~6-8% faster than the currentmalachitemod_inverseat 3072 bits (see Benchmark results).crypto/muhash'sU3072::inverse()now callslehmer::invert, and the genericmalachite-backedUint::mod_inverseis removed (it had no other production caller).malachitedropped from production — removed frommath's[dependencies]and from thepub use malachite_*re-export inuint.rs. It is kept only as a[dev-dependency], used as the bit-for-bit cross-check oracle for thelehmer.rs/lib.rstests and thecandidatesbench.cargo tree -p kaspa-math -e normal -i malachite-baseis now empty.benches/candidates.rs) — criterion comparison of the Lehmer inverse against the currentmalachitemod_inverse(the operation it replaces); the Lehmer arm is asserted equal to malachite's result before timing. Third-party candidates evaluated during selection (dashu, crypto-bigint safegcd, num-bigint, ruint) are kept commented out with their measured numbers, and their dev-dependencies were dropped once Lehmer won — so the benchmark pulls in no extra dependencies.math/fuzzu3072target driveslehmer::invertfor the MuHash prime and compares bit-for-bit against a num-bigint extended-gcd reference. The generic-mod_inversearms in theu128/u192/u256targets were removed with the method.lehmer-instrumentfeature + example — per-inverse op-count diagnostics; inert when the feature is off.Benchmark results
cargo bench -p kaspa-math --bench candidates -- modinv_3072_muhash_prime(criterion, release, Linux x86_64, 13th Gen Intel Core i9-13900HX). Group time covers N=32 inverses; per-inverse = group / 32. Four consecutive runs:Lehmer lands at ~13.5 µs/inverse vs malachite ~14.5 µs/inverse — roughly 6-8% faster, with non-overlapping 95% confidence intervals on every run, so the speedup is consistent (the exact margin drifts run-to-run with CPU turbo; the lehmer/malachite ratio is the stable figure). The inverse runs once per MuHash finalize, so this is a small but real win on top of dropping the copyleft dependency from the hot path.
Validation
lehmer::invertcross-checked bit-for-bit againstmalachitemod_inverse(now the dev-dep oracle) on 2000 random 3072-bit vectors, plus an oracle-independentv * inv == 1 (mod p)self-check, edge cases (1, p-1, 0), and tiny-value inputs that force the multi-limb-quotientdiv_remfallback.test_inverse/test_inverse_edge_case/test_mul_divnow run throughlehmer::invertand pass.u3072target ran clean — 61,427 executions in 46 s, 0 crashes, 0 mismatches vs the num-bigint reference. The pre-existing muhash multiply/divide fuzz target (which exercises the inverse viainverse()) continues to pass.ceil_log_2checked against an independent oracle across power-of-two boundaries andu64::MAX, and for equivalence with themalachiteroutine it replaces.cargo check --workspace,cargo clippy --workspace --all-targets, andcargo test -p kaspa-math -p kaspa-muhashare clean/green.Notes
core::hint::cold_path, used by the inverse, is stable as of 1.95); CI Lints toolchain and the docker chef images are bumped to match.malachiteis now a dev-dependency only — it backs no production code, just the test/bench cross-check oracle. Theproduct_is_oneself-check, the fuzz target, and the muhash tests give oracle-independent coverage, so the dev-dep can be dropped later with no loss of a production guarantee.Extended fuzzing results (3-day soak)
All four targets ran for 72 hours with
strict-assertsenabled (promoteddebug_assert!toassert!):Coverage reports: gist (u3072, u128, u192, u256)
Grown corpora: kaspanet/rusty-kaspa-corpus#1