Skip to content

Add experimental CRT-based SNF fast path#20

Merged
events555 merged 15 commits into
devfrom
crt
Jun 6, 2026
Merged

Add experimental CRT-based SNF fast path#20
events555 merged 15 commits into
devfrom
crt

Conversation

@events555

Copy link
Copy Markdown
Owner

Summary

Add an experimental, more performant CRT-based SNF algorithm: factor the modulus and solve a local SNF over each prime power via valuation-pivoted blocked LU, then CRT-recombine.

Changelog

  • ca21ba0 feat: add CRT-style non-recursive SNF path
  • d6ef56f chore: add CRT SNF prototype and profiling scripts
  • 0041c2d docs: replace PLAN.md with CRT optimization roadmap
  • c2ca597 perf(crt): i64 fast-path reduction in local_snf hot loops (Phase 1)
  • 4269691 perf(crt): split local_snf into two-phase column/row elimination (Phase 2a)
  • 209e538 docs: record Phase 2a done + detailed Phase 2b blocked-GEMM design
  • eaf4db6 perf(crt): blocked right-looking LU with GEMM trailing update (Phase 2b)
  • 072fa8d docs: mark Phase 2b done; note tiled/BLAS GEMM kernel as Phase 2c
  • 0a555e8 perf(crt): packed cache-friendly i64 GEMM micro-kernel (Phase 2c)
  • e35431e docs: mark Phase 2c done (packed i64 GEMM kernel)
  • 7af0e6e fix(snf): keep transforms unimodular for tall rank-deficient inputs
  • aa73c42 refactor(crt): remove dev scaffolding and tidy the implementation
  • 3e87fc9 refactor: collapse the Python package into a thin Rust binding
  • f187bbb test: replace Python correctness suite with PyO3 boundary tests
  • 2f6a472 docs: position CRT as experimental fast path; reflect thin wrapper

claude and others added 15 commits June 2, 2026 22:59
Add a Chinese Remainder Theorem based Smith Normal Form that avoids the
recursive divide-and-conquer pipeline entirely. Factor N = prod p^e
(passed in, so factoring is amortized across calls), compute the SNF over
each local ring Z/p^e via valuation-pivoted Gaussian elimination, then
CRT-recombine the per-prime unimodular transforms into U, V over Z/NZ.

The local elimination is flat (no recursion), produces the unimodular
transforms natively, and yields the divisibility chain for free. Exposed
via the new rust_crt_snf PyO3 entry point alongside the existing recursive
rust_smith_normal_form, which is left untouched for comparison.

https://claude.ai/code/session_013PpJ2A2YvUCUk5EVtU5rse
Pure-Python reference implementation of the CRT-style SNF, a correctness
harness validating it against the existing implementation as oracle, and a
profiling script for head-to-head comparison against the recursive path.

https://claude.ai/code/session_013PpJ2A2YvUCUk5EVtU5rse
Prior 0.2.0 release-hardening plan is fully shipped (v0.4.0: int64 contract,
i128 intermediates, abi3 wheels, dual-backend tests). Replace with the
research-backed roadmap for optimizing the CRT SNF path (Phases 0-4).
local_snf is 97-100% of CRT runtime (Phase 0 profiling). Its row/column
clears reduced every update through posmod_i128. For prime-power moduli
p^e < 2^31 the difference a-b*c fits in i64, so widen only when needed.

Adds sub_mul_mod + i64 fast path in mulmod, and env-gated CRT_PROFILE
phase-split instrumentation. ~1.5-2.05x faster across n=20..400, all moduli;
1.4-1.6x at n=1000-2000. Verified: invariant factors and U*A*V==S (mod N)
unchanged vs dev baseline and optimize-modulus across 50 configs.
…se 2a)

Restructure local_snf into Phase L (minimal-valuation column elimination ->
U, upper-triangular mat) + Phase R (diagonalize -> V, reverse-order back-
elimination to keep fill in unprocessed rows). This is the structural
groundwork for a blocked GEMM trailing update, and already removes wasted
work: the old inline row-clear updated all n rows of each column though only
the pivot row was nonzero. ~1.1-1.3x faster than Phase 1.

