Skip to content

Feat/isolated pool parallelism v2#521

Closed
diegokingston wants to merge 2 commits into
mainfrom
feat/isolated-pool-parallelism-v2
Closed

Feat/isolated pool parallelism v2#521
diegokingston wants to merge 2 commits into
mainfrom
feat/isolated-pool-parallelism-v2

Conversation

@diegokingston

Copy link
Copy Markdown
Collaborator

No description provided.

Instead of processing tables in chunks on the global Rayon pool
(where cross-table work-stealing causes contention), partition
tables into S groups using greedy LPT scheduling and give each
group its own isolated Rayon ThreadPool with cores/S threads.

Key changes:
- shard_parallelism(): configurable S (default cores/8, env SHARD_PARALLELISM)
- partition_tables_by_cost(): greedy LPT by rows * columns
- std::thread::scope + pool.install() for per-partition isolation
- SendPtr for disjoint transcript access across partitions
- Consumes cached LDEs via build_round1 (no recomputation)
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench 3

@github-actions

Copy link
Copy Markdown

Codex Code Review

  1. Medium (Security / Memory Safety)
    Unbounded unsafe impl for raw-pointer wrapper is unsound in general.

    • prover.rs:362, prover.rs:364, prover.rs:368
    • SendPtr<T> is marked Send and Sync for all T, then get_mut(&self) -> &mut T is exposed behind unsafe. In this call site, uniqueness is intended via partitioning, but the type itself allows future misuse with non-Send/non-thread-safe T, which can cause UB/data races.
    • Action: constrain impls (T: Send at minimum), and prefer a safer split API (split_at_mut/chunked slices) to avoid raw pointer sharing.
  2. Medium (Performance Regression)
    New shard calculation disables inter-table parallelism for common small table counts.

    • prover.rs:1858
    • s = shard_parallelism().min(num_airs / 2).max(1) forces s=1 for num_airs=2 and num_airs=3, so rounds 2-4 become single-partition despite available cores. Previous code parallelized these cases.
    • Action: cap by num_airs (or at least allow s=2 when num_airs=2) to avoid clear throughput regressions.
  3. Low (Reliability / Availability)
    New panic paths were introduced in the proving hot path.

    • prover.rs:1890, prover.rs:1900, prover.rs:1990
    • expect(...)/unwrap() will abort proving on internal mismatch/pool panic instead of returning ProvingError. This is a DoS surface if malformed inputs can trigger internal panics.
    • Action: convert these to error propagation where possible.

@github-actions

Copy link
Copy Markdown

Benchmark — fib_iterative_8M (median of 3)

Table parallelism: 32 (auto = cores / 3)

Metric main PR Δ
Peak heap 68037 MB 68984 MB +947 MB (+1.4%) ⚪
Prove time 33.651s 35.254s +1.603s (+4.8%) ⚪

✅ No significant change.

✅ Low variance (time: 1.5%, heap: 2.0%)

Commit: 8e25d7d · Baseline: cached · Runner: self-hosted bench

@claude

claude Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Review: Feat/isolated pool parallelism v2

Overview

Replaces the chunk-based rayon parallel loop with an isolated thread-pool architecture for Rounds 2-4. Tables are partitioned using greedy LPT scheduling into s groups, each group runs on a dedicated rayon ThreadPool, so intra-table rayon work no longer contends across tables. Clever approach, but there are a few correctness and safety issues.

Issues

[High] Thread panics bypass error handlingh.join().unwrap() (line 1990) will re-panic in the main thread if any worker panics. Given the many unwrap()/expect() calls inside prove_rounds_2_to_4 and build_round1, this is a real risk: proving errors that used to surface as ProvingError will now crash the process.

[Medium] SendPtr<T>: Sync is unsound as a general type — the Sync impl allows multiple threads to call get_mut(idx) with the same index simultaneously, which produces aliased &mut T — undefined behavior. The current usage is safe only because partition indices are guaranteed disjoint at runtime, but that invariant is invisible to the compiler and not enforced by the type. If partition_tables_by_cost ever produced duplicate indices (a bug, not impossible), the UB would be silent.

[Medium] Thread pools are created fresh per multi_prove call — spawning and tearing down s × threads_per_pool OS threads on every call is expensive if multi_prove is called more than once per process lifetime (e.g., in benchmarks or batch workflows).

[Low] Cost formula mismatch in partition_tables_by_cost — the doc comment says rows × (main_cols + aux_cols + 1) but the code uses air.context().trace_columns + 1. If trace_columns already includes aux columns the formula is correct, but the comment is misleading and should be reconciled.

[Low] cores / threads_per_pool computed on non-parallel path — both variables are computed unconditionally, but they are only meaningful (and only used for pool creation) under #[cfg(feature = "parallel")]. The non-parallel info! log printing a meaningless threads_per_pool is confusing.

Comment on lines +362 to +364
unsafe impl<T> Send for SendPtr<T> {}
#[cfg(feature = "parallel")]
unsafe impl<T> Sync for SendPtr<T> {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Sync impl is unsound for this type.

Sync means two threads can call get_mut(idx) concurrently. If they ever pass the same idx (even by mistake), you get two live &mut T pointing to the same location — instant UB with no compiler protection.

The current usage is safe because partitions are disjoint, but the type itself makes a stronger promise than it can keep. Consider limiting the surface:

Suggested change
unsafe impl<T> Send for SendPtr<T> {}
#[cfg(feature = "parallel")]
unsafe impl<T> Sync for SendPtr<T> {}
unsafe impl<T: Send> Send for SendPtr<T> {}
// NOT Sync — concurrent get_mut with the same idx would be UB.
// Safety is enforced at the call site by disjoint partition indices.

Or, better, use a newtype that is Send but not Sync, and only share &SendPtr by passing it into each thread's closure by value (cloning just the pointer), so the compiler prevents cross-thread aliasing at the API level.

})
.collect();

handles.into_iter().map(|h| h.join().unwrap()).collect()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] Thread panics silently bypass ProvingError.

h.join().unwrap() re-panics if the worker thread panicked. There are dozens of unwrap()/expect() calls inside prove_rounds_2_to_4 and build_round1 (see the TODO comment at line 1559). Any one of them will now crash the process instead of returning a ProvingError.

The old chunk loop used ? to propagate errors. This change regresses that:

Suggested change
handles.into_iter().map(|h| h.join().unwrap()).collect()
handles.into_iter().map(|h| h.join().expect("worker thread panicked — check for unwrap/expect inside prove_rounds_2_to_4")).collect()

Longer term, consider catching panics with std::panic::catch_unwind inside the worker and mapping them to a ProvingError::InternalPanic variant, or (better) eliminating the unwrap()s so workers can only return Result.

Comment on lines +1895 to 1901
let pools: Vec<rayon::ThreadPool> = (0..s)
.map(|_| {
rayon::ThreadPoolBuilder::new()
.num_threads(threads_per_pool)
.build()
.expect("failed to create Rayon thread pool")
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Pools are recreated on every multi_prove call.

ThreadPoolBuilder::build() spawns threads_per_pool OS threads per pool, so each call creates s × threads_per_pool threads and tears them down at the end. If multi_prove is called repeatedly (benchmarks, batch provers), this becomes significant overhead.

Consider building the pools once (lazily via OnceLock or passed in as a parameter) and reusing them across calls.

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.

1 participant