nassau: recompute quasi-inverses on demand, with bidegree-major batched lifting#275
nassau: recompute quasi-inverses on demand, with bidegree-major batched lifting#275JoeyBF wants to merge 8 commits into
Conversation
Port recompute-on-demand to the current (pre-zarr) save format. The quasi-inverse of d_s at (s, t) is re-derivable from the saved differential alone -- it needs only differentials[s], the module bases, and the deterministically-chosen subalgebra, never the rest of the resolution. - EXT_NASSAU_NO_SAVE_QI: resolution persists only the differentials (the qis are ~260-460x larger) and skips writing the quasi-inverse. - RecomputeReader: a streaming io::Read that regenerates the qi in exactly the byte format write_qi produced, so apply_quasi_inverse reads a recomputed qi through the unchanged apply loop -- only the source of bytes differs. It advances one signature at a time (reusing write_qi), so peak memory matches resolve-time, never the whole qi (hundreds of GB at record stems). - apply_quasi_inverse falls back to recomputation whenever no saved qi is available: no store, EXT_NASSAU_NO_SAVE_QI, or the top-of-region bidegree that nassau never saves (qi(max_s, t)). EXT_NASSAU_RECOMPUTE_QI forces it. This is independent of the zarr save rework (PR SpectralSequences#260): it targets the old Magic-bytecode format directly, so it can land first and let SpectralSequences#260 drop its qi stream tier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KiaVy4c9APWdmFeFoPCbdo
Add the bidegree-major batched lifting driver and route all-products through it. This is the format-agnostic half of the recompute work, ported to master alongside the recompute-on-demand nassau change. - resolution_homomorphism: split extend_step_raw into prepare_step (everything before the quasi-inverse solve) + finish_step (scatter + record). The single-map path is prepare -> apply_quasi_inverse -> finish, unchanged. - Liftable trait + LiftRequest: a target/source-type-free interface; prepare(b) returns the vectors to lift plus a boxed continuation, so the driver never names a liftable's internal state and liftables of different kinds (chain maps now; secondary/Massey chain homotopies later) can be batched together. - MultiLift<CC>: holds a target and Vec<Arc<dyn Liftable>>, extends them together via iter_s_t over output bidegrees. Each cell gathers every liftable's inputs, issues one apply_quasi_inverse, and scatters. Concurrency across the plane is iter_s_t's, as for a single map. - ExtAlgebra::extend_all_products collects the product maps as Arc<dyn Liftable>. - all_products example: EXT_PER_MAP toggles the per-map path; EXT_DUMP_PRODUCTS prints the full multiplication table for diffing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KiaVy4c9APWdmFeFoPCbdo
Split ChainHomotopy::extend_step into prepare_step (everything before the quasi-inverse solve) + finish_step (persist + register), so a chain homotopy can be driven through the batched MultiLift alongside chain maps. The single-map extend_step is now prepare -> apply_quasi_inverse -> finish, behaviour unchanged. - Liftable::prepare(b) recovers the source step from the target bidegree (source = b + shift - (1,0)), guards the source/target computed range, and maps the Done cases (already computed, zero, save-loaded -- which prepare_step finishes itself) to None so the driver skips them, exactly as the chain-map impl does. No pre-seeding of the zero row is needed because prepare_step adds it. - This is the cleanest homotopy case: non-fallible, no post-quasi-inverse read of the results, no intermediate DashMap. It establishes that homotopy lifts fit Liftable; the secondary-product lift (SecondaryResolutionHomomorphism) and the massey_iter_c batching consumer are the next steps. All three ext_algebra::massey unit tests pass, confirming the split preserves the single-map behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KiaVy4c9APWdmFeFoPCbdo
Iterating the third factor built and extended a fresh f_c chain map and a
fresh null-homotopy of b·c for every c. With quasi-inverses recomputed on
demand (no on-disk QIs) that re-solves the unit's quasi-inverse at every
bidegree once per third factor.
Build every map up front and extend them together through MultiLift, sharing
each quasi-inverse across all third factors:
- Phase 1 batches b_hom and all f_c (all target the unit) in one sweep.
- Phase 2 batches all null-homotopies in a second sweep, after the maps
they read are fully extended.
The bracket read is factored into massey_read_representative so the batched
path and the single-map massey() path share it exactly. massey_iter_a already
reuses a single null-homotopy, so it needs no change.
Verified: test_iter_a_matches_iter_c and test_iter_c_proper_kernel both
compare batched massey_iter_c against the independent massey_iter_a reference
and still pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiaVy4c9APWdmFeFoPCbdo
Hook the secondary-resolution machinery into the bidegree-major batching
infrastructure, so the secondary homotopy solves of many lifts sharing a
target are driven through one MultiLift and the target's quasi-inverse at each
bidegree is solved once rather than once per lift (the payoff under
recompute-on-demand). Secondary Massey (SecondaryChainHomotopy) is left for
later.
- Split SecondaryLift::try_compute_homotopy_step into prepare_homotopy_step
(assemble the intermediates to lift) + finish_homotopy_step (verify,
persist, register), behavior-preserving. The post-solve lift-validity check
keeps a copy of the pre-solve intermediates so the batched driver, which
moves the originals out to lift, can still run it.
- Add a SecondaryLiftable wrapper implementing Liftable (generic over any
SecondaryLift) plus batch_extend_secondary, which runs each lift's
composites/intermediates (no quasi-inverse) up front, then batches the
homotopy solves. prepare_homotopies early-returns for lifts with no room to
extend.
- Fix a latent MultiLift bug: step_cell skipped the finishers whenever the
combined inputs across all liftables at a bidegree were empty. A
zero-dimensional step lifts nothing but must still run its finish to
extend/register the step, or a later read of that bidegree indexes an
unfilled degree and panics. Finishers now always run (with an empty results
slice when there is nothing to lift).
- Add SecondaryExtAlgebra::extend_all_secondary_products, the secondary
analogue of extend_all_products: extend each underlying primary map to its
own bound (the secondary max() assumes that extent, so they are not
over-extended through MultiLift), then batch the secondary homotopies.
Filtration-zero multipliers are skipped (degenerate).
Verified: batched_matches_native_d2 pins the wrapper against native
compute_homotopies for a single lift; batched_secondary_products_match pins the
multi-lift batched products against the per-lift path; existing secondary,
massey, and products tests still pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiaVy4c9APWdmFeFoPCbdo
Formatting only (line wrapping, string breaks) via the crate's nightly rustfmt config, plus intra-doc link fixes: reference `MultiLift` by full path in `ext_algebra::secondary` (not imported there) and drop the now-redundant explicit target in `secondary`. No behavior change; docs build clean under `-D rustdoc::broken_intra_doc_links`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KiaVy4c9APWdmFeFoPCbdo
|
Warning Review limit reached
Next review available in: 35 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe PR adds shared prepare/finish lifting abstractions and batches quasi-inverse solves across primary, secondary, product, and Massey computations. It also adds configurable quasi-inverse regeneration and an example for comparing per-map and batched product extension. ChangesBatched lifting pipeline
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant ExtAlgebra
participant MultiLift
participant ChainHomotopy
participant Nassau
ExtAlgebra->>MultiLift: extend product and homotopy liftables
MultiLift->>ChainHomotopy: prepare bidegree requests
ChainHomotopy-->>MultiLift: lift inputs and finish closures
MultiLift->>Nassau: apply combined quasi-inverse inputs
Nassau-->>MultiLift: quasi-inverse outputs
MultiLift->>ChainHomotopy: finalize prepared steps
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ext/src/ext_algebra/massey.rs`:
- Around line 328-341: Update the batched lifting around MultiLift::extend_all
so each b_hom, f_c, and pending null-homotopy is extended only through its
required tot/profile bound rather than the full overlap. Add or reuse a bounded
MultiLift/profile-lift operation, preserve the per-result tot association from
pending, and skip both batches when no work is required so b_hom is not fully
extended when pending is empty.
In `@ext/src/nassau.rs`:
- Around line 1268-1270: Update the quasi-inverse writer state around
signatures, scratch, and buf so signature blocks are not materialized before
consumption. Retain iterator and pivot-serialization progress across reads, and
change write_qi plus the Read::read implementation to append and return bytes
incrementally for each command while preserving ordering and serialization
behavior.
In `@ext/src/resolution_homomorphism.rs`:
- Around line 408-411: Update the documentation near MultiLift to describe chain
homotopy and secondary lift implementations of Liftable as existing
implementors, removing the stale “future implementors” wording while preserving
the explanation of their shared target quasi-inverse and batching behavior.
In `@ext/src/secondary.rs`:
- Around line 834-848: Update batch_extend_secondary to validate that every
lift’s lift.target() matches the supplied target before calling
prepare_homotopies or constructing MultiLift. Reject or otherwise handle any
mismatch using the existing project convention, while preserving the current
empty-input behavior and batching flow for valid lifts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 79b864ce-551a-46e7-a990-094736d20d0b
📒 Files selected for processing (8)
ext/examples/all_products.rsext/src/chain_complex/chain_homotopy.rsext/src/ext_algebra/massey.rsext/src/ext_algebra/mod.rsext/src/ext_algebra/secondary.rsext/src/nassau.rsext/src/resolution_homomorphism.rsext/src/secondary.rs
- Update the `Liftable` trait doc to list its actual implementors (`ChainHomotopy`, secondary lifts) instead of describing them as future work. - `batch_extend_secondary`: assert every lift shares the supplied target before any preparation side effects (matching the `Arc::ptr_eq` checks the lift constructors already use); a mismatched target would silently produce invalid lifts. - `massey_iter_c`: return early when there are no valid third factors, so `b_hom` is not extended for a batch that will read nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KiaVy4c9APWdmFeFoPCbdo
`MultiLift::extend_all` swept the whole computed plane, so batched
`massey_iter_c` extended every `f_c` and null-homotopy far past the `tot`
each bracket is actually read at — materializing up to `#c × full-plane` of map
data where the per-third-factor scheme only held one `tot`-sized extent at a
time.
Add `MultiLift::extend_through_stem(bound)` (a stem-profile-bounded sweep, the
batched analogue of `MuResolutionHomomorphism::extend_through_stem`) and use it:
- the map batch (`b_hom` + every `f_c`) is read only up to output bidegree
`shift` — the map value at `tot = c_deg + shift` lands at `tot - c_deg =
shift` — so bound it there;
- each null-homotopy's top lands at output bidegree `a.degree()`, so bound the
homotopy batch there.
These bounds are exactly the output-side image of the original
`extend_through_stem(tot)` / `extend(tot)` profiles, so the extents match the
per-`tot` scheme while still sharing one quasi-inverse solve per bidegree.
`test_iter_a_matches_iter_c` and `test_iter_c_proper_kernel` (batched vs. the
independent `massey_iter_a` reference) still pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiaVy4c9APWdmFeFoPCbdo
…mand, bidegree-major batched lifting
Motivation
On-disk quasi-inverses (QIs) are the sole blocker to larger nassau computations: stem 256 already exceeds 20 TB and stem 300 would blow past 40 TB. The QIs solve the lifting problem so downstream work (products, Massey products, the secondary/
d₂structure) doesn't recompute them — but they are mathematically re-derivable from the (tiny) saved differentials. The catch is that re-deriving one QI costs about as much as computing it did originally, so recomputing per lift is far too slow.This PR makes recompute-on-demand practical by amortizing each QI recompute across every lift that needs it: sweep output bidegrees, and at each one solve the shared quasi-inverse once for all the maps/homotopies passing through it.
What's here
1. Recompute-on-demand for nassau QIs (
nassau.rs)Regenerate a QI on the fly from
differentials[s]alone (plus the module bases and the deterministically-chosen subalgebra) instead of reading it from disk. It streams one signature at a time through the exact byte formatwrite_qiproduces, soapply_quasi_inversereads a recomputed QI through the same code path as a saved one and peak memory matches what resolving that bidegree originally took — never the whole QI, which can reach hundreds of GB at record stems. Opt-in, no default behavior change:EXT_NASSAU_NO_SAVE_QIstops persisting QIs,EXT_NASSAU_RECOMPUTE_QIforces recompute. By default QIs are still saved and read.2. Bidegree-major batched lifting (
resolution_homomorphism.rs)A
Liftabletrait (target/source-type-free:prepare(b) -> Option<{inputs, finish}>) and aMultiLiftdriver that extends many liftables sharing one target complex together, in bidegree-major order viaiter_s_t, issuing one batchedapply_quasi_inverseper output bidegree. With QIs recomputed on demand this keeps a whole many-map computation at ~1× the on-disk baseline instead of scaling with the number of maps. The single-map lift paths become behavior-preservingprepare_step/finish_stepsplits.3. Consumers wired to batch
ExtAlgebra::extend_all_productsbuilds the product map per generator and extends them all through oneMultiLift.massey_iter_cnow builds everyf_cand null-homotopy up front and extends them in two batched passes (maps, then homotopies) instead of a fresh homotopy per third factor.massey_iter_aalready reuses one homotopy, so it's unchanged.SecondaryLifthomotopy solve gets the sameprepare/finishsplit and aSecondaryLiftablewrapper +batch_extend_secondary;SecondaryExtAlgebra::extend_all_secondary_productsbatches all secondary products (SecondaryChainHomotopy/ secondary Massey is left for a follow-up).4. A latent
MultiLiftfixstep_cellskipped the finishers whenever the combined inputs across all liftables at a bidegree were empty. A zero-dimensional step lifts nothing but must still run its finish to register/extend the step, or a later read of that bidegree indexes an unfilled degree and panics. Finishers now always run (with an empty results slice when there's nothing to lift).Behavior & compatibility
prepare/finishsplits), pinned by the existing tests.Testing
test_iter_a_matches_iter_c/test_iter_c_proper_kerneldiff batchedmassey_iter_cagainst the independentmassey_iter_areference.batched_matches_native_d2pins the secondary wrapper against nativecompute_homotopies;batched_secondary_products_matchpins the multi-lift batched products against the per-lift path.just lint(nightly rustfmt + clippy) is clean on the touched files, and docs build under-D rustdoc::broken_intra_doc_links.Illustrative timing (
all_productson S₂, QIs recomputed): batched 4.57 s vs. per-map 17.33 s for 516 product maps — the redundancy the batching removes.Relationship to #260 (ZarrV3)
Deliberately built on
master, independent of the ZarrV3 save-format PR, so it can land first — it settles what we store (recompute-on-demand removes the QI files) before that PR finalizes how. Once this lands, #260 can drop its QI stream tier.🤖 Generated with Claude Code
Generated by Claude Code
Summary by CodeRabbit