Adds a randomized cargo test (no external deps) asserting local_snf yields a
valid Smith form: U*A*V == diag(p^vals) (mod q), ascending valuations, and
U,V unimodular (det != 0 mod p), across 500+ cases. Verified: zero invariant-
factor mismatches vs the independent dev baseline on the head-to-head sweep.
Capture the valuation-level-staging insight (global min-valuation pivoting
defeats naive blocking; stage by p-adic level so each level is unit-pivot
blocked LU) and the L21/U12 bookkeeping + deflation plan, so 2b can be
executed cleanly against the cargo oracle.
Phase L now clears the valuation-0 bulk with blocked LU (panel width 48):
in-place panel factorization, TRSM (L11^-1) for the deferred panel-row block,
and a GEMM trailing update (mat[pend..,B..] -= L21@U12 mod q, same for U) with
an i64 accumulator + delayed reduction (i128 fallback for large q). The scalar
min-valuation path finishes the higher-valuation / rank-deficient tail; a final
pass normalizes blocked pivots (left as units to keep mat and u row-consistent).

Speedup grows with n (cache/blocking): ~1.3-1.5x over Phase 2a at n=400, and
1.6-2.6x over Phase 1 at n=1000-2000. Cumulative ~3x over the original CRT.

Verified: two randomized cargo tests (600+ cases incl. multi-panel, rank-
deficiency mod p forcing the blocked->scalar handoff, p|A skipping the blocked
pass, rectangular) assert valid SNF; head-to-head shows zero invariant-factor
mismatches vs the dev baseline and all transform_ok across the sweep.
The naive 2b trailing GEMM read panel rows target[k+s,col] with stride m
across s -> ~pb distinct cache lines per output element. Pack U12 (post-TRSM
panel rows) once into a contiguous (pb x ncols) buffer, then update each
trailing row with contiguous axpy passes (autovectorizes); i128 accumulator
fallback for large moduli.

Tried an f64 GEMM via ndarray/matrixmultiply first: helped single-prime
moduli but REGRESSED multi-prime (N=360: 0.86x) due to per-panel n*n product
allocation + thin-K (pb=48). The packed i64 kernel wins across both: ~1.34x
(n=1000,N=2), 1.24x (n=1000,N=360), 1.10x (n=2000) over Phase 2b. Correctness
unchanged (cargo tests + head-to-head vs dev: no mismatches, all transform_ok).
Tall matrices (n > m) were padded to square and had V cropped, which
could leave V non-unimodular for column-rank-deficient inputs (an
all-zero column gave V = [[0]]). U A V == S still held, so the old
tests missed it. Route tall matrices through the transpose, where the
wide path crops U (which stays unimodular) instead of V.

Add a Rust correctness oracle for smith_normal_form (U A V == S,
divisibility chain, unimodularity over random inputs) plus a
Storjohann-vs-CRT invariant-factor cross-check, which is what surfaced
the bug. Tests use rand's seeded StdRng (dev-dependency).
- Drop the CRT_PROFILE timing instrumentation and phase-by-phase
  development comments from crt.rs; keep the algorithmic invariants
  (i64 overflow bounds, deferred normalization).
- Replace section-divider comments and TRSM/GEMM shorthand with prose
  matching the rest of the crate; rustfmt ring.rs/snf.rs.
- Seed crt.rs tests with rand's StdRng instead of a hand-rolled LCG.
- Replace the dev roadmap (docs/PLAN.md) and the CRT prototype/profiling
  scripts with a concise design note (docs/crt.md).
The pure-Python Storjohann implementation is removed; modularsnf is now
a thin wrapper over the modularsnf._rust extension. Validation and
marshalling live in _core.py; snf.py and crt.py just call the binding.
The public API is a single entrance -- smith_normal_form_mod, crt_snf,
SNFResult -- with RingZModN/RingMatrix/diagonal dropped.

The PyO3 wrapper now delegates to modularsnf::smith_normal_form (which
also propagates the tall-matrix fix to Python) and exposes only
rust_smith_normal_form and rust_crt_snf; the dead RustRingZModN,
diagonal, and merge exports are removed.
Algorithm correctness now lives in the Rust crate. The Python suite
(test_binding.py) covers only the binding: input validation, type/shape
marshalling, and one end-to-end SymPy oracle on the invariant factors.
Drop the old correctness/internal suites and the python/rust backend
switch (conftest, helpers, pyproject perf marker, justfile).
Storjohann band reduction stays the documented default; add a CRT
fast-path section pointing at docs/crt.md. Update README/AGENTS to
describe the Rust-crate-plus-thin-binding layout and that correctness
is validated in the Rust suite.
@events555 events555 merged commit d7b662a into dev Jun 6, 2026
4 checks passed
@events555 events555 deleted the crt branch June 6, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants