-
Notifications
You must be signed in to change notification settings - Fork 17
Add hash-free seqno index tables to the Milnor algebra #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JoeyBF
wants to merge
6
commits into
SpectralSequences:master
Choose a base branch
from
JoeyBF:claude/milnor-seqno-index
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+375
−1
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
666cdc6
Add hash-free seqno index tables to the Milnor algebra
claude 7568215
Address review: guard seqno indexing and widen its test
claude 9738858
Take the degree in seqno, and measure it against the hashmap properly
claude 38c5938
Import Arc rather than spelling out std::sync::Arc
JoeyBF 4c855fa
Move the seqno experiment log out of the comments
JoeyBF c8cd670
Bound SeqnoRanker's validity to the revision it pinned
JoeyBF File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # algebra experiment log | ||
|
|
||
| What we tried, what it gained, and why the rejected alternatives were rejected. This is the place | ||
| for that record — the code comments are not, and neither is the README, which describes the crate | ||
| as it is rather than how it got there. | ||
|
|
||
| ## Hash-free Milnor basis indexing ("seqno") | ||
|
|
||
| `MilnorAlgebra::basis_element_to_index` is a `HashMap<MilnorBasisElement, usize>` lookup. The | ||
| table-based alternative, `MilnorAlgebra::seqno`, ranks a `p_part` by summing differences of a | ||
| precomputed `g` array, with no hash. | ||
|
|
||
| Which one wins depends on the degree, and the crossover is a cache effect. The hashmap is | ||
| per-degree, so its working set grows with the dimension of that degree and eventually falls out of | ||
| cache. The `g` table is shared across degrees and grows only linearly in the degree, so it stays | ||
| resident and its cost is flat. The hashmap wins while it is cache-resident and loses once it is | ||
| not; `benches/seqno.rs` sweeps a range of degrees that spans the crossover. | ||
|
|
||
| Because of that, `compute_basis` deliberately does not build the seqno tables: a resolution that | ||
| never reaches the crossover should not pay for them. Callers that want the hash-free index — a GPU | ||
| backend, or a high-degree CPU run — call `compute_seqno_tables` themselves. | ||
|
|
||
| ### Rejected: `OnceVec<Vec<_>>` storage | ||
|
|
||
| **Rejected.** The first version stored the tables in a `OnceVec<Vec<usize>>`. That paid two atomics | ||
| *per table access*, which was enough to make the table lose to the hashmap at every degree measured. | ||
| Storing one flat, row-major `Vec` behind an `arc_swap::ArcSwapOption` reduced a read to a single | ||
| guard load followed by direct indexing. | ||
|
|
||
| ### Rejected: re-deriving the degree inside `rank` | ||
|
|
||
| **Rejected.** `rank` could recover the degree as `Σ rᵢ·ξᵢ` instead of taking it as an argument, but | ||
| every caller already knows it, and the hashmap it competes with reads it straight off the basis | ||
| element. Re-deriving it would have put a loop in the measurement that the competing path does not | ||
| pay. It survives as a `debug_assert!`. | ||
|
|
||
| ### Hoisting the `arc_swap` guard | ||
|
|
||
| **Kept.** `seqno` acquires the guard on every call, which is one atomic per lookup and pure overhead | ||
| in a loop that ranks many elements. `seqno_ranker` hoists the acquisition out of the loop; the | ||
| `seqno` vs `seqno_naive` gap in `benches/seqno.rs` is what that is worth. |
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
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,71 @@ | ||||||||
| //! A/B benchmark: the hash-free table index against the basis hashmap. | ||||||||
|
|
||||||||
| use algebra::{Algebra, MilnorAlgebra}; | ||||||||
| use criterion::{Criterion, Throughput, black_box, criterion_group, criterion_main}; | ||||||||
| use fp::prime::TWO; | ||||||||
|
|
||||||||
| /// Degrees to sample, spanning the point where the table index overtakes the hashmap (see | ||||||||
| /// `EXPERIMENTS.md`). | ||||||||
|
Comment on lines
+7
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| /// | ||||||||
| /// `compute_basis` builds every degree below the maximum, so raising the top of this range costs | ||||||||
| /// memory as well as time. | ||||||||
| const DEGREES: &[i32] = &[32, 64, 128, 192, 256, 300, 340]; | ||||||||
|
|
||||||||
| /// Time both indices over every basis element of each degree in [`DEGREES`]. | ||||||||
| fn seqno(c: &mut Criterion) { | ||||||||
| let algebra = MilnorAlgebra::new(TWO, false); | ||||||||
| let max_degree = *DEGREES.iter().max().unwrap(); | ||||||||
| algebra.compute_basis(max_degree); | ||||||||
| algebra.compute_seqno_tables(max_degree); | ||||||||
|
|
||||||||
| let mut g = c.benchmark_group("seqno"); | ||||||||
|
|
||||||||
| for °ree in DEGREES { | ||||||||
| let dim = algebra.dimension(degree); | ||||||||
| if dim == 0 { | ||||||||
| continue; | ||||||||
| } | ||||||||
| // Snapshot the basis so neither index pays to walk the algebra's storage while timed. | ||||||||
| let basis: Vec<_> = (0..dim) | ||||||||
| .map(|i| algebra.basis_element_from_index(degree, i)) | ||||||||
| .collect(); | ||||||||
|
|
||||||||
| g.throughput(Throughput::Elements(dim as u64)); | ||||||||
|
|
||||||||
| g.bench_function(format!("hashmap/deg{degree}"), |b| { | ||||||||
| b.iter(|| { | ||||||||
| for elt in &basis { | ||||||||
| black_box(algebra.basis_element_to_index(elt)); | ||||||||
| } | ||||||||
| }); | ||||||||
| }); | ||||||||
|
|
||||||||
| g.bench_function(format!("seqno/deg{degree}"), |b| { | ||||||||
| let ranker = algebra.seqno_ranker(); | ||||||||
| b.iter(|| { | ||||||||
| for elt in &basis { | ||||||||
| black_box(ranker.rank(elt.p_part, degree)); | ||||||||
| } | ||||||||
| }); | ||||||||
| }); | ||||||||
|
|
||||||||
| // `seqno` hoists the table guard out of the loop as a hot caller would; the gap against | ||||||||
| // `seqno_naive`, which re-acquires it per call, is what that hoisting is worth. | ||||||||
| g.bench_function(format!("seqno_naive/deg{degree}"), |b| { | ||||||||
| b.iter(|| { | ||||||||
| for elt in &basis { | ||||||||
| black_box(algebra.seqno(elt.p_part, degree)); | ||||||||
| } | ||||||||
| }); | ||||||||
| }); | ||||||||
| } | ||||||||
|
|
||||||||
| g.finish(); | ||||||||
| } | ||||||||
|
|
||||||||
| criterion_group! { | ||||||||
| name = benches; | ||||||||
| config = Criterion::default(); | ||||||||
| targets = seqno | ||||||||
| } | ||||||||
| criterion_main!(benches); | ||||||||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this could use a bit of expansion here. We have two approaches: this and hashmap. Hashmap good in low degrees, seqno good in high degree.