Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
83819ef
Revert "nassau: remove the ParallelGuard priority-inversion retry mec…
JoeyBF Jul 20, 2026
5beaf22
nassau: park retries instead of busy-spinning on ParallelGuard
claude Jul 22, 2026
80d614f
nassau: make the parallel-section guard per-thread
claude Jul 22, 2026
540b31f
nassau: test that the parallel-section guard is thread-local
claude Jul 22, 2026
2b54cf9
Merge remote-tracking branch 'origin/claude/nassau-relaxed-dependency…
JoeyBF Jul 22, 2026
dbc645e
milnor_gpu: drop the global device mutex; make the resident store thr…
JoeyBF Jul 22, 2026
f1cc8fa
milnor_gpu: bound GPU-path memory (row blocks, launch permits, shared…
JoeyBF Jul 23, 2026
a0022b7
milnor_gpu: byte-weighted launch budget + prefix-doubling master uploads
JoeyBF Jul 23, 2026
a04dd91
nassau: offload the d_s image build (signature_matrix) to the GPU mul…
JoeyBF Jul 23, 2026
bc253f6
milnor_gpu: zero the out_h accumulator on-device instead of uploading…
JoeyBF Jul 23, 2026
c433556
milnor_gpu: move the resident-master device upload off the handle lock
JoeyBF Jul 24, 2026
164b129
milnor_gpu: fill the term-data upload buffers directly, killing the p…
JoeyBF Jul 24, 2026
b02dcb3
milnor_gpu: raise GPU_PAIR_CHUNK to ~2^32 so giant multiplies stop ov…
JoeyBF Jul 24, 2026
e6b85fd
milnor_gpu: make the Milnor basis resident on the GPU, upload term in…
JoeyBF Jul 24, 2026
4efee67
milnor_gpu: safe multi-stream via in-place-grown resident globals
JoeyBF Jul 25, 2026
0c1712a
fp: trace GPU vs CPU row-reduce dispatch (fp::rr)
JoeyBF Jul 25, 2026
f22f8a7
fp,algebra: serialize cubecl multiply vs cooperative GPU row-reduce
JoeyBF Jul 26, 2026
5723c04
fp-cuda: add composable (non-cooperative) row-reduce path, default it
JoeyBF Jul 26, 2026
ad185fb
fp-cuda: multi-SM row-reduce via kernel-boundary sync (not cooperative)
JoeyBF Jul 26, 2026
4f83a85
fp-cuda: fuse find+swap in streamed panel factor; add rr timing
JoeyBF Jul 26, 2026
1f5afea
fp-cuda: fuse trailing-XOR with next pivot search (lookahead panel step)
JoeyBF Jul 26, 2026
109fb3b
fp,algebra: drop GPU_EXCLUSIVE lock; the row-reduce is now composable
JoeyBF Jul 26, 2026
af74dfa
milnor_gpu: fix >2^32-element master via cubecl 64-bit addressing
JoeyBF Jul 27, 2026
59bd2d9
milnor_gpu: slash GPU host memory — single stream by default + drop t…
JoeyBF Jul 27, 2026
e4da7ed
nassau_gpu: cap the per-build GPU work so the dense readback stays bo…
JoeyBF Jul 27, 2026
28a1dff
milnor_gpu: degree-threshold eviction of the resident device master
JoeyBF Jul 27, 2026
b447de5
milnor_gpu: CPU reference for in-kernel admissible enumeration
JoeyBF Jul 27, 2026
0ae80f3
milnor_gpu: in-kernel admissible enumeration (CUDA-validated)
JoeyBF Jul 27, 2026
e41ed2f
milnor_gpu: enumerate cold masters on-device instead of uploading
JoeyBF Jul 27, 2026
a28e214
milnor_gpu: enum-path diagnostics + CPU-vs-GPU enum bench (direction …
JoeyBF Jul 27, 2026
cc6ae7b
milnor_gpu: correct the enum-vs-upload bench (parity, not 3x slower)
JoeyBF Jul 27, 2026
b647558
milnor_gpu: segmented-master read primitive (chunked-growth de-risk)
JoeyBF Jul 27, 2026
f362f85
milnor_gpu: segmented no-copy resident master/basis (kill the realloc…
JoeyBF Jul 27, 2026
7265f04
milnor_gpu: move all test kernels/helpers into the tests module
JoeyBF Jul 27, 2026
c6879d9
milnor_gpu: persistent seqno (g/xi) + per-stream output buffer (kill …
JoeyBF Jul 28, 2026
a0af1db
milnor_gpu: drop the per-thread persistent out_h (keep resident g/xi)
JoeyBF Jul 28, 2026
72190b3
milnor_gpu: CPU-fallback stopgap for cubecl context death + GPU multi…
JoeyBF Jul 29, 2026
1c09868
milnor_gpu: NASSAU_GPU_CLEANUP_EVERY knob — throttle per-launch memor…
JoeyBF Jul 29, 2026
aafe902
milnor_gpu: concurrent-growth soak #[test] — fast #1401 reproducer + …
JoeyBF Jul 30, 2026
9dbe8ae
milnor_gpu: pivot cubecl 0.10 fork → upstream v0.11.0-pre.1
JoeyBF Aug 1, 2026
3f86eb7
milnor_gpu: guard the multiply output write against OOB atomic (out[w…
JoeyBF Aug 1, 2026
bbf7c4c
Bit-pack Milnor basis elements into a u64
claude Jul 31, 2026
769fbcc
Cut per-entry overhead out of the Milnor multiplier
claude Jul 31, 2026
fb242b8
Derive the p=2 Milnor basis instead of storing it
claude Jul 31, 2026
6ac8173
Add PPartRanker behind an opt-in feature, not wired in
claude Jul 31, 2026
99ae117
Reject inputs the packing cannot represent, instead of panicking
claude Aug 1, 2026
f3aab5b
Apply nightly rustfmt to the packed p-part constants
claude Aug 1, 2026
d356e35
milnor_gpu: also guard the output row against OOB (word < out.len())
JoeyBF Aug 1, 2026
df162b9
fp,fp-cuda: lock-free GPU row reduction + cross-runtime device arbitr…
JoeyBF Aug 2, 2026
f9a7467
milnor_gpu: fail loudly on GPU context death, and split the multiply'…
JoeyBF Aug 2, 2026
4671199
nassau: probe whether signature steps are independent (they are not)
JoeyBF Aug 2, 2026
28ed66e
nassau_gpu: rustfmt reflow of an assert message
JoeyBF Aug 2, 2026
d2cee65
milnor_gpu: dedicated GPU submission thread (kill the starvation stalls)
JoeyBF Aug 2, 2026
f9f595f
milnor_gpu: bench the hard stem-200 regime instead of resolving to it
JoeyBF Aug 2, 2026
7744b4a
nassau,milnor_gpu: the multi-minute stalls were a par_iter over a 5ms…
JoeyBF Aug 2, 2026
3e7c821
milnor_gpu: zero-copy batch output, bounded bench, kernel micro-optim…
JoeyBF Aug 2, 2026
62fa89c
milnor_gpu: size the per-thread working arrays per launch (+29%)
JoeyBF Aug 2, 2026
683c4d8
Merge PR #280 (packed PPart) and port the GPU path to it
JoeyBF Aug 3, 2026
22b0a67
milnor_gpu: pack the kernel's `working` accumulator into a u64
JoeyBF Aug 3, 2026
76869d2
milnor_gpu: record the occupancy finding and prepare the cubecl change
JoeyBF Aug 3, 2026
93520e8
milnor_gpu: fuse the per-thread gather into the column loop (48 -> 40…
JoeyBF Aug 3, 2026
7887f24
milnor_gpu: NASSAU_GPU_LAUNCH_LOG — dump per-launch shape
JoeyBF Aug 3, 2026
9e0abd9
milnor_gpu: bound the column loop per thread, not per launch (+8.5%)
JoeyBF Aug 3, 2026
2aed0b0
milnor_gpu: coarse product index for the thread->product lookup (+4.2%)
JoeyBF Aug 3, 2026
c305f37
milnor_gpu: decode matrix fastest, term slowest (+2.0%)
JoeyBF Aug 3, 2026
19fb7db
milnor_gpu: look up num_mats instead of dividing for it
JoeyBF Aug 3, 2026
d54aec9
milnor_gpu: one thread per (matrix, term GROUP) (+51%)
JoeyBF Aug 3, 2026
37a298e
milnor_gpu: TERM_GROUP 4 -> 3 (+9%)
JoeyBF Aug 3, 2026
144727f
gitignore: exclude ext/*.log GPU run logs
JoeyBF Aug 3, 2026
d167b6f
nassau: span the CPU work inside `step`
JoeyBF Aug 4, 2026
d4db837
milnor_gpu: tile over matrices as well as terms (+6.6%)
JoeyBF Aug 4, 2026
9174826
milnor_gpu: shard the resident master across GPUs (2.27x on 4 devices)
JoeyBF Aug 4, 2026
cc11687
milnor_gpu: assign shards to the least-loaded device (+32%)
JoeyBF Aug 4, 2026
1639982
milnor_gpu: submit shards without spawning threads; name the GPU workers
JoeyBF Aug 4, 2026
a9fc729
milnor_gpu: marshal the shards in parallel again, wait outside
JoeyBF Aug 4, 2026
0bc8956
milnor_gpu: keep the scoped-thread fan-out; it beat both rewrites
JoeyBF Aug 4, 2026
07d7aab
maybe-rayon: add MaybeThreadPool; use it for the multi-GPU fan-out
JoeyBF Aug 4, 2026
b8c4470
milnor_gpu: size the fan-out pool for concurrent callers, not devices
JoeyBF Aug 4, 2026
8c2365c
milnor_gpu: size the fan-out pool by callers x devices
JoeyBF Aug 4, 2026
199c052
milnor_gpu: one thread, async submissions to every device
JoeyBF Aug 4, 2026
a4aed87
milnor_gpu: pipeline the readback; restore the memory bound
JoeyBF Aug 4, 2026
ef16f93
milnor_gpu: marshal the shards in parallel again
JoeyBF Aug 4, 2026
3ab3792
milnor_gpu: split `prep` into intern / basis / tgei
JoeyBF Aug 5, 2026
220e8f0
nassau, milnor_gpu: demote the fine-grained spans below info
JoeyBF Aug 5, 2026
51a8949
milnor_gpu: flag the readback keepalive bug at the site
JoeyBF Aug 5, 2026
061c667
milnor_gpu: keep launch buffers alive until the readback completes
JoeyBF Aug 5, 2026
bdbd4eb
Revert "milnor_gpu: marshal the shards in parallel again"
JoeyBF Aug 5, 2026
02dc8f0
milnor_gpu: marshal the shards on dedicated threads
JoeyBF Aug 5, 2026
9aa0211
milnor_gpu: run each shard's whole pipeline on its own thread
JoeyBF Aug 5, 2026
63595b7
milnor_gpu: persistent per-caller shard helpers, no more spawn churn
JoeyBF Aug 5, 2026
38bbdd7
milnor_gpu: crossbeam channels for the shard helpers
JoeyBF Aug 5, 2026
3c31f87
Revert "milnor_gpu: crossbeam channels for the shard helpers"
JoeyBF Aug 5, 2026
8ceb3b2
milnor_gpu: shard R by a mixed hash, not by accumulated num_mats
JoeyBF Aug 5, 2026
5d2334c
milnor_gpu: cover the eviction path, and honour CUDA_VISIBLE_DEVICES
JoeyBF Aug 5, 2026
dbe1b49
milnor_gpu: order the enumeration by matrix count; tighten its local …
JoeyBF Aug 5, 2026
2d765af
milnor_gpu: span transient scratch across segments, not one
JoeyBF Aug 6, 2026
90e38c2
fp: make the multiply/reduction device-sharing test multi-GPU aware, …
JoeyBF Aug 6, 2026
32bd1f0
fp: reductions must serialize against each other, not just against th…
JoeyBF Aug 6, 2026
85fa53c
fp: route every fp-cuda submission through one driver thread
JoeyBF Aug 6, 2026
527a18d
fp-cuda: take a share of the device for the GEMM, not all of it
JoeyBF Aug 6, 2026
9bc3012
fp-cuda: name Phase 9 clusters as the co-scheduling constraint to remove
JoeyBF Aug 6, 2026
d3bc656
fp-cuda: cluster-free GEMM as the default, cluster kernel opt-in
JoeyBF Aug 6, 2026
ec3ed45
algebra: measure the d-vs-masks enumeration shortcut, and record that…
JoeyBF Aug 6, 2026
f3391b7
algebra: measure the enum kernel's emit cost — 42.8% of it, at 1% of …
JoeyBF Aug 7, 2026
e51670c
algebra: record that stack-allocating AdmissibleMatrix loses
JoeyBF Aug 7, 2026
1a7e0f9
algebra: the enum emit is store-ISSUE bound, not bandwidth bound
JoeyBF Aug 7, 2026
ece4f45
algebra: measure R rebuild COST, not reference count — the transient …
JoeyBF Aug 7, 2026
00f8d90
algebra: simulate a pinned transient cache — it is linear, so theta i…
JoeyBF Aug 7, 2026
e7c8d2f
algebra: theta=125 costs 6.5x wall time to save 5.3 GB — set it as hi…
JoeyBF Aug 7, 2026
a3f46eb
algebra: report which theta would have fit, instead of discovering it…
JoeyBF Aug 7, 2026
a1b832b
nassau: skip step1's kernel computation when the augmentation codomai…
JoeyBF Aug 7, 2026
3b83d1e
algebra: abort on segment-table exhaustion instead of killing one sha…
JoeyBF Aug 7, 2026
b5d927a
algebra: profile the enum kernel — it is latency-bound, and shared st…
JoeyBF Aug 7, 2026
815fbdd
algebra: the enum kernel is work-starved — registers and shared memor…
JoeyBF Aug 7, 2026
18e79d8
algebra: log enum-launch geometry — production runs the kernel at 0.2…
JoeyBF Aug 7, 2026
6e5b88f
algebra: block size is not the enum lever either — the launches are s…
JoeyBF Aug 7, 2026
78872ff
milnor_gpu: GPU stream concurrency does not pay — the run is not GPU-…
JoeyBF Aug 7, 2026
72d6e1f
nassau_gpu: read the GPU multiply back a limb at a time, not a bit at…
JoeyBF Aug 7, 2026
75faee7
nassau: the missing parallelism is not inside a bidegree — two probes…
JoeyBF Aug 7, 2026
fbf5c7e
nassau: span the zero-signature region — and find the run is 44% CUDA…
JoeyBF Aug 7, 2026
a9d3f5e
milnor_gpu: theta=125 at stem 150 is an artificial worst case — 2.85x…
JoeyBF Aug 7, 2026
c068c48
milnor_gpu: record that FxHashMap is 8% SLOWER than SipHash here
JoeyBF Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ chart/python/*.egg-info
# Claude
.claude

# GPU run logs. These are multi-hundred-MB analysis artifacts written next to the crate;
# one was committed by accident and blocked a push (GitHub rejects files over 100 MB).
ext/*.log
Binary file added ext/150/nassau_differential/c/126/1
Binary file not shown.
Binary file added ext/150/nassau_differential/c/127/0
Binary file not shown.
Binary file added ext/150/nassau_differential/c/127/1
Binary file not shown.
Binary file added ext/150/nassau_differential/c/128/0
Binary file not shown.
Binary file added ext/150/nassau_differential/c/128/1
Binary file not shown.
Binary file added ext/150/nassau_differential/c/129/0
Binary file not shown.
Binary file added ext/150/nassau_differential/c/129/1
Binary file not shown.
Binary file added ext/150/nassau_differential/c/130/0
Binary file not shown.
Binary file added ext/150/nassau_differential/c/130/1
Binary file not shown.
Binary file added ext/150/nassau_differential/c/131/0
Binary file not shown.
Binary file added ext/150/nassau_differential/c/131/1
Binary file not shown.
Binary file added ext/150/nassau_differential/c/132/0
Binary file not shown.
Binary file added ext/150/nassau_differential/c/132/1
Binary file not shown.
Binary file added ext/150/nassau_differential/c/133/0
Binary file not shown.
Binary file added ext/150/nassau_differential/c/133/1
Binary file not shown.
63 changes: 63 additions & 0 deletions ext/150/nassau_differential/zarr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"zarr_format": 3,
"node_type": "array",
"shape": [
4096,
1024
],
"data_type": "bytes",
"chunk_grid": {
"name": "regular",
"configuration": {
"chunk_shape": [
8,
8
]
}
},
"chunk_key_encoding": {
"name": "default",
"configuration": {
"separator": "/"
}
},
"fill_value": [],
"codecs": [
{
"name": "sharding_indexed",
"configuration": {
"chunk_shape": [
1,
1
],
"codecs": [
{
"name": "vlen-bytes"
},
{
"name": "crc32c"
}
],
"index_codecs": [
{
"name": "bytes",
"configuration": {
"endian": "little"
}
},
{
"name": "crc32c"
}
],
"index_location": "end"
}
}
],
"attributes": {
"_zarrs": {
"description": "This array was created with zarrs",
"repository": "https://github.com/zarrs/zarrs",
"version": "0.23.13"
}
}
}
18 changes: 18 additions & 0 deletions ext/150/zarr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"zarr_format": 3,
"node_type": "group",
"attributes": {
"algebra_magic": 163840,
"prime": 2,
"algebra_prefix": "milnor",
"module_spec": {
"p": 2,
"type": "finite dimensional module",
"gens": {
"x0": 0
},
"actions": []
},
"complex_name": "S_2"
}
}
28 changes: 25 additions & 3 deletions ext/crates/algebra/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,20 @@ enum_dispatch = "0.3.13"
# `cuda` targets the local NVIDIA card via NVRTC (needs the CUDA toolkit — wired into
# the ext dev shell in flake.nix). Kernels are runtime-agnostic, so `wgpu` (Vulkan)
# remains a drop-in portable fallback.
cubecl = { version = "0.10.0", optional = true, default-features = false, features = [
cubecl = { git = "https://github.com/tracel-ai/cubecl", tag = "v0.11.0-pre.1", optional = true, default-features = false, features = [
"cuda",
] }
# For pinning all GPU work to one CUDA stream (`StreamId`), so a single memory pool is
# reclaimed by `memory_cleanup` — CubeCL's pools are per-stream, and rayon spreads launches
# across threads/streams, which otherwise accumulates buffers until the card OOMs.
cubecl-common = { version = "0.10.0", optional = true }
cubecl-common = { git = "https://github.com/tracel-ai/cubecl", tag = "v0.11.0-pre.1", optional = true }
# Spans around the GPU submission (see `gpu_thread`), so a worker blocked waiting for the device
# is visible in the log instead of silent — the stalls it diagnoses emitted nothing at all.
tracing = { version = "0.1.41", optional = true }
# Multi-consumer job queue for the per-device GPU workers (see `gpu_thread`). std's mpsc is
# single-consumer, so sharing one queue across devices there means a `Mutex<Receiver>`; this is an
# actual MPMC queue, which is what the dispatch wants.
crossbeam-channel = { version = "0.5", optional = true }

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
Expand All @@ -50,8 +57,12 @@ rstest = "0.25.0"
[features]
default = ["odd-primes"]
cache-multiplication = []
# An arithmetic replacement for the Milnor basis index map. Off by default and not wired in:
# adopting it would renumber the basis and invalidate saved resolutions. See
# `algebra::milnor_rank`.
milnor-rank = []
concurrent = ["fp/concurrent", "maybe-rayon/concurrent"]
gpu = ["dep:cubecl", "dep:cubecl-common"]
gpu = ["dep:cubecl", "dep:cubecl-common", "dep:tracing", "dep:crossbeam-channel"]
odd-primes = ["fp/odd-primes"]

[[bench]]
Expand All @@ -70,6 +81,17 @@ harness = false
name = "nassau_milnor"
harness = false

# GPU batched-multiply throughput / cubecl-regression bench (see benches/nassau_milnor_gpu.rs).
# Requires `--features gpu`; without it the target compiles to a no-op main.
[[bench]]
name = "nassau_milnor_gpu"
harness = false

[[bench]]
name = "seqno"
harness = false

[[bench]]
name = "milnor_rank"
harness = false
required-features = ["milnor-rank"]
32 changes: 16 additions & 16 deletions ext/crates/algebra/benches/milnor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Benchmarks for the low-level Milnor `PPartMultiplier` kernel.

use algebra::milnor_algebra::{PPartAllocation, PPartEntry, PPartMultiplier};
use algebra::milnor_algebra::{PPart, PPartAllocation, PPartMultiplier};
use criterion::{
BenchmarkGroup, Criterion, criterion_group, criterion_main, measurement::WallTime,
};
Expand All @@ -11,8 +11,8 @@ fn bench_ppart<const MOD4: bool>(
g: &mut BenchmarkGroup<WallTime>,
name: &str,
p: u32,
r: Vec<PPartEntry>,
s: Vec<PPartEntry>,
r: PPart,
s: PPart,
) {
let p = ValidPrime::new(p);
g.bench_function(name, |bench| {
Expand All @@ -21,7 +21,7 @@ fn bench_ppart<const MOD4: bool>(
bench.iter_batched(
PPartAllocation::default,
|alloc| {
let m = PPartMultiplier::<MOD4>::new_from_allocation(p, &r, &s, alloc, 0, 0);
let m = PPartMultiplier::<MOD4>::new_from_allocation(p, r, s, alloc, 0, 0);
for c in m {
std::hint::black_box(c);
}
Expand All @@ -38,30 +38,30 @@ fn ppart(c: &mut Criterion) {
&mut g,
"ppart_2/a",
2,
vec![60, 30, 8, 2, 1],
vec![20, 30, 20, 4, 1, 2],
PPart::from_slice(&[60, 30, 8, 2, 1]),
PPart::from_slice(&[20, 30, 20, 4, 1, 2]),
);
bench_ppart::<false>(
&mut g,
"ppart_2/b",
2,
vec![35, 12, 20, 14, 1, 3],
vec![60, 30, 0, 2, 1],
PPart::from_slice(&[35, 12, 20, 14, 1, 3]),
PPart::from_slice(&[60, 30, 0, 2, 1]),
);

bench_ppart::<true>(
&mut g,
"ppart_4/a",
2,
vec![60, 30, 8, 2, 1],
vec![20, 30, 20, 4, 1, 2],
PPart::from_slice(&[60, 30, 8, 2, 1]),
PPart::from_slice(&[20, 30, 20, 4, 1, 2]),
);
bench_ppart::<true>(
&mut g,
"ppart_4/b",
2,
vec![35, 12, 20, 14, 1, 3],
vec![60, 30, 0, 2, 1],
PPart::from_slice(&[35, 12, 20, 14, 1, 3]),
PPart::from_slice(&[60, 30, 0, 2, 1]),
);

#[cfg(feature = "odd-primes")]
Expand All @@ -70,15 +70,15 @@ fn ppart(c: &mut Criterion) {
&mut g,
"ppart_3/a",
3,
vec![120, 70, 40, 2],
vec![60, 35, 21, 6],
PPart::from_slice(&[120, 70, 40, 2]),
PPart::from_slice(&[60, 35, 21, 6]),
);
bench_ppart::<false>(
&mut g,
"ppart_3/b",
3,
vec![30, 12, 35, 24],
vec![100, 80, 16, 2, 3],
PPart::from_slice(&[30, 12, 35, 24]),
PPart::from_slice(&[100, 80, 16, 2, 3]),
);
}

Expand Down
100 changes: 100 additions & 0 deletions ext/crates/algebra/benches/milnor_rank.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Compares [`PPartRanker`] against the hash map lookup it would replace.
//!
//! `MilnorAlgebra::basis_element_to_index` is called once per term of every product, so it is one
//! of the hottest operations in a resolution. It is currently a hash map from the (packed) basis
//! element to its position in `basis_table`. The ranker computes that position arithmetically
//! instead, from a table that covers every degree at once.
//!
//! The two are compared on the same workload: recover the index of every basis element of a
//! degree. Note that they do not agree on *which* index — see [`PPartRanker`] — so this measures
//! the cost of the two strategies, not a drop-in substitution.
//!
//! Each is measured under two access orders, because the choice flatters the map:
//!
//! - **sequential** — sweep the basis in order. This is the map's insertion order, so every probe
//! walks memory linearly and prefetches perfectly. Flattering, and not what callers do.
//! - **scattered** — the same elements in a fixed pseudo-random permutation. This is closer to
//! real use, where `basis_element_to_index` is called on multiplication *outputs*, which arrive
//! in no particular order. It matters because the two structures scale differently: the map
//! stores an entry per basis element and leaves cache as the basis grows (~60 KiB in degree 120
//! alone), whereas the ranker's table is a few KiB covering every degree at once.
//!
//! [`PPartRanker`]: algebra::milnor_rank::PPartRanker

use std::hint::black_box;

use algebra::{Algebra, MilnorAlgebra, milnor_rank::PPartRanker};
use criterion::{Criterion, Throughput, criterion_group, criterion_main};
use fp::prime::TWO;
use pprof::criterion::{Output, PProfProfiler};

/// Degrees to sweep.
///
/// The range matters more than it looks, because the two structures live in different parts of the
/// memory hierarchy and the crossover is inside this range. A lookup probes only its own degree's
/// map, which is ~0.1 MB in degree 120 (L2-resident) but ~3 MB in degree 300 and ~12 MB in degree
/// 400 — well past L3, so every probe is a DRAM miss. The ranker's table is ~35 KB for *all*
/// degrees and stays in L1 throughout. Measuring only the small degrees answers a question nobody
/// is asking; the large ones are where expanding the algebra actually hurts.
const DEGREES: &[i32] = &[120, 300, 400, 500];

/// A fixed permutation of `0..n`, from a Fisher-Yates shuffle driven by a small LCG. Deterministic
/// so the two variants see exactly the same access order.
fn scattered(n: usize) -> Vec<usize> {
let mut order: Vec<usize> = (0..n).collect();
let mut state = 0x2545_f491_4f6c_dd1d_u64;
for i in (1..n).rev() {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
order.swap(i, (state >> 33) as usize % (i + 1));
}
order
}

fn milnor_rank(c: &mut Criterion) {
let algebra = MilnorAlgebra::new(TWO, false);
let max_degree = *DEGREES.iter().max().unwrap();
algebra.compute_basis(max_degree);
let ranker = PPartRanker::new(TWO, max_degree);

let mut g = c.benchmark_group("milnor_rank");
for &degree in DEGREES {
let dim = algebra.dimension(degree);
g.throughput(Throughput::Elements(dim as u64));

// Collect the elements once so neither variant pays for the table walk itself.
let elements: Vec<_> = (0..dim)
.map(|i| algebra.basis_element_from_index(degree, i))
.collect();
let shuffled: Vec<_> = scattered(dim).into_iter().map(|i| elements[i]).collect();

for (order, elements) in [("seq", &elements), ("scattered", &shuffled)] {
g.bench_function(format!("hashmap_{order}/deg{degree}"), |b| {
b.iter(|| {
for elt in elements {
black_box(algebra.basis_element_to_index(elt));
}
});
});

g.bench_function(format!("ranker_{order}/deg{degree}"), |b| {
b.iter(|| {
for elt in elements {
black_box(ranker.rank(elt.p_part, degree));
}
});
});
}
}
g.finish();
}

criterion_group! {
name = benches;
config = Criterion::default()
.measurement_time(std::time::Duration::from_secs(3))
.with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
targets = milnor_rank
}
criterion_main!(benches);
Loading