From 1015096269105ca7120907afed476497871fdb4a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 04:12:56 +0400 Subject: [PATCH 01/65] parallel: in-house fixed thread pool; port eq_mle and sumcheck off rayon Add `crates/backend/parallel`: a minimal fixed-size pool (NUM_THREADS-1 workers + dispatcher as worker 0, stable worker ids, atomic-counter task scheduling, zero per-dispatch allocation). The goal is to remove the per-task heap traffic that justifies the zk-alloc arena, and to own the parallel runtime so we can later attach a per-worker scratch strategy. API: for_each_index, par_chunks_mut, map_reduce. - eq_mle: all 6 parallel kernels dispatch through the pool (no rayon left in the file). Output is split into NUM_THREADS << PARALLEL_LOG_OVERSUB chunks so the atomic counter rebalances across heterogeneous cores; default factor 2 (4x), a conservative machine-agnostic value (the Mac optimum is higher but finer chunks risk regressing many-core CPUs). ~1.25x over rayon at hot sizes on M-series; parity even at 0x. - sumcheck: parallel_sum now uses parallel::map_reduce (one accumulator per worker instead of rayon's per-split intermediates). Crash fix: std Mutex/Barrier on macOS allocate their pthread primitives lazily on first use. If that first use lands inside a zk-alloc phase, the next begin_phase() reset corrupts them (EINVAL/segfault on the 2nd phase). parallel::init() now runs a warmup dispatch to force those allocations into the system allocator while the arena is inactive, and zk_alloc::begin_phase() calls it before activating the arena. tests/test_zk_alloc.rs now runs 2 phases to guard this. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 10 + Cargo.toml | 2 + crates/backend/parallel/Cargo.toml | 11 + crates/backend/parallel/src/lib.rs | 356 ++++++++++++++++++ crates/backend/poly/Cargo.toml | 1 + crates/backend/poly/src/eq_mle.rs | 317 ++++++++++------ crates/backend/sumcheck/Cargo.toml | 1 + crates/backend/sumcheck/src/sc_computation.rs | 5 +- crates/backend/zk-alloc/Cargo.toml | 1 + crates/backend/zk-alloc/src/lib.rs | 7 + tests/test_zk_alloc.rs | 18 +- 11 files changed, 608 insertions(+), 121 deletions(-) create mode 100644 crates/backend/parallel/Cargo.toml create mode 100644 crates/backend/parallel/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a1e508d94..8ef1d96a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -662,6 +662,7 @@ dependencies = [ "mt-field", "mt-koala-bear", "mt-utils", + "parallel", "rand", "rayon", "serde", @@ -677,6 +678,7 @@ dependencies = [ "mt-field", "mt-koala-bear", "mt-poly", + "parallel", "rayon", "tracing", ] @@ -791,6 +793,13 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parallel" +version = "0.1.0" +dependencies = [ + "system-info", +] + [[package]] name = "paste" version = "1.0.15" @@ -1478,6 +1487,7 @@ name = "zk-alloc" version = "0.1.0" dependencies = [ "libc", + "parallel", "rayon", "system-info", ] diff --git a/Cargo.toml b/Cargo.toml index f8e2ada76..047ebec81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/backend/sumcheck", "crates/backend/system-info", "crates/backend/zk-alloc", + "crates/backend/parallel", ] [workspace.lints] @@ -63,6 +64,7 @@ rec_aggregation = { path = "crates/rec_aggregation" } backend = { path = "crates/backend" } zk-alloc = { path = "crates/backend/zk-alloc" } system-info = { path = "crates/backend/system-info" } +parallel = { path = "crates/backend/parallel" } # External sha3 = "0.11.0" diff --git a/crates/backend/parallel/Cargo.toml b/crates/backend/parallel/Cargo.toml new file mode 100644 index 000000000..731b5163d --- /dev/null +++ b/crates/backend/parallel/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "parallel" +version.workspace = true +edition.workspace = true +description = "Minimal fixed-size thread pool for static data-parallel kernels" + +[dependencies] +system-info.workspace = true + +[lints] +workspace = true diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs new file mode 100644 index 000000000..79ce252f1 --- /dev/null +++ b/crates/backend/parallel/src/lib.rs @@ -0,0 +1,356 @@ +//! Minimal fixed-size thread pool for flat, static data-parallel kernels. +//! +//! This is a deliberately tiny alternative to rayon for the one shape the prover +//! actually uses on its hot paths: "split a slice into N pieces, run a closure on +//! each." Unlike rayon it does **no** work-stealing of nested tasks and allocates +//! **nothing** per dispatch — the whole point is to remove the per-task heap +//! traffic that currently forces the `zk-alloc` arena to exist. +//! +//! ## Model +//! +//! The pool owns exactly `NUM_THREADS - 1` background worker threads with stable +//! ids `1..NUM_THREADS`. The dispatching thread acts as worker `0` and runs its +//! share inline, so a dispatch keeps all `NUM_THREADS` hardware threads busy with +//! only `NUM_THREADS - 1` extra threads (no oversubscription, matching the +//! build-time `NUM_THREADS` assumption baked in elsewhere). +//! +//! Tasks are claimed from a shared atomic counter. That gives dynamic load +//! balancing across uneven task costs for free, while still allocating nothing. +//! +//! ## Why stable worker ids +//! +//! [`current_worker_id`] returns a stable `0..NUM_THREADS` id. This is the hook for +//! a future per-worker scratch-buffer strategy: once each worker can index its own +//! preallocated scratch, the short-lived per-task allocations that `zk-alloc` +//! currently absorbs disappear at the source, and the arena can be dropped. +//! +//! ## Constraints +//! +//! - **No nesting.** A worker must not itself dispatch (it would deadlock on the +//! dispatch lock / barriers). The prover's parallel sections are flat, so this +//! holds. Nesting is a logic error, not a soundness hole. +//! - **One dispatcher at a time.** Concurrent dispatches are serialized by a mutex. + +use std::cell::{Cell, UnsafeCell}; +use std::ptr::NonNull; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Barrier, Mutex, Once, OnceLock}; + +use system_info::NUM_THREADS; + +/// Total worker count (including the dispatching thread). Equal to the build-time +/// `NUM_THREADS`. +#[must_use] +pub const fn num_threads() -> usize { + NUM_THREADS +} + +thread_local! { + /// Stable id of this thread within the pool. Set once per background worker; + /// stays `0` on the dispatching thread (which acts as worker 0) and on any + /// thread that never participates. + static WORKER_ID: Cell = const { Cell::new(0) }; +} + +/// Stable id of the calling worker, in `0..NUM_THREADS`. Returns `0` on the +/// dispatching thread and on any non-worker thread. +#[must_use] +pub fn current_worker_id() -> usize { + WORKER_ID.with(Cell::get) +} + +/// A type-erased unit of parallel work. `f` is a `&(dyn Fn(usize) + Sync)` whose +/// lifetime has been erased to `'static`: it is only ever dereferenced between the +/// start and end barriers of a single dispatch, during which the dispatcher blocks, +/// so the borrow it came from outlives every call. +struct Job { + f: NonNull, + n_tasks: usize, +} + +struct Pool { + /// Current job. Written by the dispatcher before `start.wait()` and read by + /// workers after it; the barrier supplies the happens-before relationship, so + /// no additional synchronization on this cell is required. + job: UnsafeCell>, + /// Next task index to claim. Reset to 0 before each dispatch. + counter: AtomicUsize, + shutdown: AtomicBool, + /// Workers park here between dispatches; the dispatcher releases them. + start: Barrier, + /// Everyone meets here once all tasks are drained. + end: Barrier, + /// Serializes dispatchers: only one thread may drive the pool at a time. + dispatch: Mutex<()>, +} + +// SAFETY: `job` is only mutated by the unique dispatcher (serialized by `dispatch`) +// while workers are parked, and only read by workers/dispatcher while no one writes; +// the barriers order these phases. The erased `Job` pointer is never used outside a +// dispatch window during which its source borrow is live. +unsafe impl Sync for Pool {} +unsafe impl Send for Pool {} + +/// Construct the pool and exercise its full dispatch path once, now. +/// +/// **Must be called before any arena allocator that recycles memory between phases +/// is active** (e.g. before `zk_alloc::begin_phase()`), and is idempotent. +/// +/// Two things must end up in the system allocator (not a recyclable arena slab): +/// 1. the leaked `Pool` struct, and +/// 2. the OS sync primitives behind `Mutex`/`Barrier`. On macOS std allocates the +/// underlying `pthread_mutex_t` / `pthread_cond_t` **lazily on first use**, not at +/// construction. So merely building the `Pool` is not enough — if the first +/// `lock()`/`wait()` happened during a phase, that primitive would land in the +/// arena and the next reset would corrupt it (observed as `EINVAL` on lock). +/// +/// Running one real dispatch here forces every lazy primitive (the dispatch mutex +/// and both barriers, touched by the dispatcher and every worker) to allocate while +/// the arena is inactive, pinning them in the system allocator for good. +pub fn init() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let _ = pool(); + // `n_tasks > 1` and `NUM_THREADS > 1` are required to take the real dispatch + // path rather than the sequential fast path. On single-core builds the pool + // is never used, so there is nothing to warm up. + if NUM_THREADS > 1 { + for_each_index(NUM_THREADS, |_| {}); + } + }); +} + +fn pool() -> &'static Pool { + static POOL: OnceLock<&'static Pool> = OnceLock::new(); + POOL.get_or_init(|| { + let n = NUM_THREADS.max(1); + let p: &'static Pool = Box::leak(Box::new(Pool { + job: UnsafeCell::new(None), + counter: AtomicUsize::new(0), + shutdown: AtomicBool::new(false), + start: Barrier::new(n), + end: Barrier::new(n), + dispatch: Mutex::new(()), + })); + for id in 1..n { + std::thread::Builder::new() + .name(format!("parallel-worker-{id}")) + .spawn(move || worker_main(p, id)) + .expect("failed to spawn pool worker"); + } + p + }) +} + +fn worker_main(pool: &'static Pool, id: usize) { + WORKER_ID.with(|c| c.set(id)); + loop { + pool.start.wait(); + if pool.shutdown.load(Ordering::Acquire) { + break; + } + drain(pool); + pool.end.wait(); + } +} + +/// Claim and run task indices until the counter is exhausted. +fn drain(pool: &Pool) { + // SAFETY: the dispatcher published `Some(job)` before the start barrier we just + // crossed, and clears it only after the end barrier; nobody writes during drain. + let job = unsafe { (*pool.job.get()).as_ref().expect("drain without a published job") }; + // SAFETY: `job.f` points at a `&dyn Fn` borrow held live by the blocked + // dispatcher for the entire dispatch window. + let f = unsafe { job.f.as_ref() }; + let n = job.n_tasks; + loop { + let i = pool.counter.fetch_add(1, Ordering::Relaxed); + if i >= n { + break; + } + f(i); + } +} + +/// Run `f(i)` for every `i` in `0..n_tasks`, in parallel across the pool. Blocks +/// until all tasks complete. The dispatching thread participates as worker 0. +/// +/// Falls back to a sequential loop for trivial sizes or single-core builds, so no +/// workers are woken for work that isn't worth the handshake. +pub fn for_each_index(n_tasks: usize, f: F) { + if NUM_THREADS <= 1 || n_tasks <= 1 { + for i in 0..n_tasks { + f(i); + } + return; + } + + let pool = pool(); + let _guard = pool.dispatch.lock().unwrap(); + + let f_ref: &(dyn Fn(usize) + Sync) = &f; + // SAFETY: erase the borrow's lifetime to store it in the 'static `Job`. The + // dispatcher blocks on `end.wait()` below before returning, so `f` (and thus + // `f_ref`) outlives every worker call that dereferences this pointer. + let f_erased: NonNull = unsafe { + std::mem::transmute::, NonNull>(NonNull::from(f_ref)) + }; + + // SAFETY: workers are parked on `start`; we hold `dispatch`, so we are the sole + // writer of `job` and `counter` here. + unsafe { *pool.job.get() = Some(Job { f: f_erased, n_tasks }) }; + pool.counter.store(0, Ordering::Relaxed); + + pool.start.wait(); // release workers (publishes job) + drain(pool); // dispatcher runs as worker 0 + pool.end.wait(); // wait for all workers + + // SAFETY: all workers have passed `end`; none will touch `job` until the next + // dispatch republishes it. + unsafe { *pool.job.get() = None }; +} + +/// Wrapper holding a raw base pointer that is safe to share across workers because +/// each worker only ever touches a disjoint sub-slice computed from its task index. +struct SendPtr(*mut T); +// SAFETY: see `par_chunks_mut` — accesses are partitioned by task index. +unsafe impl Send for SendPtr {} +unsafe impl Sync for SendPtr {} + +impl SendPtr { + /// SAFETY: `n` must keep the result within the original allocation. + unsafe fn add(&self, n: usize) -> *mut T { + unsafe { self.0.add(n) } + } +} + +/// Parallel equivalent of `data.chunks_mut(chunk).enumerate().for_each(...)`. +/// +/// Splits `data` into `ceil(len / chunk)` consecutive chunks and runs +/// `f(chunk_index, chunk)` on each in parallel. The final chunk may be shorter. +/// Mirrors rayon's `par_chunks_mut().enumerate()` for the prover's kernels. +pub fn par_chunks_mut(data: &mut [T], chunk: usize, f: F) +where + F: Fn(usize, &mut [T]) + Sync, +{ + assert!(chunk > 0, "chunk size must be non-zero"); + let len = data.len(); + let n_chunks = len.div_ceil(chunk); + let base = SendPtr(data.as_mut_ptr()); + + for_each_index(n_chunks, |i| { + let start = i * chunk; + let this_len = chunk.min(len - start); + // SAFETY: distinct `i` produce non-overlapping `[start, start+this_len)` + // ranges within `data`, and the dispatcher keeps `data` borrowed for the + // whole call. `SendPtr` only re-exposes a pointer into that live borrow. + let slice = unsafe { std::slice::from_raw_parts_mut(base.add(start), this_len) }; + f(i, slice); + }); +} + +/// Parallel map-reduce over `0..n_tasks`, equivalent to +/// `(0..n_tasks).into_par_iter().map(map).reduce(identity, reduce)`. +/// +/// Each worker folds the task indices it claims into a single local accumulator, so +/// only one accumulator is allocated per worker (not one per task). The per-worker +/// partials are then combined on the dispatching thread. `reduce` must be +/// associative; the combination order is otherwise unspecified. +pub fn map_reduce(n_tasks: usize, identity: ID, map: M, reduce: R) -> T +where + T: Send, + ID: Fn() -> T, + M: Fn(usize) -> T + Sync, + R: Fn(T, T) -> T + Sync, +{ + if NUM_THREADS <= 1 || n_tasks <= 1 { + let mut acc = identity(); + for i in 0..n_tasks { + acc = reduce(acc, map(i)); + } + return acc; + } + + // One slot per worker id (0 == dispatcher). Each worker touches only its own slot. + let mut partials: Vec> = (0..NUM_THREADS).map(|_| None).collect(); + let slots = SendPtr(partials.as_mut_ptr()); + + for_each_index(n_tasks, |i| { + let wid = current_worker_id(); + // SAFETY: `wid` is unique per live worker and < NUM_THREADS, so slots are + // disjoint; `partials` outlives the dispatch (dispatcher blocks until done). + let slot = unsafe { &mut *slots.add(wid) }; + let v = map(i); + *slot = Some(match slot.take() { + Some(acc) => reduce(acc, v), + None => v, + }); + }); + + partials.into_iter().flatten().fold(identity(), &reduce) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::AtomicU64; + + #[test] + fn for_each_index_runs_all() { + let n = 10_000; + let sum = AtomicU64::new(0); + for_each_index(n, |i| { + sum.fetch_add(i as u64, Ordering::Relaxed); + }); + assert_eq!(sum.load(Ordering::Relaxed), (0..n as u64).sum()); + } + + #[test] + fn par_chunks_mut_writes_disjoint() { + let mut data = vec![0usize; 100_000]; + par_chunks_mut(&mut data, 64, |i, chunk| { + for (j, x) in chunk.iter_mut().enumerate() { + *x = i * 64 + j; + } + }); + for (idx, &v) in data.iter().enumerate() { + assert_eq!(v, idx); + } + } + + #[test] + fn map_reduce_matches_sequential() { + for n in [0usize, 1, 2, 1000, 100_000] { + let got = map_reduce(n, || 0u64, |i| i as u64, |a, b| a + b); + assert_eq!(got, (0..n as u64).sum::(), "scalar sum n={n}"); + } + // Vec accumulator (mirrors sumcheck's parallel_sum shape). + let n = 5000; + let got = map_reduce( + n, + || vec![0u64; 3], + |i| vec![i as u64, (i * 2) as u64, (i * 3) as u64], + |mut a, b| { + for (x, y) in a.iter_mut().zip(b) { + *x += y; + } + a + }, + ); + let s: u64 = (0..n as u64).sum(); + assert_eq!(got, vec![s, 2 * s, 3 * s]); + } + + #[test] + fn repeated_dispatch_is_stable() { + for _ in 0..50 { + let mut data = vec![0u32; 8192]; + par_chunks_mut(&mut data, 16, |_, chunk| { + for x in chunk.iter_mut() { + *x += 1; + } + }); + assert!(data.iter().all(|&x| x == 1)); + } + } +} diff --git a/crates/backend/poly/Cargo.toml b/crates/backend/poly/Cargo.toml index dcdf80aed..9839691ac 100644 --- a/crates/backend/poly/Cargo.toml +++ b/crates/backend/poly/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true field = { path = "../field", package = "mt-field" } utils = { path = "../utils", package = "mt-utils" } system-info.workspace = true +parallel.workspace = true itertools.workspace = true rayon.workspace = true diff --git a/crates/backend/poly/src/eq_mle.rs b/crates/backend/poly/src/eq_mle.rs index 64d3733f5..9c3c1b188 100644 --- a/crates/backend/poly/src/eq_mle.rs +++ b/crates/backend/poly/src/eq_mle.rs @@ -2,13 +2,47 @@ use crate::*; use crate::{EFPacking, PF}; use ::utils::{iter_array_chunks_padded, log2_ceil_usize, log2_strict_usize}; use field::*; -use rayon::prelude::*; use system_info::NUM_THREADS; const LOG_NUM_THREADS: usize = log2_ceil_usize(NUM_THREADS); -const NUM_THREADS_PADDED: usize = 1 << LOG_NUM_THREADS; const LOG_BATCHED_TILE_SIZE: usize = 14; +/// Oversubscription factor (log2) for the parallel fan-out: the prover's eq_mle +/// kernels split the output into `NUM_THREADS << PARALLEL_LOG_OVERSUB` chunks rather +/// than one per worker. Emitting more chunks than workers lets the pool's atomic +/// task counter rebalance across heterogeneous cores (e.g. Apple P/E cores) instead +/// of being bound by the slowest single chunk. `0` reproduces the one-chunk-per- +/// worker behavior. +/// +/// Default is `2` (4x): a deliberately conservative, machine-agnostic value. On the +/// M-series Mac the benefit saturates by 2-3x and a higher factor (4-5x) is slightly +/// faster, but those finer chunks risk regressing on many-core homogeneous CPUs +/// (more contention on the shared counter, worse cache reuse), so we take the low +/// end that captures most of the gain everywhere rather than the per-machine optimum. +/// Exposed as a runtime knob so the benchmark can re-sweep on each target machine. +pub static PARALLEL_LOG_OVERSUB: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(2); + +/// `(log2(n_chunks), n_chunks)` for the parallel fan-out, honoring [`PARALLEL_LOG_OVERSUB`]. +#[inline] +fn parallel_split() -> (usize, usize) { + let log_chunks = LOG_NUM_THREADS + PARALLEL_LOG_OVERSUB.load(std::sync::atomic::Ordering::Relaxed); + (log_chunks, 1 << log_chunks) +} + +/// Parallel equivalent of +/// `out.par_chunks_exact_mut(chunk).zip(buf).enumerate().for_each(|(i, (c, _))| g(i, c, &buf[i]))`, +/// dispatched through the in-house [`parallel`] pool. `chunk` must divide `out.len()` +/// exactly into `buf.len()` chunks (the eq_mle fan-out always does). +#[inline] +fn par_chunks_zip(out: &mut [T], chunk: usize, buf: &[A], g: G) +where + T: Send, + A: Sync, + G: Fn(&mut [T], &A) + Sync, +{ + parallel::par_chunks_mut(out, chunk, |i, c| g(c, &buf[i])); +} + /// Given `evals` = (α_1, ..., α_n), returns a multilinear polynomial P in n variables, /// defined on the boolean hypercube by: ∀ (x_1, ..., x_n) ∈ {0, 1}^n, /// P(x_1, ..., x_n) = Π_{i=1}^{n} (x_i.α_i + (1 - x_i).(1 - α_i)) @@ -105,7 +139,8 @@ where // If the number of variables is small, there is no need to use // parallelization or packings. - if eval.len() <= log_packing_width + 1 + LOG_NUM_THREADS { + let (log_chunks, n_chunks) = parallel_split(); + if eval.len() <= log_packing_width + 1 + log_chunks { // A basic recursive approach. eval_eq_basic::<_, _, _, INITIALIZED>(eval, out, scalar); return; @@ -114,16 +149,16 @@ where let eval_len_min_packing = eval.len() - log_packing_width; // We split eval into three parts: - // - eval[..LOG_NUM_THREADS] (the first LOG_NUM_THREADS elements) - // - eval[LOG_NUM_THREADS..eval_len_min_packing] (the middle elements) + // - eval[..log_chunks] (the first log_chunks elements) + // - eval[log_chunks..eval_len_min_packing] (the middle elements) // - eval[eval_len_min_packing..] (the last log_packing_width elements) // The middle elements are the ones which will be computed in parallel. // The last log_packing_width elements are the ones which will be packed. - // We make a buffer of elements of size `NUM_THREADS`. - let mut parallel_buffer = EF::ExtensionPacking::zero_vec(NUM_THREADS_PADDED); - let out_chunk_size = out.len() / NUM_THREADS_PADDED; + // We make a buffer with one entry per parallel chunk. + let mut parallel_buffer = EF::ExtensionPacking::zero_vec(n_chunks); + let out_chunk_size = out.len() / n_chunks; // Compute the equality polynomial corresponding to the last log_packing_width elements // and pack these. @@ -131,18 +166,13 @@ where // Update the buffer so it contains the evaluations of the equality polynomial // with respect to parts one and three. - fill_buffer(eval[..LOG_NUM_THREADS].iter().rev(), &mut parallel_buffer); + fill_buffer(eval[..log_chunks].iter().rev(), &mut parallel_buffer); // Finally do all computations involving the middle elements in parallel. - out.par_chunks_exact_mut(out_chunk_size) - .zip(parallel_buffer.par_iter()) - .for_each(|(out_chunk, buffer_val)| { - eval_eq_with_packed_scalar::<_, _, INITIALIZED>( - &eval[LOG_NUM_THREADS..(eval.len() - log_packing_width)], - out_chunk, - *buffer_val, - ); - }); + let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; + par_chunks_zip(out, out_chunk_size, ¶llel_buffer, |out_chunk, buffer_val| { + eval_eq_with_packed_scalar::<_, _, INITIALIZED>(middle, out_chunk, *buffer_val); + }); } #[inline] @@ -168,12 +198,13 @@ where // If the number of variables is small, there is no need to use // parallelization or packings. - if eval.len() <= log_packing_width + 1 + LOG_NUM_THREADS { - // A basic recursive approach. + let (log_chunks, n_chunks) = parallel_split(); + if eval.len() <= log_packing_width + 1 + log_chunks { + // A basic recursive approach. Small case: pack the result sequentially. let mut output_no_packing = EF::zero_vec(1 << eval.len()); eval_eq_basic::<_, _, _, false>(eval, &mut output_no_packing, scalar); - out.par_iter_mut() - .zip(output_no_packing.par_chunks_exact(packing_width)) + out.iter_mut() + .zip(output_no_packing.chunks_exact(packing_width)) .for_each(|(out_elem, chunk)| { if INITIALIZED { *out_elem += EF::ExtensionPacking::from_ext_slice(chunk); @@ -185,16 +216,16 @@ where let eval_len_min_packing = eval.len() - log_packing_width; // We split eval into three parts: - // - eval[..LOG_NUM_THREADS] (the first LOG_NUM_THREADS elements) - // - eval[LOG_NUM_THREADS..eval_len_min_packing] (the middle elements) + // - eval[..log_chunks] (the first log_chunks elements) + // - eval[log_chunks..eval_len_min_packing] (the middle elements) // - eval[eval_len_min_packing..] (the last log_packing_width elements) // The middle elements are the ones which will be computed in parallel. // The last log_packing_width elements are the ones which will be packed. - // We make a buffer of elements of size `NUM_THREADS`. - let mut parallel_buffer = EF::ExtensionPacking::zero_vec(NUM_THREADS_PADDED); - let out_chunk_size = out.len() / NUM_THREADS_PADDED; + // We make a buffer with one entry per parallel chunk. + let mut parallel_buffer = EF::ExtensionPacking::zero_vec(n_chunks); + let out_chunk_size = out.len() / n_chunks; // Compute the equality polynomial corresponding to the last log_packing_width elements // and pack these. @@ -202,18 +233,13 @@ where // Update the buffer so it contains the evaluations of the equality polynomial // with respect to parts one and three. - fill_buffer(eval[..LOG_NUM_THREADS].iter().rev(), &mut parallel_buffer); + fill_buffer(eval[..log_chunks].iter().rev(), &mut parallel_buffer); // Finally do all computations involving the middle elements in parallel. - out.par_chunks_exact_mut(out_chunk_size) - .zip(parallel_buffer.par_iter()) - .for_each(|(out_chunk, buffer_val)| { - eval_eq_with_packed_output::<_, _, INITIALIZED>( - &eval[LOG_NUM_THREADS..(eval.len() - log_packing_width)], - out_chunk, - *buffer_val, - ); - }); + let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; + par_chunks_zip(out, out_chunk_size, ¶llel_buffer, |out_chunk, buffer_val| { + eval_eq_with_packed_output::<_, _, INITIALIZED>(middle, out_chunk, *buffer_val); + }); } } @@ -249,7 +275,8 @@ where // If the number of variables is small, there is no need to use // parallelization or packings. - if eval.len() <= log_packing_width + 1 + LOG_NUM_THREADS { + let (log_chunks, n_chunks) = parallel_split(); + if eval.len() <= log_packing_width + 1 + log_chunks { // A basic recursive approach. eval_eq_basic::<_, _, _, INITIALIZED>(eval, out, scalar); return; @@ -258,19 +285,19 @@ where let eval_len_min_packing = eval.len() - log_packing_width; // We split eval into three parts: - // - eval[..LOG_NUM_THREADS] (the first LOG_NUM_THREADS elements) - // - eval[LOG_NUM_THREADS..eval_len_min_packing] (the middle elements) + // - eval[..log_chunks] (the first log_chunks elements) + // - eval[log_chunks..eval_len_min_packing] (the middle elements) // - eval[eval_len_min_packing..] (the last log_packing_width elements) // The middle elements are the ones which will be computed in parallel. // The last log_packing_width elements are the ones which will be packed. - // We make a buffer of PackedField elements of size `NUM_THREADS`. + // We make a buffer of PackedField elements, one entry per parallel chunk. // Note that this is a slightly different strategy to `eval_eq` which instead // uses PackedExtensionField elements. Whilst this involves slightly more mathematical // operations, it seems to be faster in practice due to less data moving around. - let mut parallel_buffer = F::Packing::zero_vec(NUM_THREADS_PADDED); - let out_chunk_size = out.len() / NUM_THREADS_PADDED; + let mut parallel_buffer = F::Packing::zero_vec(n_chunks); + let out_chunk_size = out.len() / n_chunks; // Compute the equality polynomial corresponding to the last log_packing_width elements // and pack these. @@ -278,19 +305,13 @@ where // Update the buffer so it contains the evaluations of the equality polynomial // with respect to parts one and three. - fill_buffer(eval[..LOG_NUM_THREADS].iter().rev(), &mut parallel_buffer); + fill_buffer(eval[..log_chunks].iter().rev(), &mut parallel_buffer); // Finally do all computations involving the middle elements in parallel. - out.par_chunks_exact_mut(out_chunk_size) - .zip(parallel_buffer.par_iter()) - .for_each(|(out_chunk, buffer_val)| { - base_eval_eq_packed::<_, _, INITIALIZED>( - &eval[LOG_NUM_THREADS..(eval.len() - log_packing_width)], - out_chunk, - *buffer_val, - scalar, - ); - }); + let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; + par_chunks_zip(out, out_chunk_size, ¶llel_buffer, |out_chunk, buffer_val| { + base_eval_eq_packed::<_, _, INITIALIZED>(middle, out_chunk, *buffer_val, scalar); + }); } #[inline] @@ -314,12 +335,13 @@ pub fn compute_eval_eq_base_packed( // If the number of variables is small, there is no need to use // parallelization or packings. - if eval.len() <= log_packing_width + 1 + LOG_NUM_THREADS { - // A basic recursive approach. + let (log_chunks, n_chunks) = parallel_split(); + if eval.len() <= log_packing_width + 1 + log_chunks { + // A basic recursive approach. Small case: pack the result sequentially. let mut output_no_packing = EF::zero_vec(1 << eval.len()); eval_eq_basic::<_, _, _, false>(eval, &mut output_no_packing, scalar); - out.par_iter_mut() - .zip(output_no_packing.par_chunks_exact(packing_width)) + out.iter_mut() + .zip(output_no_packing.chunks_exact(packing_width)) .for_each(|(out_elem, chunk)| { if INITIALIZED { *out_elem += EF::ExtensionPacking::from_ext_slice(chunk); @@ -331,19 +353,19 @@ pub fn compute_eval_eq_base_packed( let eval_len_min_packing = eval.len() - log_packing_width; // We split eval into three parts: - // - eval[..LOG_NUM_THREADS] (the first LOG_NUM_THREADS elements) - // - eval[LOG_NUM_THREADS..eval_len_min_packing] (the middle elements) + // - eval[..log_chunks] (the first log_chunks elements) + // - eval[log_chunks..eval_len_min_packing] (the middle elements) // - eval[eval_len_min_packing..] (the last log_packing_width elements) // The middle elements are the ones which will be computed in parallel. // The last log_packing_width elements are the ones which will be packed. - // We make a buffer of PackedField elements of size `NUM_THREADS`. + // We make a buffer of PackedField elements, one entry per parallel chunk. // Note that this is a slightly different strategy to `eval_eq` which instead // uses PackedExtensionField elements. Whilst this involves slightly more mathematical // operations, it seems to be faster in practice due to less data moving around. - let mut parallel_buffer = F::Packing::zero_vec(NUM_THREADS_PADDED); - let out_chunk_size = out.len() / NUM_THREADS_PADDED; + let mut parallel_buffer = F::Packing::zero_vec(n_chunks); + let out_chunk_size = out.len() / n_chunks; // Compute the equality polynomial corresponding to the last log_packing_width elements // and pack these. @@ -351,20 +373,14 @@ pub fn compute_eval_eq_base_packed( // Update the buffer so it contains the evaluations of the equality polynomial // with respect to parts one and three. - fill_buffer(eval[..LOG_NUM_THREADS].iter().rev(), &mut parallel_buffer); + fill_buffer(eval[..log_chunks].iter().rev(), &mut parallel_buffer); // Finally do all computations involving the middle elements in parallel. let scalar_packed = EF::ExtensionPacking::from(scalar); - out.par_chunks_exact_mut(out_chunk_size) - .zip(parallel_buffer.par_iter()) - .for_each(|(out_chunk, buffer_val)| { - base_eval_eq_packed_with_packed_output::( - &eval[LOG_NUM_THREADS..(eval.len() - log_packing_width)], - out_chunk, - *buffer_val, - scalar_packed, - ); - }); + let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; + par_chunks_zip(out, out_chunk_size, ¶llel_buffer, |out_chunk, buffer_val| { + base_eval_eq_packed_with_packed_output::(middle, out_chunk, *buffer_val, scalar_packed); + }); } } @@ -412,21 +428,21 @@ pub fn compute_eval_eq_base_packed_batched( }) .collect(); - out.par_chunks_exact_mut(tile_packed_size) - .enumerate() - .for_each(|(tile_idx, out_tile)| { - for (eq_prefix, middle, eq_suffix) in &per_query { - // Here e could precompute the eq poly, trading some memory for less computation - // (2x faster on M4 max, but 2x slower on machines with smaller caches. - // TODO implement both and choose based on cache size?) - base_eval_eq_packed_with_packed_output::( - middle, - out_tile, - *eq_suffix, - EF::ExtensionPacking::from(eq_prefix[tile_idx]), - ); - } - }); + // `out` already splits into `2^n_prefix_levels` tiles — many more than there are + // workers — so the pool's task counter load-balances these directly. + parallel::par_chunks_mut(out, tile_packed_size, |tile_idx, out_tile| { + for (eq_prefix, middle, eq_suffix) in &per_query { + // Here e could precompute the eq poly, trading some memory for less computation + // (2x faster on M4 max, but 2x slower on machines with smaller caches. + // TODO implement both and choose based on cache size?) + base_eval_eq_packed_with_packed_output::( + middle, + out_tile, + *eq_suffix, + EF::ExtensionPacking::from(eq_prefix[tile_idx]), + ); + } + }); } /// Fills the `buffer` with evaluations of the equality polynomial @@ -944,39 +960,40 @@ pub fn compute_eval_eq_packed_dual( assert!(log_packing_width <= eval_a.len()); assert_eq!(out.len(), 1 << (eval_a.len() - log_packing_width)); - if eval_a.len() <= log_packing_width + 1 + LOG_NUM_THREADS { + let (log_chunks, n_chunks) = parallel_split(); + if eval_a.len() <= log_packing_width + 1 + log_chunks { let mut output_no_packing = EF::zero_vec(1 << eval_a.len()); eval_eq_basic::<_, _, _, false>(eval_a, &mut output_no_packing, scalar_a); eval_eq_basic::<_, _, _, true>(eval_b, &mut output_no_packing, scalar_b); - out.par_iter_mut() - .zip(output_no_packing.par_chunks_exact(packing_width)) + out.iter_mut() + .zip(output_no_packing.chunks_exact(packing_width)) .for_each(|(out_elem, chunk)| { *out_elem = EF::ExtensionPacking::from_ext_slice(chunk); }); } else { let eval_len_min_packing = eval_a.len() - log_packing_width; - let mut parallel_buffer_a = EF::ExtensionPacking::zero_vec(NUM_THREADS_PADDED); - let mut parallel_buffer_b = EF::ExtensionPacking::zero_vec(NUM_THREADS_PADDED); - let out_chunk_size = out.len() / NUM_THREADS_PADDED; + let mut parallel_buffer_a = EF::ExtensionPacking::zero_vec(n_chunks); + let mut parallel_buffer_b = EF::ExtensionPacking::zero_vec(n_chunks); + let out_chunk_size = out.len() / n_chunks; parallel_buffer_a[0] = packed_eq_poly(&eval_a[eval_len_min_packing..], scalar_a); - fill_buffer(eval_a[..LOG_NUM_THREADS].iter().rev(), &mut parallel_buffer_a); + fill_buffer(eval_a[..log_chunks].iter().rev(), &mut parallel_buffer_a); parallel_buffer_b[0] = packed_eq_poly(&eval_b[eval_len_min_packing..], scalar_b); - fill_buffer(eval_b[..LOG_NUM_THREADS].iter().rev(), &mut parallel_buffer_b); - - out.par_chunks_exact_mut(out_chunk_size) - .enumerate() - .for_each(|(i, out_chunk)| { - eval_eq_with_packed_output_dual::, EF>( - &eval_a[LOG_NUM_THREADS..eval_len_min_packing], - &eval_b[LOG_NUM_THREADS..eval_len_min_packing], - out_chunk, - parallel_buffer_a[i], - parallel_buffer_b[i], - ); - }); + fill_buffer(eval_b[..log_chunks].iter().rev(), &mut parallel_buffer_b); + + let middle_a = &eval_a[log_chunks..eval_len_min_packing]; + let middle_b = &eval_b[log_chunks..eval_len_min_packing]; + parallel::par_chunks_mut(out, out_chunk_size, |i, out_chunk| { + eval_eq_with_packed_output_dual::, EF>( + middle_a, + middle_b, + out_chunk, + parallel_buffer_a[i], + parallel_buffer_b[i], + ); + }); } } @@ -1312,7 +1329,7 @@ mod tests { let time = Instant::now(); compute_eval_eq::(&eval, &mut out_3, scalar); let out_3_packed = out_3 - .par_chunks_exact(packing_width) + .chunks_exact(packing_width) .map(>::ExtensionPacking::from_ext_slice) .collect::>(); println!("EXTENSION PACKED AFTER: {:?}", time.elapsed()); @@ -1347,7 +1364,7 @@ mod tests { let time = Instant::now(); compute_eval_eq_base::(&eval, &mut out_3, scalar); let out_3_packed = out_3 - .par_chunks_exact(packing_width) + .chunks_exact(packing_width) .map(>::ExtensionPacking::from_ext_slice) .collect::>(); println!("BASE PACKED AFTER: {:?}", time.elapsed()); @@ -1357,6 +1374,84 @@ mod tests { } } + #[test] + #[ignore = "benchmark; run explicitly with --ignored --nocapture"] + fn bench_pool_oversub() { + use std::sync::atomic::Ordering; + use std::time::Instant; + + // Sweep oversubscription log-factors. 0 == one chunk per worker. + const FACTORS: [usize; 6] = [0, 1, 2, 3, 4, 5]; + + let mut rng = StdRng::seed_from_u64(0); + + // Time `f` over `iters` runs after `warmup` discarded runs; report best. + fn timed(warmup: usize, iters: usize, mut f: impl FnMut()) -> std::time::Duration { + for _ in 0..warmup { + f(); + } + let mut best = std::time::Duration::MAX; + for _ in 0..iters { + let t = Instant::now(); + f(); + best = best.min(t.elapsed()); + } + best + } + + // Time `run` at every oversub factor; print ms (best-of-N) per factor, with + // the per-row best marked. Lets us pick a factor robust across machines. + fn sweep(label: &str, n_vars: usize, warmup: usize, iters: usize, mut run: impl FnMut()) { + let restore = PARALLEL_LOG_OVERSUB.load(Ordering::Relaxed); + let times: Vec = FACTORS + .iter() + .map(|&f| { + PARALLEL_LOG_OVERSUB.store(f, Ordering::Relaxed); + timed(warmup, iters, &mut run).as_secs_f64() * 1e3 + }) + .collect(); + PARALLEL_LOG_OVERSUB.store(restore, Ordering::Relaxed); + let best = times.iter().copied().fold(f64::MAX, f64::min); + print!(" {label:>14} n={n_vars:>2} |"); + for &t in × { + let mark = if (t - best).abs() < 1e-9 { '*' } else { ' ' }; + print!(" {t:>6.2}{mark}"); + } + println!(); + } + + print!("\n oversub factor:"); + for f in FACTORS { + print!(" {f:>2}x "); + } + println!(" (ms, best-of-N, * = row best)"); + for n_vars in [18usize, 20, 22, 23, 24] { + let eval_ef: Vec = (0..n_vars).map(|_| rng.random()).collect(); + let eval_f: Vec = (0..n_vars).map(|_| rng.random()).collect(); + let scalar: EF = rng.random(); + let (warmup, iters) = if n_vars >= 23 { (1, 3) } else { (2, 10) }; + + // Correctness: the factor must not change the result. + PARALLEL_LOG_OVERSUB.store(0, Ordering::Relaxed); + let mut ref_out = EF::zero_vec(1 << n_vars); + compute_eval_eq::(&eval_ef, &mut ref_out, scalar); + for f in FACTORS { + PARALLEL_LOG_OVERSUB.store(f, Ordering::Relaxed); + let mut out = EF::zero_vec(1 << n_vars); + compute_eval_eq::(&eval_ef, &mut out, scalar); + assert_eq!(ref_out, out, "oversub {f} changed output (ext) at n={n_vars}"); + } + + let mut out = EF::zero_vec(1 << n_vars); + sweep("eval_eq (ext)", n_vars, warmup, iters, || { + compute_eval_eq::(&eval_ef, &mut out, scalar); + }); + sweep("eval_eq_base", n_vars, warmup, iters, || { + compute_eval_eq_base::(&eval_f, &mut out, scalar); + }); + } + } + #[test] fn test_compute_eval_eq_packed_dual() { let packing_width = ::Packing::WIDTH; diff --git a/crates/backend/sumcheck/Cargo.toml b/crates/backend/sumcheck/Cargo.toml index 91085f352..98316c8cd 100644 --- a/crates/backend/sumcheck/Cargo.toml +++ b/crates/backend/sumcheck/Cargo.toml @@ -8,6 +8,7 @@ field = { path = "../field", package = "mt-field" } air = { path = "../air", package = "mt-air" } poly = { path = "../poly", package = "mt-poly" } fiat-shamir = { path = "../fiat-shamir", package = "mt-fiat-shamir" } +parallel.workspace = true tracing.workspace = true rayon.workspace = true diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index f5f84e470..9d115d583 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -73,10 +73,7 @@ where if size < PARALLEL_THRESHOLD { (0..size).fold(T::zero_vec(n), |acc, i| accumulate(acc, compute_iteration(i))) } else { - (0..size) - .into_par_iter() - .map(compute_iteration) - .reduce(|| T::zero_vec(n), accumulate) + parallel::map_reduce(size, || T::zero_vec(n), compute_iteration, accumulate) } } diff --git a/crates/backend/zk-alloc/Cargo.toml b/crates/backend/zk-alloc/Cargo.toml index fe4c12233..8f783af1e 100644 --- a/crates/backend/zk-alloc/Cargo.toml +++ b/crates/backend/zk-alloc/Cargo.toml @@ -6,6 +6,7 @@ description = "Bump+reset arena allocator for ZK proving workloads" [dependencies] system-info.workspace = true +parallel.workspace = true [dev-dependencies] rayon.workspace = true diff --git a/crates/backend/zk-alloc/src/lib.rs b/crates/backend/zk-alloc/src/lib.rs index 1b43143d6..bbdef75ff 100644 --- a/crates/backend/zk-alloc/src/lib.rs +++ b/crates/backend/zk-alloc/src/lib.rs @@ -100,6 +100,13 @@ pub fn init() { /// Activates the arena and resets every thread's slab. All allocations until the next /// `end_phase()` go to the arena; the previous phase's data is overwritten in place. pub fn begin_phase() { + // Ensure the `parallel` thread pool is fully constructed *before* the arena goes + // active. Its persistent state must live in the system allocator: if it were + // lazily created during a phase it would land in a slab that the next + // `begin_phase()` recycles, corrupting the workers' barriers. Idempotent, so the + // cost after the first call is a single atomic load. + parallel::init(); + let prev_active = ARENA_ACTIVE.swap(true, Ordering::Release); assert!( !prev_active, diff --git a/tests/test_zk_alloc.rs b/tests/test_zk_alloc.rs index d826ed80f..1177db876 100644 --- a/tests/test_zk_alloc.rs +++ b/tests/test_zk_alloc.rs @@ -15,11 +15,17 @@ fn test_aggregation_with_zk_alloc() { let signatures = get_benchmark_signatures(); let raw_xmss = signatures[0..6].to_vec(); - begin_phase(); - let aggregated = aggregate_type_1(&[], raw_xmss, message, slot, log_inv_rate).unwrap(); - end_phase(); - // IMPORTANT: clone to move the data out of the arena memory - let aggregated = aggregated.clone(); + // Run TWO phases. The first phase lazily initializes anything that allocates + // during proving (notably the `parallel` thread pool); the second `begin_phase()` + // resets the arena slabs. Persistent state created in phase 1 must survive that + // reset — a single-phase test would not catch a regression where it doesn't. + for _ in 0..2 { + begin_phase(); + let aggregated = aggregate_type_1(&[], raw_xmss.clone(), message, slot, log_inv_rate).unwrap(); + end_phase(); + // IMPORTANT: clone to move the data out of the arena memory + let aggregated = aggregated.clone(); - verify_type_1(&aggregated).unwrap(); + verify_type_1(&aggregated).unwrap(); + } } From 2c46cf916f3f95e678539ea4d96e34a80c90f92d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 04:18:16 +0400 Subject: [PATCH 02/65] sumcheck: port clean reductions to parallel::map_reduce - sc_computation.rs: the two `into_par_iter().map().reduce()` fold sites now use parallel::map_reduce; file is fully rayon-free. - product_computation.rs: the element-wise and chunked pure reductions in compute_product_sumcheck_polynomial{,_base_ext_packed} ported to map_reduce. The fold_and_compute* sites still use par_zip_fold_2 (the composable split-iterator family) and remain on rayon pending a dedicated pass, so the file keeps its rayon import. Full suite green (55 suites), incl. the two-phase zk-alloc aggregation that exercises these reductions through real proofs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../sumcheck/src/product_computation.rs | 56 +++++++++---------- crates/backend/sumcheck/src/sc_computation.rs | 25 +++++---- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index 2828af039..eaa9fc1dc 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -146,15 +146,13 @@ pub fn compute_product_sumcheck_polynomial< (a0 + b0, a2 + b2) }) } else { - pol_0[..n / 2] - .par_iter() - .zip(pol_0[n / 2..].par_iter()) - .zip(pol_1[..n / 2].par_iter().zip(pol_1[n / 2..].par_iter())) - .map(sumcheck_quadratic) - .reduce( - || (EFPacking::ZERO, EFPacking::ZERO), - |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), - ) + let half = n / 2; + parallel::map_reduce( + half, + || (EFPacking::ZERO, EFPacking::ZERO), + |i| sumcheck_quadratic(((&pol_0[i], &pol_0[half + i]), (&pol_1[i], &pol_1[half + i]))), + |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), + ) }; let c0 = decompose(c0_packed).into_iter().sum::(); @@ -186,15 +184,17 @@ pub fn compute_product_sumcheck_polynomial_base_ext_packed< let chunk_size = 1024; - let (c0_acc, c2_acc) = pol_0[..half] - .par_chunks(chunk_size) - .zip(pol_0[half..].par_chunks(chunk_size)) - .zip( - pol_1[..half] - .par_chunks(chunk_size) - .zip(pol_1[half..].par_chunks(chunk_size)), - ) - .map(|((b_lo, b_hi), (e_lo, e_hi))| { + let n_chunks = half.div_ceil(chunk_size); + let (c0_acc, c2_acc) = parallel::map_reduce( + n_chunks, + || ([0u128; DIM], [0i128; DIM]), + |c| { + let start = c * chunk_size; + let end = (start + chunk_size).min(half); + let b_lo = &pol_0[start..end]; + let b_hi = &pol_0[half + start..half + end]; + let e_lo = &pol_1[start..end]; + let e_hi = &pol_1[half + start..half + end]; let mut c0 = [0u128; DIM]; let mut c2 = [0i128; DIM]; for i in 0..b_lo.len() { @@ -216,17 +216,15 @@ pub fn compute_product_sumcheck_polynomial_base_ext_packed< } } (c0, c2) - }) - .reduce( - || ([0u128; DIM], [0i128; DIM]), - |(mut a0, mut a2): Acc, (b0, b2): Acc| { - for j in 0..DIM { - a0[j] += b0[j]; - a2[j] += b2[j]; - } - (a0, a2) - }, - ); + }, + |(mut a0, mut a2): Acc, (b0, b2): Acc| { + for j in 0..DIM { + a0[j] += b0[j]; + a2[j] += b2[j]; + } + (a0, a2) + }, + ); let c0 = EF::from_basis_coefficients_fn(|j| F::reduce_product_sum(c0_acc[j])); let c2 = EF::from_basis_coefficients_fn(|j| F::reduce_signed_product_sum(c2_acc[j])); diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index 9d115d583..3e9a3bbc6 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -2,7 +2,6 @@ use crate::*; use air::*; use field::*; use poly::*; -use rayon::prelude::*; use std::any::TypeId; use std::ops::{Add, AddAssign, Mul, MulAssign, Sub}; @@ -564,9 +563,10 @@ where acc }; - let sums: Vec> = (0..n_lo) - .into_par_iter() - .map(|b_lo| { + let sums: Vec> = parallel::map_reduce( + n_lo, + zero, + |b_lo| { let eq_lo_bc = EFPacking::::from(eq_lo[b_lo]); let base = b_lo << log_packed_hi; let mut block_acc = zero(); @@ -605,8 +605,9 @@ where *a *= eq_lo_bc; } block_acc - }) - .reduce(zero, accumulate); + }, + accumulate, + ); let unpacked = sums.into_iter().map(&unpack_sum); build_evals(unpacked, missing_mul_factor) @@ -651,9 +652,10 @@ where acc }; - let sums: Vec> = (0..n_lo) - .into_par_iter() - .map(|b_lo| { + let sums: Vec> = parallel::map_reduce( + n_lo, + zero, + |b_lo| { let eq_lo_bc = EFPacking::::from(eq_lo[b_lo]); let base = b_lo << log_packed_hi; let mut block_acc = zero(); @@ -696,8 +698,9 @@ where *a *= eq_lo_bc; } block_acc - }) - .reduce(zero, accumulate); + }, + accumulate, + ); let unpacked = sums.into_iter().map(&unpack_sum); (build_evals(unpacked, missing_mul_factor), wrap_f(folded_f)) From aa773e05ebd4a0a636171e0d153d73631643fa81 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 04:29:18 +0400 Subject: [PATCH 03/65] parallel/sumcheck: per-worker scratch in sumcheck_compute_core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add parallel::map_reduce_with_state: a parallel reduce where each worker keeps reusable scratch alongside its accumulator, folding many task indices into one accumulator with one scratch buffer (vs map_reduce's fresh-value-per-task). Rewrite sumcheck_compute_core to use it: per task it previously allocated `rows`, the `point` (once + per degree step), and the returned `evals` Vec — ~(2 + degree) tiny Vecs times fold_size tasks. Now each worker reuses one `rows` and one `point` buffer and accumulates directly into its partial, so the hot loop allocates nothing. Required changing the core's `eval_fn` to take `&[IF]` instead of `Vec` (it only read it); the 4 call-site closures updated accordingly. This is the mechanism meant to recover what the zk-alloc arena absorbs. Scope is the non-fold core only; the fold variant still uses parallel_sum (Vec-returning) for now. Full suite green (55 suites) + two-phase zk-alloc aggregation. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/parallel/src/lib.rs | 76 +++++++++++++++++ crates/backend/sumcheck/src/sc_computation.rs | 84 +++++++++++-------- 2 files changed, 124 insertions(+), 36 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 79ce252f1..df406c5d2 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -290,6 +290,54 @@ where partials.into_iter().flatten().fold(identity(), &reduce) } +/// Parallel reduce where each worker keeps reusable **scratch** alongside its +/// accumulator, so the per-task body can avoid allocating. +/// +/// Each worker creates `(scratch, acc)` once (via `init_state` / `init_acc`) the +/// first time it claims a task, then calls `fold(&mut scratch, &mut acc, i)` for +/// every task index it owns — reusing `scratch` across all of them. The per-worker +/// `acc`s are then combined on the dispatching thread, starting from `init_acc()`. +/// +/// This is the allocation-elimination counterpart to [`map_reduce`]: where that one +/// builds and reduces a fresh value per task, this one lets a worker fold many tasks +/// into one accumulator using one scratch buffer. `combine` must be associative. +pub fn map_reduce_with_state(n_tasks: usize, init_state: IS, init_acc: IA, fold: F, combine: C) -> A +where + S: Send, + A: Send, + IS: Fn() -> S + Sync, + IA: Fn() -> A + Sync, + F: Fn(&mut S, &mut A, usize) + Sync, + C: Fn(A, A) -> A, +{ + if NUM_THREADS <= 1 || n_tasks <= 1 { + let mut state = init_state(); + let mut acc = init_acc(); + for i in 0..n_tasks { + fold(&mut state, &mut acc, i); + } + return acc; + } + + let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); + let ptr = SendPtr(slots.as_mut_ptr()); + + for_each_index(n_tasks, |i| { + let wid = current_worker_id(); + // SAFETY: `wid` is unique per live worker and < NUM_THREADS, so slots are + // disjoint; `slots` outlives the dispatch (dispatcher blocks until done). + let slot = unsafe { &mut *ptr.add(wid) }; + let (state, acc) = slot.get_or_insert_with(|| (init_state(), init_acc())); + fold(state, acc, i); + }); + + slots + .into_iter() + .flatten() + .map(|(_, acc)| acc) + .fold(init_acc(), &combine) +} + #[cfg(test)] mod tests { use super::*; @@ -341,6 +389,34 @@ mod tests { assert_eq!(got, vec![s, 2 * s, 3 * s]); } + #[test] + fn map_reduce_with_state_matches_sequential() { + // Scratch (reused Vec) + Vec accumulator, mirroring the sumcheck use. + for n in [0usize, 1, 3, 1000, 50_000] { + let got = map_reduce_with_state( + n, + Vec::::new, // scratch + || vec![0u64; 2], // accumulator + |scratch: &mut Vec, acc: &mut Vec, i| { + scratch.clear(); + scratch.push(i as u64); + scratch.push((i * i) as u64); + acc[0] += scratch[0]; + acc[1] += scratch[1]; + }, + |mut a: Vec, b: Vec| { + for (x, y) in a.iter_mut().zip(b) { + *x += y; + } + a + }, + ); + let s0: u64 = (0..n as u64).sum(); + let s1: u64 = (0..n as u64).map(|i| i * i).sum(); + assert_eq!(got, vec![s0, s1], "n={n}"); + } + } + #[test] fn repeated_dispatch_is_stable() { for _ in 0..50 { diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index 3e9a3bbc6..dbd1e7be1 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -218,7 +218,7 @@ where extra_data, missing_mul_factor, packed_fold_size, - |sc, pf, ed| sc.eval_packed_extension(&pf, ed), + |sc, pf, ed| sc.eval_packed_extension(pf, ed), packing_unpack_sum, ), MleGroupRef::BasePacked(multilinears) => { @@ -236,7 +236,7 @@ where extra_data, missing_mul_factor, packed_fold_size, - |sc, pf, ed| sc.eval_packed_base(&pf, ed), + |sc, pf, ed| sc.eval_packed_base(pf, ed), packing_unpack_sum, ) } @@ -248,7 +248,7 @@ where extra_data, missing_mul_factor, fold_size, - |sc, pf, ed| sc.eval_base(&pf, ed), + |sc, pf, ed| sc.eval_base(pf, ed), |s| s, ), MleGroupRef::Extension(multilinears) => sumcheck_compute_core( @@ -259,7 +259,7 @@ where extra_data, missing_mul_factor, fold_size, - |sc, pf, ed| sc.eval_extension(&pf, ed), + |sc, pf, ed| sc.eval_extension(pf, ed), |s| s, ), } @@ -400,7 +400,7 @@ fn sumcheck_compute_core( extra_data: &SC::ExtraData, missing_mul_factor: Option, fold_size: usize, - eval_fn: impl Fn(&SC, Vec, &SC::ExtraData) -> EFT + Sync + Send, + eval_fn: impl Fn(&SC, &[IF], &SC::ExtraData) -> EFT + Sync + Send, unpack_sum: impl Fn(EFT) -> EF, ) -> Vec where @@ -416,43 +416,55 @@ where + MulAssign, SC: SumcheckComputation, { - let compute_at = |i: usize, eq_val: Option| -> Vec { - let mut rows = multilinears - .iter() - .map(|m| { + // Per-worker scratch: `rows` (the [lo, diff, hi] triples) and `point` (the + // evaluation point handed to `eval_fn`) are reused across every task a worker + // owns, so the hot loop allocates nothing — recovering what the zk-alloc arena + // would otherwise absorb. `acc` (length `degree`) is the per-worker partial sum. + let n_mult = multilinears.len(); + let sums = parallel::map_reduce_with_state( + fold_size, + || (Vec::<[IF; 3]>::with_capacity(n_mult), Vec::::with_capacity(n_mult)), + || EFT::zero_vec(degree), + |(rows, point), acc, i| { + let eq_val = eq_at(i); + + rows.clear(); + rows.extend(multilinears.iter().map(|m| { let lo = m[i]; let hi = m[i + fold_size]; - let diff_hi_lo = hi - lo; - [lo, diff_hi_lo, hi] - }) - .collect::>(); + [lo, hi - lo, hi] + })); - // z = 0 - let point_0 = rows.iter().map(|row| row[0]).collect::>(); - let mut eval_0 = eval_fn(computation, point_0, extra_data); - if let Some(eq) = eq_val { - eval_0 *= eq; - } - - let mut evals = Vec::with_capacity(degree); - evals.push(eval_0); - - // z = 2, 3, ... - for _ in 1..degree { - for [_, diff_hi_lo, running] in &mut rows { - *running += *diff_hi_lo; - } - let point_f = rows.iter().map(|row| row[2]).collect::>(); - let mut eval = eval_fn(computation, point_f, extra_data); + // z = 0 + point.clear(); + point.extend(rows.iter().map(|row| row[0])); + let mut eval_0 = eval_fn(computation, point, extra_data); if let Some(eq) = eq_val { - eval *= eq; + eval_0 *= eq; } - evals.push(eval); - } - evals - }; + acc[0] += eval_0; - let sums = parallel_sum(fold_size, degree, |i| compute_at(i, eq_at(i))); + // z = 2, 3, ... + for acc_d in acc.iter_mut().skip(1) { + for [_, diff_hi_lo, running] in rows.iter_mut() { + *running += *diff_hi_lo; + } + point.clear(); + point.extend(rows.iter().map(|row| row[2])); + let mut eval = eval_fn(computation, point, extra_data); + if let Some(eq) = eq_val { + eval *= eq; + } + *acc_d += eval; + } + }, + |mut a: Vec, b: Vec| { + for (x, y) in a.iter_mut().zip(b) { + *x += y; + } + a + }, + ); let unpacked_sums = sums.into_iter().map(&unpack_sum); build_evals(unpacked_sums, missing_mul_factor) } From 56d2a594fddccdc485109dcb8581a8f13abdc9f4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 05:06:45 +0400 Subject: [PATCH 04/65] parallel: lock-free spin-then-park dispatch (replaces std::Barrier) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The barrier-based dispatch (two N-thread condvar rendezvous + waking all sleeping workers per dispatch) cost ~94µs/dispatch, ~2x rayon. Replace it with a generation counter that idle workers watch by spinning (parking only after SPIN_LIMIT), a lock-free atomic countdown for completion, and a per-worker parked flag (SeqCst vs generation) so the unpark syscall is skipped while hot. Now ~7.5µs/dispatch — ~5x faster than rayon in isolation. This keeps the custom pool (vs retreating to rayon): owning the runtime is what enables per-worker scratch, dropping the flush_rayon hack, and eventually removing zk-alloc. NOTE: while rayon still exists elsewhere, the two coexisting pools regress the prover ~30%; that tax only disappears once rayon is removed everywhere, so intermediate (partial-migration) benchmarks are not meaningful — only the all-pool end state is. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/parallel/src/lib.rs | 273 ++++++++++++++++------------- 1 file changed, 151 insertions(+), 122 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index df406c5d2..8cf68c10b 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -1,45 +1,57 @@ //! Minimal fixed-size thread pool for flat, static data-parallel kernels. //! -//! This is a deliberately tiny alternative to rayon for the one shape the prover -//! actually uses on its hot paths: "split a slice into N pieces, run a closure on -//! each." Unlike rayon it does **no** work-stealing of nested tasks and allocates -//! **nothing** per dispatch — the whole point is to remove the per-task heap -//! traffic that currently forces the `zk-alloc` arena to exist. +//! A deliberately tiny alternative to rayon for the one shape the prover uses on its +//! hot paths: "split a range/slice into pieces, run a closure on each." Unlike rayon +//! it does **no** work-stealing of nested tasks and allocates **nothing** per +//! dispatch — the whole point is to own the runtime so we can (a) attach per-worker +//! scratch buffers (eliminating the per-task allocations that justify the `zk-alloc` +//! arena), and (b) drop rayon entirely along with its `flush_rayon` injector hack. //! //! ## Model //! -//! The pool owns exactly `NUM_THREADS - 1` background worker threads with stable -//! ids `1..NUM_THREADS`. The dispatching thread acts as worker `0` and runs its -//! share inline, so a dispatch keeps all `NUM_THREADS` hardware threads busy with -//! only `NUM_THREADS - 1` extra threads (no oversubscription, matching the -//! build-time `NUM_THREADS` assumption baked in elsewhere). +//! The pool owns exactly `NUM_THREADS - 1` background workers with stable ids +//! `1..NUM_THREADS`; the dispatching thread acts as worker `0` and runs its share +//! inline (so a dispatch keeps all `NUM_THREADS` hardware threads busy with only +//! `NUM_THREADS - 1` extra threads — no oversubscription). Tasks are claimed from a +//! shared atomic counter, giving dynamic load balancing for free. //! -//! Tasks are claimed from a shared atomic counter. That gives dynamic load -//! balancing across uneven task costs for free, while still allocating nothing. +//! ## Dispatch is lock-free on the hot path //! -//! ## Why stable worker ids +//! A `std::Barrier` (mutex + condvar) wake-up costs ~2x rayon per dispatch, which the +//! prover's many dispatches turn into a real regression. Instead, dispatch bumps a +//! `generation` counter that idle workers watch by **spinning** (so back-to-back +//! dispatches never pay a syscall), parking only after `SPIN_LIMIT` unrewarded spins. +//! Completion is a lock-free atomic countdown (`working`) the dispatcher spins on. +//! Park/unpark uses a per-worker `parked` flag with SeqCst ordering against +//! `generation` to avoid lost wake-ups, so the unpark syscall is skipped while +//! workers are hot. Measured ~7.5µs/dispatch vs rayon's ~37µs. //! -//! [`current_worker_id`] returns a stable `0..NUM_THREADS` id. This is the hook for -//! a future per-worker scratch-buffer strategy: once each worker can index its own -//! preallocated scratch, the short-lived per-task allocations that `zk-alloc` -//! currently absorbs disappear at the source, and the arena can be dropped. +//! ## Coexistence caveat +//! +//! Running this pool *alongside* rayon (a partial migration) regresses the prover +//! ~30% — work bounces between two disjoint thread sets, thrashing caches and +//! oversubscribing at region boundaries. This pool only pays off once rayon is gone +//! everywhere. Treat partial-migration benchmarks accordingly. //! //! ## Constraints //! -//! - **No nesting.** A worker must not itself dispatch (it would deadlock on the -//! dispatch lock / barriers). The prover's parallel sections are flat, so this -//! holds. Nesting is a logic error, not a soundness hole. +//! - **No nesting.** A worker must not itself dispatch (it would deadlock). The +//! prover's parallel sections are flat, so this holds. //! - **One dispatcher at a time.** Concurrent dispatches are serialized by a mutex. use std::cell::{Cell, UnsafeCell}; use std::ptr::NonNull; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Barrier, Mutex, Once, OnceLock}; +use std::sync::{Mutex, Once, OnceLock}; +use std::thread::Thread; use system_info::NUM_THREADS; -/// Total worker count (including the dispatching thread). Equal to the build-time -/// `NUM_THREADS`. +/// Spins an idle worker performs before parking. Tuned so back-to-back dispatches +/// keep workers hot (no syscalls) while a long idle stretch still lets them sleep. +const SPIN_LIMIT: u32 = 1 << 16; + +/// Total worker count (including the dispatching thread). Equal to build-time `NUM_THREADS`. #[must_use] pub const fn num_threads() -> usize { NUM_THREADS @@ -47,73 +59,63 @@ pub const fn num_threads() -> usize { thread_local! { /// Stable id of this thread within the pool. Set once per background worker; - /// stays `0` on the dispatching thread (which acts as worker 0) and on any - /// thread that never participates. + /// stays `0` on the dispatching thread (worker 0) and on any non-worker thread. static WORKER_ID: Cell = const { Cell::new(0) }; } -/// Stable id of the calling worker, in `0..NUM_THREADS`. Returns `0` on the -/// dispatching thread and on any non-worker thread. +/// Stable id of the calling worker, in `0..NUM_THREADS` (`0` off-pool). The hook for +/// per-worker scratch buffers. #[must_use] pub fn current_worker_id() -> usize { WORKER_ID.with(Cell::get) } -/// A type-erased unit of parallel work. `f` is a `&(dyn Fn(usize) + Sync)` whose -/// lifetime has been erased to `'static`: it is only ever dereferenced between the -/// start and end barriers of a single dispatch, during which the dispatcher blocks, -/// so the borrow it came from outlives every call. +/// Type-erased unit of work: a `&(dyn Fn(usize) + Sync)` whose lifetime is erased to +/// `'static`. Only dereferenced inside a dispatch window during which the dispatcher +/// blocks, so the source borrow outlives every call. struct Job { f: NonNull, n_tasks: usize, } struct Pool { - /// Current job. Written by the dispatcher before `start.wait()` and read by - /// workers after it; the barrier supplies the happens-before relationship, so - /// no additional synchronization on this cell is required. + /// Current job. Written by the dispatcher before bumping `generation`, read by + /// workers after they observe the bump; `generation` supplies the happens-before. job: UnsafeCell>, + /// Bumped once per dispatch. Idle workers watch it (spin, then park). + generation: AtomicUsize, /// Next task index to claim. Reset to 0 before each dispatch. counter: AtomicUsize, + /// Background workers still draining the current dispatch; dispatcher spins to 0. + working: AtomicUsize, shutdown: AtomicBool, - /// Workers park here between dispatches; the dispatcher releases them. - start: Barrier, - /// Everyone meets here once all tasks are drained. - end: Barrier, + /// Per-worker "currently parked" flags (indexed by worker id; slot 0 unused). + parked: Vec, + /// Per-worker thread handles for `unpark` (indexed by worker id; slot 0 unused). + handles: Vec>, /// Serializes dispatchers: only one thread may drive the pool at a time. dispatch: Mutex<()>, } -// SAFETY: `job` is only mutated by the unique dispatcher (serialized by `dispatch`) -// while workers are parked, and only read by workers/dispatcher while no one writes; -// the barriers order these phases. The erased `Job` pointer is never used outside a -// dispatch window during which its source borrow is live. +// SAFETY: `job` is mutated only by the unique dispatcher while workers are parked or +// before they observe the generation bump, and read only after; the `generation` +// release/acquire (and SeqCst park protocol) order these phases. The erased `Job` +// pointer is never used outside a dispatch window during which its borrow is live. unsafe impl Sync for Pool {} unsafe impl Send for Pool {} -/// Construct the pool and exercise its full dispatch path once, now. -/// -/// **Must be called before any arena allocator that recycles memory between phases -/// is active** (e.g. before `zk_alloc::begin_phase()`), and is idempotent. +/// Construct the pool and exercise its dispatch path once, now. /// -/// Two things must end up in the system allocator (not a recyclable arena slab): -/// 1. the leaked `Pool` struct, and -/// 2. the OS sync primitives behind `Mutex`/`Barrier`. On macOS std allocates the -/// underlying `pthread_mutex_t` / `pthread_cond_t` **lazily on first use**, not at -/// construction. So merely building the `Pool` is not enough — if the first -/// `lock()`/`wait()` happened during a phase, that primitive would land in the -/// arena and the next reset would corrupt it (observed as `EINVAL` on lock). -/// -/// Running one real dispatch here forces every lazy primitive (the dispatch mutex -/// and both barriers, touched by the dispatcher and every worker) to allocate while -/// the arena is inactive, pinning them in the system allocator for good. +/// **Must be called before any arena allocator that recycles memory between phases is +/// active** (e.g. before `zk_alloc::begin_phase()`); idempotent. The leaked `Pool` +/// and the `dispatch` mutex's lazily-allocated `pthread_mutex_t` (macOS allocates it +/// on first lock, not construction) must live in the system allocator, not a slab the +/// next reset recycles. Running one real dispatch forces those allocations while the +/// arena is inactive. (Worker `Parker`s are allocated eagerly at spawn, also here.) pub fn init() { static INIT: Once = Once::new(); INIT.call_once(|| { let _ = pool(); - // `n_tasks > 1` and `NUM_THREADS > 1` are required to take the real dispatch - // path rather than the sequential fast path. On single-core builds the pool - // is never used, so there is nothing to warm up. if NUM_THREADS > 1 { for_each_index(NUM_THREADS, |_| {}); } @@ -126,10 +128,12 @@ fn pool() -> &'static Pool { let n = NUM_THREADS.max(1); let p: &'static Pool = Box::leak(Box::new(Pool { job: UnsafeCell::new(None), + generation: AtomicUsize::new(0), counter: AtomicUsize::new(0), + working: AtomicUsize::new(0), shutdown: AtomicBool::new(false), - start: Barrier::new(n), - end: Barrier::new(n), + parked: (0..n).map(|_| AtomicBool::new(false)).collect(), + handles: (0..n).map(|_| OnceLock::new()).collect(), dispatch: Mutex::new(()), })); for id in 1..n { @@ -144,23 +148,52 @@ fn pool() -> &'static Pool { fn worker_main(pool: &'static Pool, id: usize) { WORKER_ID.with(|c| c.set(id)); + let _ = pool.handles[id].set(std::thread::current()); + + let mut last_gen = 0usize; loop { - pool.start.wait(); - if pool.shutdown.load(Ordering::Acquire) { - break; - } + let mut spins = 0u32; + let g = loop { + let g = pool.generation.load(Ordering::Acquire); + if g != last_gen { + break g; + } + if pool.shutdown.load(Ordering::Acquire) { + return; + } + if spins < SPIN_LIMIT { + spins += 1; + std::hint::spin_loop(); + } else { + // About to park. Publish it, then re-check `generation`: by SeqCst + // total order with the dispatcher's `generation` bump and `parked` + // load, at least one side sees the other, so no wake-up is lost. + pool.parked[id].store(true, Ordering::SeqCst); + if pool.generation.load(Ordering::SeqCst) != last_gen { + pool.parked[id].store(false, Ordering::SeqCst); + } else if pool.shutdown.load(Ordering::SeqCst) { + pool.parked[id].store(false, Ordering::SeqCst); + return; + } else { + std::thread::park(); + pool.parked[id].store(false, Ordering::SeqCst); + } + spins = 0; + } + }; + last_gen = g; drain(pool); - pool.end.wait(); + pool.working.fetch_sub(1, Ordering::Release); } } /// Claim and run task indices until the counter is exhausted. fn drain(pool: &Pool) { - // SAFETY: the dispatcher published `Some(job)` before the start barrier we just - // crossed, and clears it only after the end barrier; nobody writes during drain. + // SAFETY: the dispatcher published `Some(job)` before the generation bump this + // worker just observed, and overwrites it only on the next dispatch (gated on + // `working == 0`); nobody writes during drain. let job = unsafe { (*pool.job.get()).as_ref().expect("drain without a published job") }; - // SAFETY: `job.f` points at a `&dyn Fn` borrow held live by the blocked - // dispatcher for the entire dispatch window. + // SAFETY: `job.f` points at a `&dyn Fn` borrow held live by the blocked dispatcher. let f = unsafe { job.f.as_ref() }; let n = job.n_tasks; loop { @@ -172,11 +205,8 @@ fn drain(pool: &Pool) { } } -/// Run `f(i)` for every `i` in `0..n_tasks`, in parallel across the pool. Blocks -/// until all tasks complete. The dispatching thread participates as worker 0. -/// -/// Falls back to a sequential loop for trivial sizes or single-core builds, so no -/// workers are woken for work that isn't worth the handshake. +/// Run `f(i)` for every `i` in `0..n_tasks`, in parallel across the pool. Blocks until +/// all tasks complete; the dispatching thread participates as worker 0. pub fn for_each_index(n_tasks: usize, f: F) { if NUM_THREADS <= 1 || n_tasks <= 1 { for i in 0..n_tasks { @@ -187,33 +217,46 @@ pub fn for_each_index(n_tasks: usize, f: F) { let pool = pool(); let _guard = pool.dispatch.lock().unwrap(); + let n = NUM_THREADS; let f_ref: &(dyn Fn(usize) + Sync) = &f; - // SAFETY: erase the borrow's lifetime to store it in the 'static `Job`. The - // dispatcher blocks on `end.wait()` below before returning, so `f` (and thus - // `f_ref`) outlives every worker call that dereferences this pointer. + // SAFETY: erase the borrow's lifetime to store in the 'static `Job`. The + // dispatcher spins on `working` below before returning, so `f` outlives every + // worker call that dereferences this pointer. let f_erased: NonNull = unsafe { std::mem::transmute::, NonNull>(NonNull::from(f_ref)) }; - // SAFETY: workers are parked on `start`; we hold `dispatch`, so we are the sole - // writer of `job` and `counter` here. + // SAFETY: all workers finished the previous dispatch (we waited for `working == 0`) + // and none observes this one until the generation bump, so we are the sole writer. unsafe { *pool.job.get() = Some(Job { f: f_erased, n_tasks }) }; pool.counter.store(0, Ordering::Relaxed); + pool.working.store(n - 1, Ordering::Release); + + // Publish the dispatch. SeqCst so the parked-flag protocol can't lose a wake-up. + pool.generation.fetch_add(1, Ordering::SeqCst); + + // Wake only workers that actually parked; hot (spinning) ones see the bump for free. + for id in 1..n { + if pool.parked[id].load(Ordering::SeqCst) { + if let Some(t) = pool.handles[id].get() { + t.unpark(); + } + } + } - pool.start.wait(); // release workers (publishes job) drain(pool); // dispatcher runs as worker 0 - pool.end.wait(); // wait for all workers - // SAFETY: all workers have passed `end`; none will touch `job` until the next - // dispatch republishes it. - unsafe { *pool.job.get() = None }; + // Lock-free completion: wait for every background worker to finish draining. + while pool.working.load(Ordering::Acquire) != 0 { + std::hint::spin_loop(); + } } /// Wrapper holding a raw base pointer that is safe to share across workers because -/// each worker only ever touches a disjoint sub-slice computed from its task index. +/// each worker only touches a disjoint sub-slice computed from its task index. struct SendPtr(*mut T); -// SAFETY: see `par_chunks_mut` — accesses are partitioned by task index. +// SAFETY: accesses are partitioned by task index (see callers). unsafe impl Send for SendPtr {} unsafe impl Sync for SendPtr {} @@ -224,11 +267,8 @@ impl SendPtr { } } -/// Parallel equivalent of `data.chunks_mut(chunk).enumerate().for_each(...)`. -/// -/// Splits `data` into `ceil(len / chunk)` consecutive chunks and runs +/// Parallel equivalent of `data.chunks_mut(chunk).enumerate().for_each(...)`, running /// `f(chunk_index, chunk)` on each in parallel. The final chunk may be shorter. -/// Mirrors rayon's `par_chunks_mut().enumerate()` for the prover's kernels. pub fn par_chunks_mut(data: &mut [T], chunk: usize, f: F) where F: Fn(usize, &mut [T]) + Sync, @@ -241,9 +281,8 @@ where for_each_index(n_chunks, |i| { let start = i * chunk; let this_len = chunk.min(len - start); - // SAFETY: distinct `i` produce non-overlapping `[start, start+this_len)` - // ranges within `data`, and the dispatcher keeps `data` borrowed for the - // whole call. `SendPtr` only re-exposes a pointer into that live borrow. + // SAFETY: distinct `i` produce non-overlapping in-bounds ranges, and `data` + // stays borrowed for the whole call. let slice = unsafe { std::slice::from_raw_parts_mut(base.add(start), this_len) }; f(i, slice); }); @@ -252,10 +291,9 @@ where /// Parallel map-reduce over `0..n_tasks`, equivalent to /// `(0..n_tasks).into_par_iter().map(map).reduce(identity, reduce)`. /// -/// Each worker folds the task indices it claims into a single local accumulator, so -/// only one accumulator is allocated per worker (not one per task). The per-worker -/// partials are then combined on the dispatching thread. `reduce` must be -/// associative; the combination order is otherwise unspecified. +/// Each worker folds the task indices it claims into one local accumulator (one per +/// worker, not per task); the per-worker partials are combined on the dispatcher. +/// `reduce` must be associative. pub fn map_reduce(n_tasks: usize, identity: ID, map: M, reduce: R) -> T where T: Send, @@ -271,15 +309,14 @@ where return acc; } - // One slot per worker id (0 == dispatcher). Each worker touches only its own slot. - let mut partials: Vec> = (0..NUM_THREADS).map(|_| None).collect(); - let slots = SendPtr(partials.as_mut_ptr()); + let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); + let ptr = SendPtr(slots.as_mut_ptr()); for_each_index(n_tasks, |i| { let wid = current_worker_id(); - // SAFETY: `wid` is unique per live worker and < NUM_THREADS, so slots are - // disjoint; `partials` outlives the dispatch (dispatcher blocks until done). - let slot = unsafe { &mut *slots.add(wid) }; + // SAFETY: `wid` is unique per live worker and < NUM_THREADS; slots disjoint; + // `slots` outlives the dispatch. + let slot = unsafe { &mut *ptr.add(wid) }; let v = map(i); *slot = Some(match slot.take() { Some(acc) => reduce(acc, v), @@ -287,20 +324,14 @@ where }); }); - partials.into_iter().flatten().fold(identity(), &reduce) + slots.into_iter().flatten().fold(identity(), &reduce) } /// Parallel reduce where each worker keeps reusable **scratch** alongside its -/// accumulator, so the per-task body can avoid allocating. -/// -/// Each worker creates `(scratch, acc)` once (via `init_state` / `init_acc`) the -/// first time it claims a task, then calls `fold(&mut scratch, &mut acc, i)` for -/// every task index it owns — reusing `scratch` across all of them. The per-worker -/// `acc`s are then combined on the dispatching thread, starting from `init_acc()`. -/// -/// This is the allocation-elimination counterpart to [`map_reduce`]: where that one -/// builds and reduces a fresh value per task, this one lets a worker fold many tasks -/// into one accumulator using one scratch buffer. `combine` must be associative. +/// accumulator, so the per-task body can avoid allocating. Each worker creates +/// `(scratch, acc)` once on its first task and reuses the scratch across all the +/// tasks it claims; the per-worker `acc`s are then combined. `combine` must be +/// associative. pub fn map_reduce_with_state(n_tasks: usize, init_state: IS, init_acc: IA, fold: F, combine: C) -> A where S: Send, @@ -324,8 +355,8 @@ where for_each_index(n_tasks, |i| { let wid = current_worker_id(); - // SAFETY: `wid` is unique per live worker and < NUM_THREADS, so slots are - // disjoint; `slots` outlives the dispatch (dispatcher blocks until done). + // SAFETY: `wid` unique per live worker and < NUM_THREADS; slots disjoint; + // `slots` outlives the dispatch. let slot = unsafe { &mut *ptr.add(wid) }; let (state, acc) = slot.get_or_insert_with(|| (init_state(), init_acc())); fold(state, acc, i); @@ -372,7 +403,6 @@ mod tests { let got = map_reduce(n, || 0u64, |i| i as u64, |a, b| a + b); assert_eq!(got, (0..n as u64).sum::(), "scalar sum n={n}"); } - // Vec accumulator (mirrors sumcheck's parallel_sum shape). let n = 5000; let got = map_reduce( n, @@ -391,12 +421,11 @@ mod tests { #[test] fn map_reduce_with_state_matches_sequential() { - // Scratch (reused Vec) + Vec accumulator, mirroring the sumcheck use. for n in [0usize, 1, 3, 1000, 50_000] { let got = map_reduce_with_state( n, - Vec::::new, // scratch - || vec![0u64; 2], // accumulator + Vec::::new, + || vec![0u64; 2], |scratch: &mut Vec, acc: &mut Vec, i| { scratch.clear(); scratch.push(i as u64); From bde96743260fc47d0401db8068600a41f8445c7a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 05:10:46 +0400 Subject: [PATCH 05/65] parallel: nested dispatch falls back to sequential (no deadlock) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flat pool can't dispatch from inside a task (it would deadlock on the dispatch lock), but real kernels nest parallelism — e.g. whir/dft.rs's NTT fans out over blocks, then over rows within each block. Track an in-task thread-local; a `for_each_index` issued while already running a pool task runs sequentially inline. Correct, never deadlocks; the inner level loses parallelism but the outer level has usually already saturated the cores. Prerequisite for porting the nested rayon sites during full migration. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/parallel/src/lib.rs | 55 +++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 8cf68c10b..5a3be5af8 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -33,11 +33,20 @@ //! oversubscribing at region boundaries. This pool only pays off once rayon is gone //! everywhere. Treat partial-migration benchmarks accordingly. //! -//! ## Constraints +//! ## Nesting falls back to sequential //! -//! - **No nesting.** A worker must not itself dispatch (it would deadlock). The -//! prover's parallel sections are flat, so this holds. -//! - **One dispatcher at a time.** Concurrent dispatches are serialized by a mutex. +//! Some kernels nest parallelism (e.g. the NTT fans out over blocks, then over rows +//! within a block). A flat pool can't dispatch from inside a task — that would +//! deadlock on the dispatch lock. So a `for_each_index` call made from a thread +//! already running a pool task runs **sequentially inline** instead. Correct, never +//! deadlocks; the inner level loses parallelism but the outer level has usually +//! already saturated all cores. (rayon work-steals through nesting instead; this is +//! the one place we trade a little potential utilization for a vastly simpler pool.) +//! +//! ## Constraint +//! +//! - **One dispatcher at a time.** Concurrent (non-nested) dispatches from different +//! threads are serialized by a mutex. use std::cell::{Cell, UnsafeCell}; use std::ptr::NonNull; @@ -61,6 +70,9 @@ thread_local! { /// Stable id of this thread within the pool. Set once per background worker; /// stays `0` on the dispatching thread (worker 0) and on any non-worker thread. static WORKER_ID: Cell = const { Cell::new(0) }; + /// True while this thread is executing a pool task. A `for_each_index` issued in + /// that state is a nested dispatch and runs sequentially (see module docs). + static IN_TASK: Cell = const { Cell::new(false) }; } /// Stable id of the calling worker, in `0..NUM_THREADS` (`0` off-pool). The hook for @@ -196,6 +208,9 @@ fn drain(pool: &Pool) { // SAFETY: `job.f` points at a `&dyn Fn` borrow held live by the blocked dispatcher. let f = unsafe { job.f.as_ref() }; let n = job.n_tasks; + // Mark this thread as in-task so a nested `for_each_index` runs sequentially + // rather than deadlocking on the dispatch lock. + let prev = IN_TASK.replace(true); loop { let i = pool.counter.fetch_add(1, Ordering::Relaxed); if i >= n { @@ -203,12 +218,15 @@ fn drain(pool: &Pool) { } f(i); } + IN_TASK.set(prev); } /// Run `f(i)` for every `i` in `0..n_tasks`, in parallel across the pool. Blocks until /// all tasks complete; the dispatching thread participates as worker 0. pub fn for_each_index(n_tasks: usize, f: F) { - if NUM_THREADS <= 1 || n_tasks <= 1 { + // Trivial sizes, single-core builds, and nested dispatches (called from within a + // pool task) all run sequentially — the last avoids deadlocking on the lock. + if NUM_THREADS <= 1 || n_tasks <= 1 || IN_TASK.get() { for i in 0..n_tasks { f(i); } @@ -238,10 +256,10 @@ pub fn for_each_index(n_tasks: usize, f: F) { // Wake only workers that actually parked; hot (spinning) ones see the bump for free. for id in 1..n { - if pool.parked[id].load(Ordering::SeqCst) { - if let Some(t) = pool.handles[id].get() { - t.unpark(); - } + if pool.parked[id].load(Ordering::SeqCst) + && let Some(t) = pool.handles[id].get() + { + t.unpark(); } } @@ -446,6 +464,25 @@ mod tests { } } + #[test] + fn nested_dispatch_does_not_deadlock() { + // Outer parallel loop whose body itself dispatches — must run (inner goes + // sequential) and produce correct results, not hang. + let mut data = vec![0u64; 1000]; + par_chunks_mut(&mut data, 50, |outer, chunk| { + // Nested dispatch from inside a pool task. + let sum = AtomicU64::new(0); + for_each_index(chunk.len(), |i| { + sum.fetch_add((outer * 50 + i) as u64, Ordering::Relaxed); + }); + chunk[0] = sum.load(Ordering::Relaxed); + }); + for (c, chunk) in data.chunks(50).enumerate() { + let expected: u64 = (0..50).map(|i| (c * 50 + i) as u64).sum(); + assert_eq!(chunk[0], expected, "chunk {c}"); + } + } + #[test] fn repeated_dispatch_is_stable() { for _ in 0..50 { From 9d67ff904fbfeaf7293ed5ca956b7049e05a6098 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 03:52:04 +0200 Subject: [PATCH 06/65] parallel: port field/poly/sumcheck off rayon (evals, utils, product_computation) Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 4 +- crates/backend/field/Cargo.toml | 2 +- crates/backend/field/src/field.rs | 18 +-- crates/backend/poly/Cargo.toml | 1 - crates/backend/poly/src/evals.rs | 148 +++++++++++------- crates/backend/poly/src/utils.rs | 135 +++++++--------- crates/backend/sumcheck/Cargo.toml | 1 - .../sumcheck/src/product_computation.rs | 64 +++++++- 8 files changed, 209 insertions(+), 164 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ef1d96a3..3bbb4ae5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,9 +632,9 @@ dependencies = [ "itertools", "mt-utils", "num-bigint", + "parallel", "paste", "rand", - "rayon", "serde", "tracing", ] @@ -664,7 +664,6 @@ dependencies = [ "mt-utils", "parallel", "rand", - "rayon", "serde", "system-info", ] @@ -679,7 +678,6 @@ dependencies = [ "mt-koala-bear", "mt-poly", "parallel", - "rayon", "tracing", ] diff --git a/crates/backend/field/Cargo.toml b/crates/backend/field/Cargo.toml index 89e87c133..cde41bb61 100644 --- a/crates/backend/field/Cargo.toml +++ b/crates/backend/field/Cargo.toml @@ -9,7 +9,7 @@ utils = { path = "../utils", package = "mt-utils" } itertools.workspace = true num-bigint = "*" paste = "*" +parallel.workspace = true rand.workspace = true -rayon.workspace = true serde.workspace = true tracing.workspace = true diff --git a/crates/backend/field/src/field.rs b/crates/backend/field/src/field.rs index b44ed45ed..836529cf9 100644 --- a/crates/backend/field/src/field.rs +++ b/crates/backend/field/src/field.rs @@ -9,7 +9,6 @@ use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAss use core::{array, slice}; use num_bigint::BigUint; -use rayon::{current_num_threads, prelude::*}; use serde::Serialize; use serde::de::DeserializeOwned; use utils::{flatten_to_base, iter_array_chunks_padded}; @@ -1020,7 +1019,7 @@ impl BoundedPowers { let mut points_packed = F::Packing::zero_vec(num_packed); // Split computation evenly among threads - let num_threads = current_num_threads().max(1); + let num_threads = parallel::num_threads().max(1); let chunk_size = num_packed.div_ceil(num_threads); // Precompute base for each chunk. @@ -1028,16 +1027,13 @@ impl BoundedPowers { let chunk_base = base.exp_u64((chunk_size * width) as u64); let shift = self.iter.current; - points_packed - .par_chunks_mut(chunk_size) - .enumerate() - .for_each(|(chunk_idx, chunk_slice)| { - // First power in this chunk - let chunk_start = shift * chunk_base.exp_u64(chunk_idx as u64); + parallel::par_chunks_mut(&mut points_packed, chunk_size, |chunk_idx, chunk_slice| { + // First power in this chunk + let chunk_start = shift * chunk_base.exp_u64(chunk_idx as u64); - // Fill the chunk with packed powers. - F::Packing::packed_shifted_powers(base, chunk_start).fill(chunk_slice); - }); + // Fill the chunk with packed powers. + F::Packing::packed_shifted_powers(base, chunk_start).fill(chunk_slice); + }); // return the number of requested points, discarding the unused packed powers // SAFETY: size_of:: always divides size_of::. diff --git a/crates/backend/poly/Cargo.toml b/crates/backend/poly/Cargo.toml index 9839691ac..f198a2d19 100644 --- a/crates/backend/poly/Cargo.toml +++ b/crates/backend/poly/Cargo.toml @@ -10,7 +10,6 @@ system-info.workspace = true parallel.workspace = true itertools.workspace = true -rayon.workspace = true rand.workspace = true serde.workspace = true diff --git a/crates/backend/poly/src/evals.rs b/crates/backend/poly/src/evals.rs index 7e0e07b4f..64c03f870 100644 --- a/crates/backend/poly/src/evals.rs +++ b/crates/backend/poly/src/evals.rs @@ -1,8 +1,8 @@ use crate::*; use crate::{EFPacking, PF}; +use ::utils::log2_ceil_usize; use field::{ExtensionField, Field, PrimeCharacteristicRing}; use itertools::Itertools; -use rayon::{join, prelude::*}; use std::borrow::Borrow; pub trait EvaluationsList { @@ -87,7 +87,16 @@ pub fn scale_poly>(poly: &[F], factor: EF) -> Ve if poly.len() < PARALLEL_THRESHOLD { poly.iter().map(|&e| factor * e).collect() } else { - poly.par_iter().map(|&e| factor * e).collect() + let mut out: Vec = unsafe { uninitialized_vec(poly.len()) }; + let n_chunks = (parallel::num_threads() * 4).min(poly.len()); + let chunk = poly.len().div_ceil(n_chunks); + parallel::par_chunks_mut(&mut out, chunk, |i, c| { + let start = i * chunk; + for (j, o) in c.iter_mut().enumerate() { + *o = factor * poly[start + j]; + } + }); + out } } @@ -257,20 +266,23 @@ where // // This chain of operations computes the regrouped sum: // Σ_{v_high} eq(v_high, p_high) * (Σ_{v_low} f(v_high, v_low) * eq(v_low, p_low)) - evals - .par_chunks(left.len()) - .zip_eq(right.par_iter()) - .map(|(part, &c)| { + let left_len = left.len(); + parallel::map_reduce( + right.len(), + || Res::ZERO, + |i| { + let part = &evals[i * left_len..][..left_len]; // This is the inner sum: a dot product between the evaluation chunk and the `left` basis values. mul_res_point( part.iter() .zip_eq(left.iter()) .map(|(&a, &b)| mul_coeffs_point(a, b)) .sum::(), - c, + right[i], ) - }) - .sum() + }, + |a, b| a + b, + ) } else { evals .chunks(left.len()) @@ -290,62 +302,78 @@ where } else { // For moderately sized inputs (5 to 19 variables), use the recursive strategy. // - // Split the evaluations into two halves, corresponding to the first variable being 0 or 1. - let (f0, f1) = evals.split_at(evals.len() / 2); - - // Recursively evaluate on the two smaller hypercubes. - let (f0_eval, f1_eval) = { - // Only spawn parallel tasks if the subproblem is large enough to overcome - // the overhead of threading. - let work_size: usize = (1 << 15) / std::mem::size_of::(); - if evals.len() > work_size && PARALLEL { - join( - || { - eval_multilinear_generic::<_, _, _, _, _, _, PARALLEL>( - f0, - tail, - mul_coeffs_point, - add_res_coeffs, - mul_res_point, - ) - }, - || { - eval_multilinear_generic::<_, _, _, _, _, _, PARALLEL>( - f1, - tail, - mul_coeffs_point, - add_res_coeffs, - mul_res_point, - ) - }, - ) - } else { - // For smaller subproblems, execute sequentially. - ( - eval_multilinear_generic::<_, _, _, _, _, _, false>( - f0, - tail, - mul_coeffs_point, - add_res_coeffs, - mul_res_point, - ), - eval_multilinear_generic::<_, _, _, _, _, _, false>( - f1, - tail, - mul_coeffs_point, - add_res_coeffs, - mul_res_point, - ), - ) - } - }; - // Perform the final linear interpolation for the first variable `x`. - f0_eval + mul_res_point(f1_eval - f0_eval, *x) + // Only spawn parallel tasks if the subproblem is large enough to overcome + // the overhead of threading. + let work_size: usize = (1 << 15) / std::mem::size_of::(); + if evals.len() > work_size && PARALLEL { + // Flat fan-out: peel the `n_split` leading variables into `2^n_split` + // independent subproblems, evaluate each over the remaining coordinates + // sequentially across the pool, then interpolate the partial results over + // the leading coordinates. Equivalent to the recursive `join` split, but + // flat so the in-house pool can parallelize it (nested dispatches fall + // back to sequential, so a recursive split would lose all parallelism). + let log_work = log2_ceil_usize(work_size.max(2)); + let n_split = point.len().saturating_sub(log_work).max(1); + let (lead, sub_point) = point.split_at(n_split); + let n_chunks = 1 << n_split; + let chunk = evals.len() >> n_split; + let mut partials = vec![Res::ZERO; n_chunks]; + parallel::par_chunks_mut(&mut partials, 1, |j, slot| { + slot[0] = eval_multilinear_generic::<_, _, _, _, _, _, false>( + &evals[j * chunk..][..chunk], + sub_point, + mul_coeffs_point, + add_res_coeffs, + mul_res_point, + ); + }); + interpolate_res(&partials, lead, mul_res_point) + } else { + // For smaller subproblems, execute sequentially. + let (f0, f1) = evals.split_at(evals.len() / 2); + let f0_eval = eval_multilinear_generic::<_, _, _, _, _, _, false>( + f0, + tail, + mul_coeffs_point, + add_res_coeffs, + mul_res_point, + ); + let f1_eval = eval_multilinear_generic::<_, _, _, _, _, _, false>( + f1, + tail, + mul_coeffs_point, + add_res_coeffs, + mul_res_point, + ); + // Perform the final linear interpolation for the first variable `x`. + f0_eval + mul_res_point(f1_eval - f0_eval, *x) + } } } } } +/// Multilinear interpolation of `values` (the `2^point.len()` hypercube evaluations of a +/// function, indexed lexicographically) at `point`, using only `Res` arithmetic and the +/// `mul_res_point` scaling. Used to recombine the partial results of the flat parallel +/// fan-out in [`eval_multilinear_generic`]. +fn interpolate_res(values: &[Res], point: &[Point], mul_res_point: &MRP) -> Res +where + Point: Field, + Res: Copy + PrimeCharacteristicRing, + MRP: Fn(Res, Point) -> Res, +{ + match point { + [] => values[0], + [x, tail @ ..] => { + let (low, high) = values.split_at(values.len() / 2); + let p0 = interpolate_res(low, tail, mul_res_point); + let p1 = interpolate_res(high, tail, mul_res_point); + p0 + mul_res_point(p1 - p0, *x) + } + } +} + #[cfg(test)] mod tests { use std::time::Instant; diff --git a/crates/backend/poly/src/utils.rs b/crates/backend/poly/src/utils.rs index 5bb5fb1b4..97d6366c7 100644 --- a/crates/backend/poly/src/utils.rs +++ b/crates/backend/poly/src/utils.rs @@ -1,19 +1,22 @@ use std::{ mem::ManuallyDrop, - ops::{Add, Range, Sub}, + ops::{Add, Sub}, }; use field::*; -use rayon::{ - iter::Zip, - prelude::*, - slice::{Iter, IterMut}, -}; use crate::{EFPacking, PF, PFPacking}; pub const PARALLEL_THRESHOLD: usize = 1 << 9; +/// Number of elements each pool task should handle in a flat fan-out. Aims for a few +/// chunks per worker so the pool's atomic counter can load-balance heterogeneous cores, +/// while keeping chunks coarse enough to amortize dispatch. +#[inline] +fn par_chunk_size(n_items: usize) -> usize { + n_items.div_ceil(parallel::num_threads() * 4).max(1) +} + pub fn pack_extension>>(slice: &[EF]) -> Vec> { let width = packing_width::(); let n_packed = slice.len() / width; @@ -26,9 +29,13 @@ pub fn pack_extension>>(slice: &[EF]) -> Vec>>(vec: &[EFPacking]) -> Ve write(chunk, x); } } else { - out.par_chunks_exact_mut(width) - .zip(vec.par_iter()) - .for_each(|(chunk, x)| write(chunk, x)); + // One pool task per group of `group` packed elements, each writing `group * width` + // contiguous output scalars from a disjoint slice of `vec`. + let group = par_chunk_size(vec.len()); + parallel::par_chunks_mut(&mut out, group * width, |ci, out_chunk| { + for (k, sub) in out_chunk.chunks_exact_mut(width).enumerate() { + write(sub, &vec[ci * group + k]); + } + }); } out } @@ -84,10 +96,12 @@ pub fn batch_fold_multilinears< .map(|poly| fold_multilinear(poly, alpha, &mul_if_of)) .collect() } else { - polys - .par_iter() - .map(|poly| fold_multilinear(poly, alpha, &mul_if_of)) - .collect() + // One task per poly (inner fold runs sequentially via the pool's nesting fallback). + let mut out: Vec> = (0..polys.len()).map(|_| Vec::new()).collect(); + parallel::par_chunks_mut(&mut out, 1, |i, slot| { + slot[0] = fold_multilinear(polys[i], alpha, &mul_if_of); + }); + out } } @@ -109,7 +123,13 @@ pub fn fold_multilinear_lsb< if new_size < PARALLEL_THRESHOLD { m.chunks_exact(2).zip(res.iter_mut()).for_each(compute); } else { - m.par_chunks_exact(2).zip(res.par_iter_mut()).for_each(compute); + let chunk = par_chunk_size(new_size); + parallel::par_chunks_mut(&mut res, chunk, |ci, res_chunk| { + for (k, r_v) in res_chunk.iter_mut().enumerate() { + let j = ci * chunk + k; + compute((&m[2 * j..2 * j + 2], r_v)); + } + }); } res } @@ -149,11 +169,12 @@ pub fn fold_multilinear_at_bit< *res_v = compute(new_j); } } else { - (0..new_size) - .into_par_iter() - .with_min_len(PARALLEL_THRESHOLD) - .map(compute) - .collect_into_vec(&mut res); + let chunk = par_chunk_size(new_size); + parallel::par_chunks_mut(&mut res, chunk, |ci, res_chunk| { + for (k, res_v) in res_chunk.iter_mut().enumerate() { + *res_v = compute(ci * chunk + k); + } + }); } res } @@ -176,11 +197,13 @@ pub fn fold_multilinear< res[i] = mul_if_of(m[i + new_size] - m[i], alpha) + m[i]; } } else { - (0..new_size) - .into_par_iter() - .with_min_len(PARALLEL_THRESHOLD) - .map(|i| mul_if_of(m[i + new_size] - m[i], alpha) + m[i]) - .collect_into_vec(&mut res); + let chunk = par_chunk_size(new_size); + parallel::par_chunks_mut(&mut res, chunk, |ci, res_chunk| { + for (k, res_v) in res_chunk.iter_mut().enumerate() { + let i = ci * chunk + k; + *res_v = mul_if_of(m[i + new_size] - m[i], alpha) + m[i]; + } + }); } res } @@ -203,10 +226,12 @@ pub fn batch_fold_multilinears_at_bit< .map(|poly| fold_multilinear_at_bit(poly, alpha, bit, &mul_if_of)) .collect() } else { - polys - .par_iter() - .map(|poly| fold_multilinear_at_bit(poly, alpha, bit, &mul_if_of)) - .collect() + // One task per poly (inner fold runs sequentially via the pool's nesting fallback). + let mut out: Vec> = (0..polys.len()).map(|_| Vec::new()).collect(); + parallel::par_chunks_mut(&mut out, 1, |i, slot| { + slot[0] = fold_multilinear_at_bit(polys[i], alpha, bit, &mul_if_of); + }); + out } } @@ -281,54 +306,6 @@ pub fn split_at_mut_many<'a, A>(slice: &'a mut [A], indices: &[usize]) -> Vec<&' result } -// Parallel - -#[allow(clippy::type_complexity)] -pub fn par_iter_split_4<'a, A: Sync + Send>( - u: &'a [A], -) -> Zip, Iter<'a, A>>, Zip, Iter<'a, A>>> { - let n = u.len(); - assert!(n.is_multiple_of(4)); - let [u_ll, u_lr, u_rl, u_rr] = split_at_many(u, &[n / 4, n / 2, 3 * n / 4]).try_into().ok().unwrap(); - (u_ll.par_iter().zip(u_lr)).zip(u_rl.par_iter().zip(u_rr.par_iter())) -} - -pub fn par_iter_split_2<'a, A: Sync + Send>(u: &'a [A]) -> Zip, Iter<'a, A>> { - par_iter_split_2_capped(u, 0..u.len() / 2) -} - -pub fn par_iter_split_2_capped<'a, A: Sync + Send>(u: &'a [A], range: Range) -> Zip, Iter<'a, A>> { - let n = u.len(); - assert!(n.is_multiple_of(2)); - let (u_left, u_right) = u.split_at(n / 2); - u_left[range.clone()].par_iter().zip(u_right[range.clone()].par_iter()) -} - -pub fn par_iter_mut_split_2<'a, A: Sync + Send>(u: &'a mut [A]) -> Zip, IterMut<'a, A>> { - par_iter_mut_split_2_capped(u, 0..u.len() / 2) -} - -pub fn par_iter_mut_split_2_capped<'a, A: Sync + Send>( - u: &'a mut [A], - range: Range, -) -> Zip, IterMut<'a, A>> { - let n = u.len(); - assert!(n.is_multiple_of(2)); - let (u_left, u_right) = u.split_at_mut(n / 2); - u_left[range.clone()].par_iter_mut().zip(u_right[range].par_iter_mut()) -} - -#[allow(clippy::type_complexity)] -pub fn par_zip_fold_2<'a, 'b, A: Sync + Send, B: Sync + Send>( - u: &'a [A], - folded: &'b mut [B], -) -> Zip, Iter<'a, A>>, Zip, Iter<'a, A>>>, Zip, IterMut<'b, B>>> { - let n = u.len(); - assert!(n.is_multiple_of(4)); - assert_eq!(folded.len(), n / 2); - par_iter_split_4(u).zip(par_iter_mut_split_2(folded)) -} - // Sequential pub fn iter_split_2(u: &[A]) -> impl Iterator { diff --git a/crates/backend/sumcheck/Cargo.toml b/crates/backend/sumcheck/Cargo.toml index 98316c8cd..1d5f486ca 100644 --- a/crates/backend/sumcheck/Cargo.toml +++ b/crates/backend/sumcheck/Cargo.toml @@ -10,7 +10,6 @@ poly = { path = "../poly", package = "mt-poly" } fiat-shamir = { path = "../fiat-shamir", package = "mt-fiat-shamir" } parallel.workspace = true tracing.workspace = true -rayon.workspace = true [dev-dependencies] koala-bear = { path = "../koala-bear", package = "mt-koala-bear" } diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index eaa9fc1dc..55fb52450 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -1,11 +1,26 @@ use fiat_shamir::*; use field::*; use poly::*; -use rayon::prelude::*; use tracing::instrument; use crate::{SumcheckComputation, sumcheck_prove_many_rounds}; +/// Raw mutable base pointer shareable across pool tasks; each task writes only the +/// disjoint folded slots computed from its index range. +struct SyncMutPtr(*mut T); +// SAFETY: writes are partitioned by task index (see `fold_and_compute_*`). +unsafe impl Send for SyncMutPtr {} +unsafe impl Sync for SyncMutPtr {} + +impl SyncMutPtr { + /// SAFETY: `n` must keep the result within the original allocation, and writes + /// through it must target slots no other task touches. + #[inline] + unsafe fn add(&self, n: usize) -> *mut T { + unsafe { self.0.add(n) } + } +} + #[derive(Debug)] pub struct ProductComputation; @@ -281,13 +296,46 @@ pub fn fold_and_compute_product_sumcheck_polynomial< (a0 + b0, a2 + b2) }) } else { - par_zip_fold_2(pol_0, &mut pol_0_folded) - .zip(par_zip_fold_2(pol_1, &mut pol_1_folded)) - .map(|(p0, p1)| process_element(p0, p1)) - .reduce( - || (EFPacking::ZERO, EFPacking::ZERO), - |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), - ) + // Fused single pass: fold both polynomials (writing the disjoint `i` / `quarter + i` + // slots of the output buffers) and reduce the per-index quadratic contributions. + let quarter = n / 4; + let p0f = SyncMutPtr(pol_0_folded.as_mut_ptr()); + let p1f = SyncMutPtr(pol_1_folded.as_mut_ptr()); + let chunk_size = 1024.min(quarter).max(1); + let n_chunks = quarter.div_ceil(chunk_size); + parallel::map_reduce( + n_chunks, + || (EFPacking::ZERO, EFPacking::ZERO), + |c| { + let start = c * chunk_size; + let end = (start + chunk_size).min(quarter); + let mut acc = (EFPacking::ZERO, EFPacking::ZERO); + for i in start..end { + let diff_0 = pol_0[2 * quarter + i] - pol_0[i]; + let diff_1 = pol_0[3 * quarter + i] - pol_0[quarter + i]; + let x_0 = prev_folding_factor_packed * diff_0 + pol_0[i]; + let x_1 = prev_folding_factor_packed * diff_1 + pol_0[quarter + i]; + + let y_0 = prev_folding_factor_packed * (pol_1[2 * quarter + i] - pol_1[i]) + pol_1[i]; + let y_1 = prev_folding_factor_packed * (pol_1[3 * quarter + i] - pol_1[quarter + i]) + + pol_1[quarter + i]; + + // SAFETY: distinct `i` write disjoint slots `i` and `quarter + i` in + // `[0, n/2)`; the dispatcher keeps both buffers borrowed for the call. + unsafe { + *p0f.add(i) = x_0; + *p0f.add(quarter + i) = x_1; + *p1f.add(i) = y_0; + *p1f.add(quarter + i) = y_1; + } + + let (b0, b2) = sumcheck_quadratic(((&x_0, &x_1), (&y_0, &y_1))); + acc = (acc.0 + b0, acc.1 + b2); + } + acc + }, + |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), + ) }; let c0 = decompose(c0_packed).into_iter().sum::(); From b19131a39aa667b0002e88cef862a87ba7a50727 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 03:54:26 +0200 Subject: [PATCH 07/65] parallel: port symetric merkle + fiat-shamir grinding off rayon Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 4 +-- crates/backend/fiat-shamir/Cargo.toml | 2 +- crates/backend/fiat-shamir/src/prover.rs | 31 +++++++++++++++++------- crates/backend/symetric/Cargo.toml | 2 +- crates/backend/symetric/src/merkle.rs | 25 +++++++++---------- 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bbb4ae5e..14fc8ec37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,7 +620,7 @@ dependencies = [ "mt-koala-bear", "mt-symetric", "mt-utils", - "rayon", + "parallel", "serde", "tracing", ] @@ -687,7 +687,7 @@ version = "0.1.0" dependencies = [ "mt-field", "mt-koala-bear", - "rayon", + "parallel", ] [[package]] diff --git a/crates/backend/fiat-shamir/Cargo.toml b/crates/backend/fiat-shamir/Cargo.toml index ec8649bc2..57f32d2ba 100644 --- a/crates/backend/fiat-shamir/Cargo.toml +++ b/crates/backend/fiat-shamir/Cargo.toml @@ -10,4 +10,4 @@ symetric = { path = "../symetric", package = "mt-symetric" } utils = { path = "../utils", package = "mt-utils" } tracing.workspace = true serde.workspace = true -rayon.workspace = true +parallel.workspace = true diff --git a/crates/backend/fiat-shamir/src/prover.rs b/crates/backend/fiat-shamir/src/prover.rs index 80bb6d13e..79d3859bb 100644 --- a/crates/backend/fiat-shamir/src/prover.rs +++ b/crates/backend/fiat-shamir/src/prover.rs @@ -9,8 +9,7 @@ use field::PrimeCharacteristicRing; use field::integers::QuotientMap; use field::{ExtensionField, PrimeField64}; use koala_bear::symmetric::Permutation; -use rayon::prelude::*; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::Duration; use std::{fmt::Debug, sync::Mutex, time::Instant}; @@ -132,9 +131,22 @@ where let witness_found = Mutex::>>::new(None); // each batch tests lanes witnesses simultaneously let num_batches = PF::::ORDER_U64.div_ceil(lanes as u64); - (0..num_batches) - .into_par_iter() - .find_any(|&batch| { + + // Parallel short-circuiting search (replaces rayon `find_any`): spawn one + // searcher per worker, each claiming batches from a shared counter and bailing + // as soon as any worker finds a witness. Bounds the work to ~expected + a few + // extra batches instead of enumerating all `num_batches` (which can be ~2^31). + let next_batch = AtomicU64::new(0); + let found = AtomicBool::new(false); + parallel::for_each_index(parallel::num_threads(), |_| { + loop { + if found.load(Ordering::Relaxed) { + return; + } + let batch = next_batch.fetch_add(1, Ordering::Relaxed); + if batch >= num_batches { + return; + } let base = batch * lanes as u64; let packed_witnesses = Packed::::from_fn(|lane| { @@ -159,12 +171,13 @@ where let rand_usize = sample.as_canonical_u64() as usize; if (rand_usize & ((1 << bits) - 1)) == 0 { *witness_found.lock().unwrap() = Some(*witness); - return true; + found.store(true, Ordering::Relaxed); + return; } } - false - }) - .expect("failed to find witness"); + } + }); + assert!(found.load(Ordering::Relaxed), "failed to find witness"); let witness = witness_found.lock().unwrap().unwrap(); diff --git a/crates/backend/symetric/Cargo.toml b/crates/backend/symetric/Cargo.toml index 125fb5535..86b7c3cd3 100644 --- a/crates/backend/symetric/Cargo.toml +++ b/crates/backend/symetric/Cargo.toml @@ -6,4 +6,4 @@ edition.workspace = true [dependencies] koala-bear = { path = "../koala-bear", package = "mt-koala-bear" } field = { path = "../field", package = "mt-field" } -rayon.workspace = true +parallel.workspace = true diff --git a/crates/backend/symetric/src/merkle.rs b/crates/backend/symetric/src/merkle.rs index adcf69b35..a5be3217c 100644 --- a/crates/backend/symetric/src/merkle.rs +++ b/crates/backend/symetric/src/merkle.rs @@ -4,7 +4,6 @@ use std::array; use field::PackedValue; -use rayon::prelude::*; use crate::Compression; @@ -67,18 +66,18 @@ where let default_digest = [P::Value::default(); DIGEST_ELEMS]; let mut next_digests = vec![default_digest; next_len_padded]; - next_digests[0..next_len] - .par_chunks_exact_mut(width) - .enumerate() - .for_each(|(i, digests_chunk)| { - let first_row = i * width; - let left = array::from_fn(|j| P::from_fn(|k| prev_layer[2 * (first_row + k)][j])); - let right = array::from_fn(|j| P::from_fn(|k| prev_layer[2 * (first_row + k) + 1][j])); - let packed_digest = crate::compress(comp, [left, right]); - for (dst, src) in digests_chunk.iter_mut().zip(unpack_array(packed_digest)) { - *dst = src; - } - }); + // Process only the full packed chunks in parallel (matches `par_chunks_exact_mut`); + // the `< width` remainder is handled by the sequential tail loop below. + let n_full = next_len / width * width; + parallel::par_chunks_mut(&mut next_digests[0..n_full], width, |i, digests_chunk| { + let first_row = i * width; + let left = array::from_fn(|j| P::from_fn(|k| prev_layer[2 * (first_row + k)][j])); + let right = array::from_fn(|j| P::from_fn(|k| prev_layer[2 * (first_row + k) + 1][j])); + let packed_digest = crate::compress(comp, [left, right]); + for (dst, src) in digests_chunk.iter_mut().zip(unpack_array(packed_digest)) { + *dst = src; + } + }); for i in (next_len / width * width)..next_len { let left = prev_layer[2 * i]; From dc3b81334f5cf933bd6c18dc65200e0d149d99bc Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 03:59:18 +0200 Subject: [PATCH 08/65] parallel: port whir (dft/merkle/open/utils) off rayon Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 2 +- crates/whir/Cargo.toml | 2 +- crates/whir/src/dft.rs | 148 +++++++++++++++++++------------------- crates/whir/src/merkle.rs | 31 ++++---- crates/whir/src/open.rs | 22 +++--- crates/whir/src/utils.rs | 35 +++++---- 6 files changed, 119 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14fc8ec37..15c21a3bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,8 +709,8 @@ dependencies = [ "mt-sumcheck", "mt-symetric", "mt-utils", + "parallel", "rand", - "rayon", "system-info", "tracing", "tracing-forest", diff --git a/crates/whir/Cargo.toml b/crates/whir/Cargo.toml index 1c2a2b0a7..6845c34b4 100644 --- a/crates/whir/Cargo.toml +++ b/crates/whir/Cargo.toml @@ -14,7 +14,7 @@ symetric = { path = "../backend/symetric", package = "mt-symetric" } system-info.workspace = true itertools.workspace = true -rayon.workspace = true +parallel.workspace = true rand.workspace = true tracing.workspace = true diff --git a/crates/whir/src/dft.rs b/crates/whir/src/dft.rs index 277597eb8..527f565df 100644 --- a/crates/whir/src/dft.rs +++ b/crates/whir/src/dft.rs @@ -29,7 +29,6 @@ use field::PackedValue; use field::{BasedVectorSpace, Field, PackedField, TwoAdicField}; use itertools::Itertools; -use rayon::prelude::*; use tracing::instrument; use utils::{as_base_slice, log2_strict_usize}; @@ -164,7 +163,8 @@ where /// also divide by the height. #[inline] fn par_initial_layers(mat: &mut [F], chunk_size: usize, root_table: &[Vec], width: usize) { - mat.par_chunks_exact_mut(chunk_size).for_each(|chunk| { + let n_full = mat.len() / chunk_size * chunk_size; + parallel::par_chunks_mut(&mut mat[..n_full], chunk_size, |_, chunk| { initial_layers(chunk, root_table, width); }); } @@ -197,11 +197,15 @@ fn dft_layer>(vec: &mut [F], twiddles: &[B], width: us #[inline] fn dft_layer_par>(vec: &mut [F], twiddles: &[B], width: usize) { - vec.par_chunks_exact_mut(twiddles.len() * 2 * width).for_each(|block| { + let block_size = twiddles.len() * 2 * width; + let n_full = vec.len() / block_size * block_size; + // Outer fan-out over blocks runs on the pool; the inner per-row butterflies run + // sequentially within each task (nested dispatches fall back to sequential). + parallel::par_chunks_mut(&mut vec[..n_full], block_size, |_, block| { let (left, right) = block.split_at_mut(twiddles.len() * width); - left.par_chunks_exact_mut(width) - .zip(right.par_chunks_exact_mut(width)) - .zip(twiddles.par_iter()) + left.chunks_exact_mut(width) + .zip(right.chunks_exact_mut(width)) + .zip(twiddles.iter()) .for_each(|((hi_chunk, lo_chunk), twiddle)| { twiddle.apply_to_rows(hi_chunk, lo_chunk); }); @@ -235,39 +239,29 @@ fn dft_layer_par_double, M: MultiLayerButterfly> assert_eq!(twiddles_large.len(), twiddles_small.len() * 2); // TODO optimal workload size with L1 cache - mat.values - .par_chunks_exact_mut(twiddles_large.len() * 2 * width) - .for_each(|block| { - // (0..twiddles_small.len()).into_par_iter().for_each(|ind| { - // let hi_hi = slice_ref_mut(block, ind * width, width); - // let hi_lo = slice_ref_mut(block, (ind + twiddles_small.len()) * width, width); - // let lo_hi = slice_ref_mut(block, (ind + 2 * twiddles_small.len()) * width, width); - // let lo_lo = slice_ref_mut(block, (ind + 3 * twiddles_small.len()) * width, width); - // multi_butterfly.apply_2_layers( - // ((hi_hi, hi_lo), (lo_hi, lo_lo)), - // ind, - // twiddles_small, - // twiddles_large, - // ); - // }); - let (hi_blocks, lo_blocks) = block.split_at_mut(twiddles_small.len() * width * 2); - let (hi_hi_blocks, hi_lo_blocks) = hi_blocks.split_at_mut(twiddles_small.len() * width); - let (lo_hi_blocks, lo_lo_blocks) = lo_blocks.split_at_mut(twiddles_small.len() * width); - hi_hi_blocks - .par_chunks_exact_mut(width) - .zip(hi_lo_blocks.par_chunks_exact_mut(width)) - .zip(lo_hi_blocks.par_chunks_exact_mut(width)) - .zip(lo_lo_blocks.par_chunks_exact_mut(width)) - .enumerate() - .for_each(|(ind, (((hi_hi, hi_lo), lo_hi), lo_lo))| { - multi_butterfly.apply_2_layers( - ((hi_hi, hi_lo), (lo_hi, lo_lo)), - ind, - twiddles_small, - twiddles_large, - ); - }); - }); + let block_size = twiddles_large.len() * 2 * width; + let n_full = mat.values.len() / block_size * block_size; + // Outer fan-out over blocks runs on the pool; the inner butterflies run sequentially + // within each task (nested dispatches fall back to sequential). + parallel::par_chunks_mut(&mut mat.values[..n_full], block_size, |_, block| { + let (hi_blocks, lo_blocks) = block.split_at_mut(twiddles_small.len() * width * 2); + let (hi_hi_blocks, hi_lo_blocks) = hi_blocks.split_at_mut(twiddles_small.len() * width); + let (lo_hi_blocks, lo_lo_blocks) = lo_blocks.split_at_mut(twiddles_small.len() * width); + hi_hi_blocks + .chunks_exact_mut(width) + .zip(hi_lo_blocks.chunks_exact_mut(width)) + .zip(lo_hi_blocks.chunks_exact_mut(width)) + .zip(lo_lo_blocks.chunks_exact_mut(width)) + .enumerate() + .for_each(|(ind, (((hi_hi, hi_lo), lo_hi), lo_lo))| { + multi_butterfly.apply_2_layers( + ((hi_hi, hi_lo), (lo_hi, lo_lo)), + ind, + twiddles_small, + twiddles_large, + ); + }); + }); } /// Applies three layers of a Radix-2 FFT butterfly network making use of parallelization. @@ -303,44 +297,46 @@ fn dft_layer_par_triple, M: MultiLayerButterfly> // let inner_chunk_size = // (workload_size::().next_power_of_two() / 8).min(eighth_outer_block_size); - mat.values - .par_chunks_exact_mut(twiddles_large.len() * 2 * width) - .for_each(|block| { - let (hi_blocks, lo_blocks) = block.split_at_mut(twiddles_small.len() * width * 4); - let (hi_hi_blocks, hi_lo_blocks) = hi_blocks.split_at_mut(twiddles_small.len() * width * 2); - let (lo_hi_blocks, lo_lo_blocks) = lo_blocks.split_at_mut(twiddles_small.len() * width * 2); - let (hi_hi_hi_blocks, hi_hi_lo_blocks) = hi_hi_blocks.split_at_mut(twiddles_small.len() * width); - let (hi_lo_hi_blocks, hi_lo_lo_blocks) = hi_lo_blocks.split_at_mut(twiddles_small.len() * width); - let (lo_hi_hi_blocks, lo_hi_lo_blocks) = lo_hi_blocks.split_at_mut(twiddles_small.len() * width); - let (lo_lo_hi_blocks, lo_lo_lo_blocks) = lo_lo_blocks.split_at_mut(twiddles_small.len() * width); - hi_hi_hi_blocks - .par_chunks_exact_mut(width) - .zip(hi_hi_lo_blocks.par_chunks_exact_mut(width)) - .zip(hi_lo_hi_blocks.par_chunks_exact_mut(width)) - .zip(hi_lo_lo_blocks.par_chunks_exact_mut(width)) - .zip(lo_hi_hi_blocks.par_chunks_exact_mut(width)) - .zip(lo_hi_lo_blocks.par_chunks_exact_mut(width)) - .zip(lo_lo_hi_blocks.par_chunks_exact_mut(width)) - .zip(lo_lo_lo_blocks.par_chunks_exact_mut(width)) - .enumerate() - .for_each( - |( + let block_size = twiddles_large.len() * 2 * width; + let n_full = mat.values.len() / block_size * block_size; + // Outer fan-out over blocks runs on the pool; the inner butterflies run sequentially + // within each task (nested dispatches fall back to sequential). + parallel::par_chunks_mut(&mut mat.values[..n_full], block_size, |_, block| { + let (hi_blocks, lo_blocks) = block.split_at_mut(twiddles_small.len() * width * 4); + let (hi_hi_blocks, hi_lo_blocks) = hi_blocks.split_at_mut(twiddles_small.len() * width * 2); + let (lo_hi_blocks, lo_lo_blocks) = lo_blocks.split_at_mut(twiddles_small.len() * width * 2); + let (hi_hi_hi_blocks, hi_hi_lo_blocks) = hi_hi_blocks.split_at_mut(twiddles_small.len() * width); + let (hi_lo_hi_blocks, hi_lo_lo_blocks) = hi_lo_blocks.split_at_mut(twiddles_small.len() * width); + let (lo_hi_hi_blocks, lo_hi_lo_blocks) = lo_hi_blocks.split_at_mut(twiddles_small.len() * width); + let (lo_lo_hi_blocks, lo_lo_lo_blocks) = lo_lo_blocks.split_at_mut(twiddles_small.len() * width); + hi_hi_hi_blocks + .chunks_exact_mut(width) + .zip(hi_hi_lo_blocks.chunks_exact_mut(width)) + .zip(hi_lo_hi_blocks.chunks_exact_mut(width)) + .zip(hi_lo_lo_blocks.chunks_exact_mut(width)) + .zip(lo_hi_hi_blocks.chunks_exact_mut(width)) + .zip(lo_hi_lo_blocks.chunks_exact_mut(width)) + .zip(lo_lo_hi_blocks.chunks_exact_mut(width)) + .zip(lo_lo_lo_blocks.chunks_exact_mut(width)) + .enumerate() + .for_each( + |( + ind, + (((((((hi_hi_hi, hi_hi_lo), hi_lo_hi), hi_lo_lo), lo_hi_hi), lo_hi_lo), lo_lo_hi), lo_lo_lo), + )| { + multi_butterfly.apply_3_layers( + ( + ((hi_hi_hi, hi_hi_lo), (hi_lo_hi, hi_lo_lo)), + ((lo_hi_hi, lo_hi_lo), (lo_lo_hi, lo_lo_lo)), + ), ind, - (((((((hi_hi_hi, hi_hi_lo), hi_lo_hi), hi_lo_lo), lo_hi_hi), lo_hi_lo), lo_lo_hi), lo_lo_lo), - )| { - multi_butterfly.apply_3_layers( - ( - ((hi_hi_hi, hi_hi_lo), (hi_lo_hi, hi_lo_lo)), - ((lo_hi_hi, lo_hi_lo), (lo_lo_hi, lo_lo_lo)), - ), - ind, - twiddles_small, - twiddles_med, - twiddles_large, - ); - }, - ); - }); + twiddles_small, + twiddles_med, + twiddles_large, + ); + }, + ); + }); } /// Applies the remaining layers of the Radix-2 FFT butterfly network in parallel. diff --git a/crates/whir/src/merkle.rs b/crates/whir/src/merkle.rs index 43446714a..ec6cdf0aa 100644 --- a/crates/whir/src/merkle.rs +++ b/crates/whir/src/merkle.rs @@ -12,7 +12,6 @@ use field::PrimeCharacteristicRing; use koala_bear::{KoalaBear, QuinticExtensionFieldKB, default_koalabear_poseidon1_16}; use poly::*; -use rayon::prelude::*; use symetric::Compression; use symetric::merkle::unpack_array; use tracing::instrument; @@ -228,22 +227,20 @@ where let mut digests = unsafe { uninitialized_vec(height) }; - digests - .par_chunks_exact_mut(width) - .enumerate() - .for_each(|(i, digests_chunk)| { - let first_row = i * width; - let rtl_iter = matrix.vertically_packed_row_rtl::

(first_row, effective_base_width, n_pad); - let packed_digest: [P; DIGEST_ELEMS] = - symetric::hash_rtl_iter_with_initial_state::<_, _, _, WIDTH, RATE, DIGEST_ELEMS>( - perm, - rtl_iter, - packed_initial_state, - ); - for (dst, src) in digests_chunk.iter_mut().zip(unpack_array(packed_digest)) { - *dst = src; - } - }); + // `height` is a multiple of `width`, so every chunk is exactly `width` long. + parallel::par_chunks_mut(&mut digests, width, |i, digests_chunk| { + let first_row = i * width; + let rtl_iter = matrix.vertically_packed_row_rtl::

(first_row, effective_base_width, n_pad); + let packed_digest: [P; DIGEST_ELEMS] = + symetric::hash_rtl_iter_with_initial_state::<_, _, _, WIDTH, RATE, DIGEST_ELEMS>( + perm, + rtl_iter, + packed_initial_state, + ); + for (dst, src) in digests_chunk.iter_mut().zip(unpack_array(packed_digest)) { + *dst = src; + } + }); digests } diff --git a/crates/whir/src/open.rs b/crates/whir/src/open.rs index 6636b77c7..1dee5b196 100644 --- a/crates/whir/src/open.rs +++ b/crates/whir/src/open.rs @@ -5,7 +5,6 @@ use fiat_shamir::{FSProver, MerklePath, ProofResult}; use field::PrimeCharacteristicRing; use field::{ExtensionField, Field, TwoAdicField}; use poly::*; -use rayon::prelude::*; use sumcheck::{ProductComputation, run_product_sumcheck, sumcheck_prove_many_rounds}; use tracing::{info_span, instrument}; @@ -594,17 +593,18 @@ where for (e, &scalar) in smt.values.iter().zip(&next_gamma_powers) { combined_sum += e.value * scalar; } - chunks_mut - .into_par_iter() - .zip(&indexed_smt_values) - .for_each(|(out_buff, &(origin_index, _))| { - out_buff[..1 << shift] - .par_iter_mut() - .zip(&inner_poly) - .for_each(|(out_elem, &poly_elem)| { - *out_elem += poly_elem * next_gamma_powers[origin_index]; - }); + // Few sparse statements (the outer chunks) but each inner accumulation can be + // large, so parallelize the inner loop per statement (the outer runs serial). + for (out_buff, &(origin_index, _)) in chunks_mut.iter_mut().zip(&indexed_smt_values) { + let out = &mut out_buff[..1 << shift]; + let scalar = next_gamma_powers[origin_index]; + let chunk = out.len().div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(out, chunk, |ci, sub| { + for (k, out_elem) in sub.iter_mut().enumerate() { + *out_elem += inner_poly[ci * chunk + k] * scalar; + } }); + } gamma_pow = *next_gamma_powers.last().unwrap() * gamma; } } diff --git a/crates/whir/src/utils.rs b/crates/whir/src/utils.rs index e64799149..e80b72cca 100644 --- a/crates/whir/src/utils.rs +++ b/crates/whir/src/utils.rs @@ -6,7 +6,6 @@ use field::Field; use field::PackedValue; use field::{ExtensionField, TwoAdicField}; use poly::*; -use rayon::prelude::*; use std::any::{Any, TypeId}; use std::collections::HashMap; use std::sync::{Arc, Mutex, OnceLock}; @@ -138,15 +137,18 @@ fn prepare_evals_for_fft_unpacked( let log_block_size = log2_strict_usize(block_size); let out_len = block_size * dft_n_cols; - (0..out_len) - .into_par_iter() - .map(|i| { + let mut out: Vec = unsafe { uninitialized_vec(out_len) }; + let chunk = out_len.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut out, chunk, |ci, out_chunk| { + for (k, slot) in out_chunk.iter_mut().enumerate() { + let i = ci * chunk + k; let block_index = i % dft_n_cols; let offset_in_block = i / dft_n_cols; let src_index = ((block_index << log_block_size) + offset_in_block) >> log_inv_rate; - unsafe { *evals.get_unchecked(src_index) } - }) - .collect() + *slot = unsafe { *evals.get_unchecked(src_index) }; + } + }); + out } fn prepare_evals_for_fft_packed_extension>>( @@ -163,9 +165,11 @@ fn prepare_evals_for_fft_packed_extension>>( let n_blocks_mask = n_blocks - 1; let packing_mask = (1 << log_packing) - 1; - (0..full_len) - .into_par_iter() - .map(|i| { + let mut out: Vec = unsafe { uninitialized_vec(full_len) }; + let chunk = full_len.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut out, chunk, |ci, out_chunk| { + for (k, slot) in out_chunk.iter_mut().enumerate() { + let i = ci * chunk + k; let block_index = i & n_blocks_mask; let offset_in_block = i >> folding_factor; let src_index = ((block_index << log_block_size) + offset_in_block) >> log_inv_rate; @@ -173,12 +177,13 @@ fn prepare_evals_for_fft_packed_extension>>( let offset_in_packing = src_index & packing_mask; let packed = unsafe { evals.get_unchecked(packed_src_index) }; let unpacked: &[PFPacking] = packed.as_basis_coefficients_slice(); - EF::from_basis_coefficients_fn(|i| unsafe { - let u: &PFPacking = unpacked.get_unchecked(i); + *slot = EF::from_basis_coefficients_fn(|j| unsafe { + let u: &PFPacking = unpacked.get_unchecked(j); *u.as_slice().get_unchecked(offset_in_packing) - }) - }) - .collect() + }); + } + }); + out } type CacheKey = TypeId; From f5dd79c7f7c1b246cd3bfc0edde1db6479940e2d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 04:08:18 +0200 Subject: [PATCH 09/65] parallel: port sub_protocols (logup/gkr/air_sumcheck) off rayon; re-export parallel from backend Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 1 + crates/backend/Cargo.toml | 1 + crates/backend/src/lib.rs | 1 + crates/sub_protocols/src/air_sumcheck.rs | 323 ++++++++---------- crates/sub_protocols/src/logup.rs | 65 ++-- .../sub_protocols/src/quotient_gkr/layers.rs | 82 +++-- crates/sub_protocols/src/quotient_gkr/mod.rs | 2 +- .../src/quotient_gkr/sumcheck_utils.rs | 73 ++-- 8 files changed, 290 insertions(+), 258 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15c21a3bd..7a2dfea80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,7 @@ dependencies = [ "mt-symetric", "mt-utils", "mt-whir", + "parallel", "rayon", "tracing", ] diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 3f61957af..3bf3dd328 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -9,6 +9,7 @@ poly = { path = "poly", package = "mt-poly" } sumcheck = { path = "sumcheck", package = "mt-sumcheck" } field = { path = "field", package = "mt-field" } air = { path = "air", package = "mt-air" } +parallel.workspace = true rayon.workspace = true whir = { path = "../whir", package = "mt-whir" } tracing.workspace = true diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index cbd44fb2b..2e7aac735 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -2,6 +2,7 @@ pub use air::*; pub use fiat_shamir::*; pub use field::*; pub use koala_bear::*; +pub use parallel; pub use poly::*; pub use rayon; pub use rayon::prelude::*; diff --git a/crates/sub_protocols/src/air_sumcheck.rs b/crates/sub_protocols/src/air_sumcheck.rs index 0f536d7fa..8600de649 100644 --- a/crates/sub_protocols/src/air_sumcheck.rs +++ b/crates/sub_protocols/src/air_sumcheck.rs @@ -89,22 +89,20 @@ where let _span = info_span!("chunk-bit-reversing columns").entered(); let chunk_size = 1usize << pivot; let shift = usize::BITS as usize - pivot; - let bit_reversed = cols - .par_iter() - .map(|&src| { - let mut dst: Vec> = unsafe { uninitialized_vec(src.len()) }; - let src_u = PFPacking::::unpack_slice(src); - let dst_u = PFPacking::::unpack_slice_mut(&mut dst); - for (src_chunk, dst_chunk) in - src_u.chunks_exact(chunk_size).zip(dst_u.chunks_exact_mut(chunk_size)) - { - for (p, slot) in dst_chunk.iter_mut().enumerate() { - *slot = src_chunk[p.reverse_bits() >> shift]; - } + let mut bit_reversed: Vec>> = (0..cols.len()).map(|_| Vec::new()).collect(); + parallel::par_chunks_mut(&mut bit_reversed, 1, |i, out_slot| { + let src = cols[i]; + let mut dst: Vec> = unsafe { uninitialized_vec(src.len()) }; + let src_u = PFPacking::::unpack_slice(src); + let dst_u = PFPacking::::unpack_slice_mut(&mut dst); + for (src_chunk, dst_chunk) in src_u.chunks_exact(chunk_size).zip(dst_u.chunks_exact_mut(chunk_size)) + { + for (p, slot) in dst_chunk.iter_mut().enumerate() { + *slot = src_chunk[p.reverse_bits() >> shift]; } - dst - }) - .collect(); + } + out_slot[0] = dst; + }); MleGroup::Owned(MleGroupOwned::BasePacked(bit_reversed)) } _ => unreachable!(), @@ -438,120 +436,112 @@ where let hi_zs_halved: Vec<_> = hi_zs.iter().map(|&tz| tz.halve()).collect(); let lagrange_coeffs = lagrange_basis_evals(&low_zs, &hi_zs); - let acc = (0..active_count_pairs) - .into_par_iter() - .fold( - || { - ( - vec![EFPacking::::ZERO; degree], - Vec::::with_capacity(n_cols), - Vec::::with_capacity(n_cols), - vec![EFPacking::::ZERO; n_full], - Vec::::new(), - Vec::::new(), - Vec::::new(), - ) - }, - |(mut acc, mut point, mut diff, mut low_evals, mut state_0, mut state_2, mut cached_buf), new_j| { - let i_hi = new_j >> fold_bit; - let i_lo = new_j & lo_mask; - let i0 = (i_hi << (fold_bit + 1)) | i_lo; - let i1 = i0 | stride; - let partial_eq = get_split_eq(new_j); - - // `point` holds column values at z=0; `diff[k] = col_k[i1] - col_k[i0]`. - // Invariant for the rest of this closure: `col_k(z) = point[k] + z · diff[k]`, - // so advancing z by 1 means `point[k] += diff[k]` for all k. - point.clear(); - diff.clear(); - for c in cols { - let lo = c[i0]; - let hi = c[i1]; - point.push(lo); - diff.push(hi - lo); - } + let acc = parallel::map_reduce_with_state( + active_count_pairs, + || { + ( + Vec::::with_capacity(n_cols), + Vec::::with_capacity(n_cols), + vec![EFPacking::::ZERO; n_full], + Vec::::new(), + Vec::::new(), + Vec::::new(), + ) + }, + || vec![EFPacking::::ZERO; degree], + |(point, diff, low_evals, state_0, state_2, cached_buf), acc, new_j| { + let i_hi = new_j >> fold_bit; + let i_lo = new_j & lo_mask; + let i0 = (i_hi << (fold_bit + 1)) | i_lo; + let i1 = i0 | stride; + let partial_eq = get_split_eq(new_j); + + // `point` holds column values at z=0; `diff[k] = col_k[i1] - col_k[i0]`. + // Invariant for the rest of this closure: `col_k(z) = point[k] + z · diff[k]`, + // so advancing z by 1 means `point[k] += diff[k]` for all k. + point.clear(); + diff.clear(); + for c in cols { + let lo = c[i0]; + let hi = c[i1]; + point.push(lo); + diff.push(hi - lo); + } - // Phase 1: full AIR constraints + // Phase 1: full AIR constraints - // z = 0: full eval, capture post-block state. - { - let mut folder = ConstraintFolderPacked::new(&point[..n_flat], &point[n_flat..], extra_data); - folder.cached_state = Some(state_0); - Air::eval(computation, &mut folder, extra_data); - acc[0] += folder.accumulator * partial_eq; - low_evals[0] = folder.accumulator_low; - state_0 = folder.cached_state.unwrap(); - } + // z = 0: full eval, capture post-block state. + { + let mut folder = ConstraintFolderPacked::new(&point[..n_flat], &point[n_flat..], extra_data); + folder.cached_state = Some(std::mem::take(state_0)); + Air::eval(computation, &mut folder, extra_data); + acc[0] += folder.accumulator * partial_eq; + low_evals[0] = folder.accumulator_low; + *state_0 = folder.cached_state.unwrap(); + } - // z = 2: advance `point` by 2·diff, full eval, capture post-block state. - // Together with `state_0` this pins down the linear `state(z)` (linear when we "omit" the low degree constraints of the block) + // z = 2: advance `point` by 2·diff, full eval, capture post-block state. + // Together with `state_0` this pins down the linear `state(z)` (linear when we "omit" the low degree constraints of the block) + for k in 0..n_cols { + point[k] += diff[k].double(); + } + { + let mut folder = ConstraintFolderPacked::new(&point[..n_flat], &point[n_flat..], extra_data); + folder.cached_state = Some(std::mem::take(state_2)); + Air::eval(computation, &mut folder, extra_data); + acc[1] += folder.accumulator * partial_eq; + low_evals[1] = folder.accumulator_low; + *state_2 = folder.cached_state.unwrap(); + } + + // z = 3, …, d_low+1: still doing full eval + for z_idx in 2..n_full { for k in 0..n_cols { - point[k] += diff[k].double(); - } - { - let mut folder = ConstraintFolderPacked::new(&point[..n_flat], &point[n_flat..], extra_data); - folder.cached_state = Some(state_2); - Air::eval(computation, &mut folder, extra_data); - acc[1] += folder.accumulator * partial_eq; - low_evals[1] = folder.accumulator_low; - state_2 = folder.cached_state.unwrap(); + point[k] += diff[k]; } + let mut folder = ConstraintFolderPacked::new(&point[..n_flat], &point[n_flat..], extra_data); + Air::eval(computation, &mut folder, extra_data); + acc[z_idx] += folder.accumulator * partial_eq; + low_evals[z_idx] = folder.accumulator_low; + } - // z = 3, …, d_low+1: still doing full eval - for z_idx in 2..n_full { - for k in 0..n_cols { - point[k] += diff[k]; - } - let mut folder = ConstraintFolderPacked::new(&point[..n_flat], &point[n_flat..], extra_data); - Air::eval(computation, &mut folder, extra_data); - acc[z_idx] += folder.accumulator * partial_eq; - low_evals[z_idx] = folder.accumulator_low; + // Phase 2: skip the low degree constraints of the block + // For each skipped point, assemble Constraints(z) = high(z) + low(z): + // -high(z): run folder with `skip_low = true` + // -low(z): deduce it via Lagrange-interpolation from previous computations + for t in 0..n_skip { + for k in 0..n_cols { + point[k] += diff[k]; } - // Phase 2: skip the low degree constraints of the block - // For each skipped point, assemble Constraints(z) = high(z) + low(z): - // -high(z): run folder with `skip_low = true` - // -low(z): deduce it via Lagrange-interpolation from previous computations - for t in 0..n_skip { - for k in 0..n_cols { - point[k] += diff[k]; - } - - cached_buf.clear(); - for i in 0..state_0.len() { - cached_buf - .push(state_0[i] + (state_2[i] - state_0[i]) * PFPacking::::from(hi_zs_halved[t])); - } - - let mut folder = ConstraintFolderPacked::new(&point[..n_flat], &point[n_flat..], extra_data); - folder.skip_low = true; - folder.cached_state = Some(cached_buf); - folder.low_ci_count = low_n_constraints; - Air::eval(computation, &mut folder, extra_data); - cached_buf = folder.cached_state.unwrap(); - - // low(hi_zs[t]) = Σ_i L_i(hi_zs[t]) · low(low_zs[i]) - let mut low_interpolated = EFPacking::::ZERO; - for (i, lc) in lagrange_coeffs[t].iter().enumerate() { - low_interpolated += low_evals[i] * PFPacking::::from(*lc); - } - - acc[n_full + t] += (folder.accumulator + low_interpolated) * partial_eq; + cached_buf.clear(); + for i in 0..state_0.len() { + cached_buf.push(state_0[i] + (state_2[i] - state_0[i]) * PFPacking::::from(hi_zs_halved[t])); } - (acc, point, diff, low_evals, state_0, state_2, cached_buf) - }, - ) - .map(|(acc, ..)| acc) - .reduce( - || vec![EFPacking::::ZERO; degree], - |mut a, b| { - for i in 0..degree { - a[i] += b[i]; + let mut folder = ConstraintFolderPacked::new(&point[..n_flat], &point[n_flat..], extra_data); + folder.skip_low = true; + folder.cached_state = Some(std::mem::take(cached_buf)); + folder.low_ci_count = low_n_constraints; + Air::eval(computation, &mut folder, extra_data); + *cached_buf = folder.cached_state.unwrap(); + + // low(hi_zs[t]) = Σ_i L_i(hi_zs[t]) · low(low_zs[i]) + let mut low_interpolated = EFPacking::::ZERO; + for (i, lc) in lagrange_coeffs[t].iter().enumerate() { + low_interpolated += low_evals[i] * PFPacking::::from(*lc); } - a - }, - ); + + acc[n_full + t] += (folder.accumulator + low_interpolated) * partial_eq; + } + }, + |mut a, b| { + for i in 0..degree { + a[i] += b[i]; + } + a + }, + ); acc.into_iter().map(&unpack_sum).collect() } @@ -581,54 +571,43 @@ where let stride = 1usize << fold_bit; let lo_mask = stride - 1; - let acc = (0..active_count_pairs) - .into_par_iter() - .fold( - || { - ( - vec![EFT::ZERO; degree], - Vec::::with_capacity(n_cols), - Vec::::with_capacity(n_cols), - ) - }, - |(mut acc, mut point, mut diff), new_j| { - let i_hi = new_j >> fold_bit; - let i_lo = new_j & lo_mask; - let i0 = (i_hi << (fold_bit + 1)) | i_lo; - let i1 = i0 | stride; - let partial_eq = get_split_eq(new_j); - point.clear(); - diff.clear(); - for c in cols { - let lo = c[i0]; - let hi = c[i1]; - point.push(lo); - diff.push(hi - lo); - } - // z = 0 then (skip z = 1) z = 2, 3, …, degree. - acc[0] += eval_fn(computation, &point, extra_data) * partial_eq; + let acc = parallel::map_reduce_with_state( + active_count_pairs, + || (Vec::::with_capacity(n_cols), Vec::::with_capacity(n_cols)), + || vec![EFT::ZERO; degree], + |(point, diff), acc, new_j| { + let i_hi = new_j >> fold_bit; + let i_lo = new_j & lo_mask; + let i0 = (i_hi << (fold_bit + 1)) | i_lo; + let i1 = i0 | stride; + let partial_eq = get_split_eq(new_j); + point.clear(); + diff.clear(); + for c in cols { + let lo = c[i0]; + let hi = c[i1]; + point.push(lo); + diff.push(hi - lo); + } + // z = 0 then (skip z = 1) z = 2, 3, …, degree. + acc[0] += eval_fn(computation, point, extra_data) * partial_eq; + for k in 0..n_cols { + point[k] += diff[k]; + } + for acc_z in &mut acc[1..] { for k in 0..n_cols { point[k] += diff[k]; } - for acc_z in &mut acc[1..] { - for k in 0..n_cols { - point[k] += diff[k]; - } - *acc_z += eval_fn(computation, &point, extra_data) * partial_eq; - } - (acc, point, diff) - }, - ) - .map(|(acc, _, _)| acc) - .reduce( - || vec![EFT::ZERO; degree], - |mut a, b| { - for i in 0..degree { - a[i] += b[i]; - } - a - }, - ); + *acc_z += eval_fn(computation, point, extra_data) * partial_eq; + } + }, + |mut a, b| { + for i in 0..degree { + a[i] += b[i]; + } + a + }, + ); acc.into_iter().map(unpack_sum).collect() } @@ -680,15 +659,15 @@ pub fn prove_batched_air_sumcheck<'a, EF: ExtensionField>>( pub fn compute_shifted_columns(n_shift_columns: usize, columns: &[&[F]]) -> Vec> { // Convention: the first `n_shift_columns` columns are the ones that get shifted. - columns[..n_shift_columns] - .par_iter() - .map(|column| { - let mut shifted = unsafe { uninitialized_vec(column.len()) }; - shifted[..column.len() - 1].copy_from_slice(&column[1..]); - shifted[column.len() - 1] = column[column.len() - 1]; - shifted - }) - .collect() + let mut out: Vec> = (0..n_shift_columns).map(|_| Vec::new()).collect(); + parallel::par_chunks_mut(&mut out, 1, |i, slot| { + let column = columns[i]; + let mut shifted = unsafe { uninitialized_vec(column.len()) }; + shifted[..column.len() - 1].copy_from_slice(&column[1..]); + shifted[column.len() - 1] = column[column.len() - 1]; + slot[0] = shifted; + }); + out } pub fn natural_ordering_point_for_session(sumcheck_air_point: &[EF], log_n_rows: usize) -> Vec { diff --git a/crates/sub_protocols/src/logup.rs b/crates/sub_protocols/src/logup.rs index 55af0a320..57830351d 100644 --- a/crates/sub_protocols/src/logup.rs +++ b/crates/sub_protocols/src/logup.rs @@ -72,15 +72,13 @@ pub fn prove_generic_logup( }; let fill_num_from = |dst: &mut [F], src: &[F], neg: bool| { - dst.par_chunks_exact_mut(chunk_size) - .enumerate() - .for_each(|(c, dst_chunk)| { - let src_chunk = &src[c * chunk_size..][..chunk_size]; - for (i, slot) in dst_chunk.iter_mut().enumerate() { - let v = src_chunk[i.reverse_bits() >> chunk_shift]; - *slot = if neg { -v } else { v }; - } - }); + parallel::par_chunks_mut(dst, chunk_size, |c, dst_chunk| { + let src_chunk = &src[c * chunk_size..][..chunk_size]; + for (i, slot) in dst_chunk.iter_mut().enumerate() { + let v = src_chunk[i.reverse_bits() >> chunk_shift]; + *slot = if neg { -v } else { v }; + } + }); }; let mut offset = 0; @@ -118,12 +116,14 @@ pub fn prove_generic_logup( ); if 1 << log_bytecode < max_table_height { // padding - numerators[offset + (1 << log_bytecode)..offset + max_table_height] - .par_iter_mut() - .for_each(|n| *n = F::ZERO); - denominators[(offset + (1 << log_bytecode)) / width..(offset + max_table_height) / width] - .par_iter_mut() - .for_each(|d| *d = EFPacking::::ONE); + par_fill( + &mut numerators[offset + (1 << log_bytecode)..offset + max_table_height], + |_| F::ZERO, + ); + par_fill( + &mut denominators[(offset + (1 << log_bytecode)) / width..(offset + max_table_height) / width], + |_| EFPacking::::ONE, + ); } offset += max_table_height.max(1 << log_bytecode); @@ -142,17 +142,15 @@ pub fn prove_generic_logup( let col_index = &trace.columns[group.idx_col]; let packed_chunk_size = (1 << log_n_rows) / width; - numerators[offset..][..group_len << log_n_rows] - .par_iter_mut() - .for_each(|n| *n = F::ONE); + par_fill(&mut numerators[offset..][..group_len << log_n_rows], |_| F::ONE); - denominators[offset / width..][..group_len * packed_chunk_size] - .par_chunks_exact_mut(packed_chunk_size) - .enumerate() - .for_each(|(i, denom_chunk)| { + parallel::par_chunks_mut( + &mut denominators[offset / width..][..group_len * packed_chunk_size], + packed_chunk_size, + |i, denom_chunk| { let i_field = F::from_usize(i); let col_value = &trace.columns[group.value_cols[i]]; - denom_chunk.par_iter_mut().enumerate().for_each(|(p, slot)| { + for (p, slot) in denom_chunk.iter_mut().enumerate() { *slot = c_packed - finger_print_packed::( memory_domainsep_packed, @@ -162,8 +160,9 @@ pub fn prove_generic_logup( ], &alphas_packed, ); - }); - }); + } + }, + ); offset += group_len << log_n_rows; bus_idx += group_len; next_group += 1; @@ -175,7 +174,7 @@ pub fn prove_generic_logup( match bus.multiplicity { BusMultiplicity::One => { let val = bus.direction.to_field_flag(); - slice.par_iter_mut().for_each(|n| *n = val); + par_fill(slice, |_| val); } BusMultiplicity::Column(col) => { fill_num_from(slice, &trace.columns[col], matches!(bus.direction, BusDirection::Pull)); @@ -532,5 +531,17 @@ fn fill_denoms(dst: &mut [EFPacking], build: Build) where Build: Fn(usize) -> EFPacking + Sync, { - dst.par_iter_mut().enumerate().for_each(|(p, slot)| *slot = build(p)); + par_fill(dst, build); +} + +/// Fill `dst` in parallel through the in-house pool, computing each slot from its +/// global index. Replaces the rayon `par_iter_mut().enumerate()` constant/index fills. +#[inline] +fn par_fill T + Sync>(dst: &mut [T], build: Build) { + let chunk = dst.len().div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(dst, chunk, |ci, sub| { + for (k, slot) in sub.iter_mut().enumerate() { + *slot = build(ci * chunk + k); + } + }); } diff --git a/crates/sub_protocols/src/quotient_gkr/layers.rs b/crates/sub_protocols/src/quotient_gkr/layers.rs index 0ff9e1663..0c9245b26 100644 --- a/crates/sub_protocols/src/quotient_gkr/layers.rs +++ b/crates/sub_protocols/src/quotient_gkr/layers.rs @@ -3,6 +3,23 @@ use std::borrow::Cow; use backend::*; +/// Raw mutable base pointer shareable across pool tasks; each task writes only the +/// disjoint slot computed from its index (the paired second output buffer of the +/// quotient-fold kernels, while the pool iterates the first via `par_chunks_mut`). +pub(super) struct SyncMutPtr(pub *mut T); +// SAFETY: writes are partitioned by task index (see `sum_quotients_*`). +unsafe impl Send for SyncMutPtr {} +unsafe impl Sync for SyncMutPtr {} + +impl SyncMutPtr { + /// SAFETY: `n` must stay within the original allocation and target a slot no + /// other task writes. + #[inline] + pub(super) unsafe fn add(&self, n: usize) -> *mut T { + unsafe { self.0.add(n) } + } +} + pub(super) enum LayerStorage<'a, EF: ExtensionField>> { Initial { nums: Cow<'a, [PFPacking]>, @@ -111,13 +128,12 @@ pub(super) fn bit_reverse_chunks(v: &[T], chunk_log: usiz return out; } let shift = usize::BITS as usize - chunk_log; - out.par_chunks_exact_mut(chunk_size) - .zip(v.par_chunks_exact(chunk_size)) - .for_each(|(dst, src)| { - for (p, slot) in dst.iter_mut().enumerate() { - *slot = src[p.reverse_bits() >> shift]; - } - }); + parallel::par_chunks_mut(&mut out, chunk_size, |c, dst| { + let src = &v[c * chunk_size..][..chunk_size]; + for (p, slot) in dst.iter_mut().enumerate() { + *slot = src[p.reverse_bits() >> shift]; + } + }); out } @@ -130,18 +146,22 @@ fn sum_quotients_2_by_2>>(nums: &[EF], dens: &[EF]) -> let mut new_nums: Vec = unsafe { uninitialized_vec(new_active) }; let mut new_dens: Vec = unsafe { uninitialized_vec(new_active) }; - new_nums[..full_pairs] - .par_iter_mut() - .zip(new_dens[..full_pairs].par_iter_mut()) - .enumerate() - .for_each(|(i, (num, den))| { - let n0 = nums[2 * i]; - let n1 = nums[2 * i + 1]; - let d0 = dens[2 * i]; - let d1 = dens[2 * i + 1]; - *num = d1 * n0 + d0 * n1; - *den = d0 * d1; + { + let dp = SyncMutPtr(new_dens.as_mut_ptr()); + let chunk = full_pairs.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut new_nums[..full_pairs], chunk, |ci, num_chunk| { + for (k, num) in num_chunk.iter_mut().enumerate() { + let i = ci * chunk + k; + let n0 = nums[2 * i]; + let n1 = nums[2 * i + 1]; + let d0 = dens[2 * i]; + let d1 = dens[2 * i + 1]; + *num = d1 * n0 + d0 * n1; + // SAFETY: each `i` writes a distinct slot in `new_dens`, a separate buffer. + unsafe { *dp.add(i) = d0 * d1 }; + } }); + } // Boundary (at most one pair: a/b + 0/1 = a/b). if full_pairs < new_active { @@ -172,18 +192,22 @@ where let mut new_nums: Vec> = unsafe { uninitialized_vec(nums.len() >> 1) }; let mut new_dens: Vec> = unsafe { uninitialized_vec(nums.len() >> 1) }; - new_nums - .par_iter_mut() - .zip(new_dens.par_iter_mut()) - .enumerate() - .for_each(|(new_j, (num_out, den_out))| { - let i_hi = new_j >> bit; - let i_lo = new_j & lo_mask; - let i0 = (i_hi << (bit + 1)) | i_lo; - let i1 = i0 | stride; - *num_out = dens[i1] * nums[i0] + dens[i0] * nums[i1]; - *den_out = dens[i0] * dens[i1]; + { + let dp = SyncMutPtr(new_dens.as_mut_ptr()); + let chunk = new_nums.len().div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut new_nums, chunk, |ci, num_chunk| { + for (k, num_out) in num_chunk.iter_mut().enumerate() { + let new_j = ci * chunk + k; + let i_hi = new_j >> bit; + let i_lo = new_j & lo_mask; + let i0 = (i_hi << (bit + 1)) | i_lo; + let i1 = i0 | stride; + *num_out = dens[i1] * nums[i0] + dens[i0] * nums[i1]; + // SAFETY: each `new_j` writes a distinct slot in `new_dens`, a separate buffer. + unsafe { *dp.add(new_j) = dens[i0] * dens[i1] }; + } }); + } (new_nums, new_dens) } diff --git a/crates/sub_protocols/src/quotient_gkr/mod.rs b/crates/sub_protocols/src/quotient_gkr/mod.rs index 26fa25a65..c9e9554fc 100644 --- a/crates/sub_protocols/src/quotient_gkr/mod.rs +++ b/crates/sub_protocols/src/quotient_gkr/mod.rs @@ -207,7 +207,7 @@ mod tests { type EF = QuinticExtensionFieldKB; fn sum_all_quotients(nums: &[F], den: &[EF]) -> EF { - nums.par_iter().zip(den).map(|(&n, &d)| EF::from(n) / d).sum() + nums.iter().zip(den).map(|(&n, &d)| EF::from(n) / d).sum() } fn bit_reverse_chunks_and_pack_ext>>(v: &[EF], chunk_log: usize) -> Vec> { diff --git a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs index 27afd58f7..18ec8e9c7 100644 --- a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs +++ b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs @@ -5,7 +5,7 @@ use std::{ use backend::*; -use crate::quotient_gkr::layers::unpack_and_unreverse_active; +use crate::quotient_gkr::layers::{SyncMutPtr, unpack_and_unreverse_active}; pub(super) fn even_odd_split(v: &[T]) -> (Vec, Vec) { ( @@ -328,10 +328,7 @@ pub(super) fn run_phase2_sumcheck>>( }; let acc: RoundCoeffs = if active_pairs > PARALLEL_THRESHOLD { - (0..active_pairs) - .into_par_iter() - .map(term) - .reduce(RoundCoeffs::zero, Add::add) + parallel::map_reduce(active_pairs, RoundCoeffs::zero, term, Add::add) } else { (0..active_pairs).map(term).fold(RoundCoeffs::::zero(), Add::add) }; @@ -362,7 +359,14 @@ pub(super) fn run_phase2_sumcheck>>( if new_eq_len > 0 { let fold_eq = |i: usize| eq_table[2 * i] + eq_table[2 * i + 1]; eq_table = if new_eq_len >= PARALLEL_THRESHOLD { - (0..new_eq_len).into_par_iter().map(fold_eq).collect() + let mut out: Vec = unsafe { uninitialized_vec(new_eq_len) }; + let chunk = new_eq_len.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { + for (k, slot) in sub.iter_mut().enumerate() { + *slot = fold_eq(ci * chunk + k); + } + }); + out } else { (0..new_eq_len).map(fold_eq).collect() }; @@ -392,10 +396,12 @@ fn fold_normal_with_padding>>(m: &[EF], r: EF, pad_val if new_active < PARALLEL_THRESHOLD { out.iter_mut().enumerate().for_each(compute); } else { - out.par_iter_mut() - .with_min_len(PARALLEL_THRESHOLD) - .enumerate() - .for_each(compute); + let chunk = new_active.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { + for (k, slot) in sub.iter_mut().enumerate() { + compute((ci * chunk + k, slot)); + } + }); } out } @@ -420,10 +426,13 @@ where debug_assert_eq!(dens.len(), nums.len()); debug_assert_eq!(eq_within.len(), quarter); - nums.par_chunks_exact(layer_packed) - .zip(dens.par_chunks_exact(layer_packed)) - .enumerate() - .fold(RoundCoeffs::zero, |mut acc, (c, (n_c, d_c))| { + let n_chunks = nums.len() / layer_packed; + parallel::map_reduce( + n_chunks, + RoundCoeffs::zero, + |c| { + let n_c = &nums[c * layer_packed..][..layer_packed]; + let d_c = &dens[c * layer_packed..][..layer_packed]; let eq_o: EF = eq_outer.get(c).copied().unwrap_or(EF::ONE); let mut local = RoundCoeffs::>::zero(); for inner in 0..quarter { @@ -435,10 +444,10 @@ where ); local += coeffs * eq_within[inner]; } - acc += local * eq_o; - acc - }) - .reduce(RoundCoeffs::zero, Add::add) + local * eq_o + }, + Add::add, + ) } #[allow(clippy::type_complexity)] @@ -472,13 +481,19 @@ where let mut new_dens: Vec> = unsafe { uninitialized_vec(active_out_packed) }; let prev_r_packed: EFPacking = as From>::from(prev_r); - let coeffs = nums - .par_chunks_exact(in_packed) - .zip(dens.par_chunks_exact(in_packed)) - .zip(new_nums.par_chunks_exact_mut(out_packed)) - .zip(new_dens.par_chunks_exact_mut(out_packed)) - .enumerate() - .fold(RoundCoeffs::zero, |mut acc, (c, (((n_c, d_c), nn_c), nd_c))| { + let n_chunks = nums.len() / in_packed; + let nn = SyncMutPtr(new_nums.as_mut_ptr()); + let nd = SyncMutPtr(new_dens.as_mut_ptr()); + let coeffs = parallel::map_reduce( + n_chunks, + RoundCoeffs::zero, + |c| { + let n_c = &nums[c * in_packed..][..in_packed]; + let d_c = &dens[c * in_packed..][..in_packed]; + // SAFETY: chunk `c` owns the disjoint `out_packed`-sized regions of the two + // output buffers at `c * out_packed`; no other task touches them. + let nn_c = unsafe { std::slice::from_raw_parts_mut(nn.add(c * out_packed), out_packed) }; + let nd_c = unsafe { std::slice::from_raw_parts_mut(nd.add(c * out_packed), out_packed) }; let eq_o: EF = eq_outer.get(c).copied().unwrap_or(EF::ONE); let mut local = RoundCoeffs::>::zero(); for i in 0..in_eighth { @@ -499,10 +514,10 @@ where ); local += round * eq_within[i]; } - acc += local * eq_o; - acc - }) - .reduce(RoundCoeffs::zero, Add::add); + local * eq_o + }, + Add::add, + ); (new_nums, new_dens, coeffs) } From c8610819accbf2d621972ba8cc94d2d6858eac43 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 04:16:28 +0200 Subject: [PATCH 10/65] parallel: port lean_vm/lean_prover/xmss/rec_aggregation/lean_compiler/utils off rayon Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/lean_compiler/src/c_compile_final.rs | 11 +++- crates/lean_prover/src/prove_execution.rs | 3 +- crates/lean_prover/src/trace_gen.rs | 51 +++++++++++-------- crates/lean_vm/src/execution/memory.rs | 2 +- crates/lean_vm/src/execution/runner.rs | 36 ++++++++++--- .../lean_vm/src/tables/poseidon/trace_gen.rs | 2 +- crates/rec_aggregation/src/bytecode_claims.rs | 24 +++++---- crates/utils/src/misc.rs | 20 +++++--- crates/utils/src/multilinear.rs | 14 +++-- crates/xmss/src/signers_cache.rs | 19 ++++--- crates/xmss/src/xmss.rs | 35 ++++++++----- crates/xmss/tests/xmss_tests.rs | 12 +++-- 12 files changed, 151 insertions(+), 78 deletions(-) diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index 1a3397d27..75abf3557 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -132,7 +132,16 @@ pub fn compile_to_low_level_bytecode( validate_instruction(instruction)?; } - let instructions_encoded = instructions.par_iter().map(field_representation).collect::>(); + let mut instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]> = + unsafe { uninitialized_vec(instructions.len()) }; + { + let chunk = instructions.len().div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut instructions_encoded, chunk, |ci, sub| { + for (k, out) in sub.iter_mut().enumerate() { + *out = field_representation(&instructions[ci * chunk + k]); + } + }); + } let mut instructions_multilinear = vec![]; for instr in &instructions_encoded { diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index aaf50be3b..a952b373b 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -165,8 +165,9 @@ pub fn prove_execution( }) .collect(); let _span = info_span!("Computing shifted columns for AIR sumcheck").entered(); + // Only a few tables; run them serially and let `compute_shifted_columns` use the full pool. let shifted_rows: Vec>> = ALL_TABLES - .par_iter() + .iter() .zip(&column_refs) .map(|(table, cols)| compute_shifted_columns(table.n_shift_columns(), cols)) .collect(); diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 5bee4a97a..399acec38 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -1,7 +1,7 @@ use backend::*; use lean_vm::*; use std::{array, collections::BTreeMap}; -use utils::{ToUsize, get_poseidon_16_of_zero, transposed_par_iter_mut}; +use utils::{ToUsize, get_poseidon_16_of_zero, transposed_par_for_each_mut}; #[derive(Debug)] pub struct ExecutionTrace { @@ -27,10 +27,9 @@ pub fn get_execution_trace( } } - transposed_par_iter_mut(&mut main_trace) - .zip(execution_result.pcs.par_iter()) - .zip(execution_result.fps.par_iter()) - .for_each(|((trace_row, &pc), &fp)| { + transposed_par_for_each_mut(&mut main_trace, |i, trace_row| { + let pc = execution_result.pcs[i]; + let fp = execution_result.fps[i]; let instruction = &bytecode.code[pc].instruction; let field_repr = &bytecode.instructions_multilinear[pc * N_INSTRUCTION_COLUMNS.next_power_of_two()..] [..N_INSTRUCTION_COLUMNS]; @@ -94,7 +93,15 @@ pub fn get_execution_trace( *trace_row[EXEC_COL_ADDR_C] = addr_c; }); - let mut memory_padded = memory.0.par_iter().map(|&v| v.unwrap_or(F::ZERO)).collect::>(); + let mut memory_padded: Vec = unsafe { uninitialized_vec(memory.0.len()) }; + { + let chunk = memory_padded.len().div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut memory_padded, chunk, |ci, sub| { + for (k, slot) in sub.iter_mut().enumerate() { + *slot = memory.0[ci * chunk + k].unwrap_or(F::ZERO); + } + }); + } // Write [0000000000000000 | poseidon_compress(0000000000000000)] (to make lookups work on padding-rows). let padding_zero_vec_ptr = memory_padded.len(); @@ -122,23 +129,22 @@ pub fn get_execution_trace( const N: usize = HALF_DIGEST_LEN + DIGEST_LEN; let cols: &mut [Vec; N] = (&mut right[..N]).try_into().unwrap(); - transposed_par_iter_mut(cols) - .zip(flag_short_col) - .zip(permute_col) - .zip(nu_c_col) - .for_each(|(((row, &flag_short), &permute), &nu_c)| { - if permute == F::ZERO { - let base = nu_c.to_usize(); - if flag_short == F::ONE { - for j in 0..HALF_DIGEST_LEN { - *row[j] = memory_padded[base + HALF_DIGEST_LEN + j]; - } - } - for j in 0..DIGEST_LEN { - *row[HALF_DIGEST_LEN + j] = memory_padded[base + DIGEST_LEN + j]; + transposed_par_for_each_mut(cols, |i, row| { + let flag_short = flag_short_col[i]; + let permute = permute_col[i]; + let nu_c = nu_c_col[i]; + if permute == F::ZERO { + let base = nu_c.to_usize(); + if flag_short == F::ONE { + for j in 0..HALF_DIGEST_LEN { + *row[j] = memory_padded[base + HALF_DIGEST_LEN + j]; } } - }); + for j in 0..DIGEST_LEN { + *row[HALF_DIGEST_LEN + j] = memory_padded[base + DIGEST_LEN + j]; + } + } + }); } let extension_op_trace = traces.get_mut(&Table::extension_op()).unwrap(); @@ -195,7 +201,8 @@ fn pad_table( trace.log_n_rows = log2_ceil_usize(h + 1).max(min_log_n_rows); let n_rows = 1 << trace.log_n_rows; let padding_row = table.padding_row(zero_vec_ptr, null_poseidon_16_hash_ptr, ending_pc); - trace.columns.par_iter_mut().enumerate().for_each(|(i, col)| { + parallel::par_chunks_mut(&mut trace.columns, 1, |i, slot| { + let col = &mut slot[0]; assert!(col.len() <= h); // potentially some columns have not been filled (in Poseidon -> we fill it later with SIMD + parallelism), but the first one should always be representative col.resize(n_rows, padding_row[i]); }); diff --git a/crates/lean_vm/src/execution/memory.rs b/crates/lean_vm/src/execution/memory.rs index 364eb7471..954dd9a10 100644 --- a/crates/lean_vm/src/execution/memory.rs +++ b/crates/lean_vm/src/execution/memory.rs @@ -77,7 +77,7 @@ impl MemoryAccess for Memory { impl Memory { pub fn new(public_memory: Vec) -> Self { - Self(public_memory.into_par_iter().map(Some).collect()) + Self(public_memory.into_iter().map(Some).collect()) } pub fn get(&self, index: usize) -> Result { diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index f00e04880..adb7f0288 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -333,7 +333,8 @@ fn execute_bytecode_helper( None }; let runtime_memory_size = memory.0.len() - PUBLIC_INPUT_LEN - witness.preamble_memory_len; - let used_memory_cells = memory.0.par_iter().filter(|&&x| x.is_some()).count(); + let used_memory_cells = + parallel::map_reduce(memory.0.len(), || 0usize, |i| usize::from(memory.0[i].is_some()), |a, b| a + b); let metadata = ExecutionMetadata { cycles: trace.pcs.len(), memory: memory.0.len(), @@ -432,13 +433,31 @@ fn handle_parallel_batch( let split_at = batch.batch_fp + stride; // end of iteration 0's frame let (left, right) = memory.0.split_at_mut(split_at); let shared: &[Option] = &*left; - let segment_slices: Vec<&mut [Option]> = right.chunks_mut(stride).take(n_par).collect(); + let mut segment_slices: Vec<&mut [Option]> = right.chunks_mut(stride).take(n_par).collect(); type SegResult = Result<(Trace, Vec<(usize, F)>), RunnerError>; - let results: Vec = segment_slices - .into_par_iter() - .enumerate() - .map(|(i, seg_slice)| { + + // Raw base pointer + length per disjoint segment, so the pool can run each segment + // on its own slice without moving `&mut` references through the `Fn` task closure. + struct SegPtr(*mut Option); + // SAFETY: the segments are non-overlapping `chunks_mut` of `right`; each task `i` + // reconstructs and touches only segment `i`. + unsafe impl Send for SegPtr {} + unsafe impl Sync for SegPtr {} + + let seg_info: Vec<(SegPtr, usize)> = segment_slices + .iter_mut() + .map(|s| (SegPtr(s.as_mut_ptr()), s.len())) + .collect(); + // Release the `&mut` borrows so only the raw pointers alias the segments. + drop(segment_slices); + + let mut results: Vec> = (0..n_par).map(|_| None).collect(); + parallel::par_chunks_mut(&mut results, 1, |i, out| { + let (seg_ptr, seg_len) = &seg_info[i]; + // SAFETY: distinct `i` reconstruct disjoint segments of `right`, valid for the dispatch. + let seg_slice: &mut [Option] = unsafe { std::slice::from_raw_parts_mut(seg_ptr.0, *seg_len) }; + out[0] = Some((|| -> SegResult { let seg_start = split_at + i * stride; let mut seg_mem = SegmentMemory::new(shared, seg_slice, seg_start); let fp_i = batch.batch_fp + (i + 1) * stride; @@ -478,8 +497,9 @@ fn handle_parallel_batch( } let deferred = seg_mem.into_deferred_writes(); Ok((seg_trace, deferred)) - }) - .collect(); + })()); + }); + let results: Vec = results.into_iter().map(Option::unwrap).collect(); for (idx, result) in results.into_iter().enumerate() { let (seg_trace, deferred) = result.map_err(|e| RunnerError::ParallelSegmentFailed(idx + 1, Box::new(e)))?; diff --git a/crates/lean_vm/src/tables/poseidon/trace_gen.rs b/crates/lean_vm/src/tables/poseidon/trace_gen.rs index 3664048d0..14f1a785d 100644 --- a/crates/lean_vm/src/tables/poseidon/trace_gen.rs +++ b/crates/lean_vm/src/tables/poseidon/trace_gen.rs @@ -19,7 +19,7 @@ pub fn fill_trace_poseidon_16(trace: &mut [Vec]) { let trace_packed: Vec<_> = trace.iter().map(|col| FPacking::::pack_slice(&col[..m])).collect(); // fill the packed rows - (0..m / packing_width::()).into_par_iter().for_each(|i| { + parallel::for_each_index(m / packing_width::(), |i| { let ptrs: Vec<*mut FPacking> = trace_packed .iter() .map(|col| unsafe { (col.as_ptr() as *mut FPacking).add(i) }) diff --git a/crates/rec_aggregation/src/bytecode_claims.rs b/crates/rec_aggregation/src/bytecode_claims.rs index b948dc0e8..a5eeca582 100644 --- a/crates/rec_aggregation/src/bytecode_claims.rs +++ b/crates/rec_aggregation/src/bytecode_claims.rs @@ -64,15 +64,21 @@ pub(crate) fn reduce_bytecode_claims(verified: &[InnerVerified]) -> ReducedBytec let n_claims = claims.len(); let alpha_powers: Vec = alpha.powers().take(n_claims).collect(); - let weights_packed = claims - .par_iter() - .zip(&alpha_powers) - .map(|(eval, &alpha_i)| eval_eq_packed_scaled(&eval.point.0, alpha_i)) - .reduce_with(|mut acc, eq_i| { - acc.par_iter_mut().zip(&eq_i).for_each(|(w, e)| *w += *e); - acc - }) - .unwrap(); + let weights_packed = parallel::map_reduce( + n_claims, + Vec::>::new, + |i| eval_eq_packed_scaled(&claims[i].point.0, alpha_powers[i]), + |mut acc, eq_i| { + if acc.is_empty() { + eq_i + } else { + for (w, e) in acc.iter_mut().zip(&eq_i) { + *w += *e; + } + acc + } + }, + ); let claimed_sum: EF = dot_product(claims.iter().map(|c| c.value), alpha_powers.iter().copied()); diff --git a/crates/utils/src/misc.rs b/crates/utils/src/misc.rs index ff317ae4c..09bad5162 100644 --- a/crates/utils/src/misc.rs +++ b/crates/utils/src/misc.rs @@ -7,15 +7,23 @@ pub fn from_end(slice: &[A], n: usize) -> &[A] { &slice[slice.len() - n..] } -pub fn transposed_par_iter_mut( - array: &mut [Vec; N], // all vectors must have the same length -) -> impl IndexedParallelIterator + '_ { +/// Run `g(i, row)` in parallel over `i in 0..len`, where `row` is `[&mut A; N]` holding +/// the `i`-th element of each of the `N` equal-length vectors (a transposed row). +/// Dispatched through the in-house [`parallel`] pool. +pub fn transposed_par_for_each_mut(array: &mut [Vec; N], g: G) +where + G: Fn(usize, [&mut A; N]) + Sync, +{ + // all vectors must have the same length let len = array[0].len(); let data_ptrs: [AtomicPtr; N] = array.each_mut().map(|v| AtomicPtr::new(v.as_mut_ptr())); - (0..len) - .into_par_iter() - .map(move |i| unsafe { std::array::from_fn(|j| &mut *data_ptrs[j].load(Ordering::Relaxed).add(i)) }) + parallel::for_each_index(len, |i| { + // SAFETY: distinct `i` access disjoint row `i` of each of the `N` vectors, and the + // arrays outlive the dispatch (the dispatcher blocks until all tasks complete). + let row: [&mut A; N] = unsafe { std::array::from_fn(|j| &mut *data_ptrs[j].load(Ordering::Relaxed).add(i)) }; + g(i, row); + }); } pub fn collect_refs(vecs: &[Vec]) -> Vec<&[T]> { diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 2030ca5f4..5ae33fd68 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -13,10 +13,16 @@ pub fn multilinears_linear_combination, P: Borro assert_eq!(pols.len(), scalars.len()); let n_vars = log2_strict_usize(pols[0].borrow().len()); assert!(pols.iter().all(|p| log2_strict_usize(p.borrow().len()) == n_vars)); - (0..1 << n_vars) - .into_par_iter() - .map(|i| dot_product(scalars.iter().copied(), pols.iter().map(|p| p.borrow()[i]))) - .collect::>() + let n = 1usize << n_vars; + let mut out: Vec = unsafe { uninitialized_vec(n) }; + let chunk = n.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { + for (k, slot) in sub.iter_mut().enumerate() { + let i = ci * chunk + k; + *slot = dot_product(scalars.iter().copied(), pols.iter().map(|p| p.borrow()[i])); + } + }); + out } pub fn multilinear_eval_constants_at_right(limit: usize, point: &[F]) -> F { diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 6e7a9956e..63cd67f12 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -89,18 +89,22 @@ fn gen_benchmark_signers_cache() -> Vec<(XmssPublicKey, XmssSignature)> { let completed = AtomicUsize::new(1); let time = Instant::now(); - let rest: Vec<_> = (1..NUM_BENCHMARK_SIGNERS) - .into_par_iter() - .map(|index| { + let n_rest = NUM_BENCHMARK_SIGNERS - 1; + let mut rest_opt: Vec> = (0..n_rest).map(|_| None).collect(); + let chunk = n_rest.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut rest_opt, chunk, |ci, sub| { + for (k, out) in sub.iter_mut().enumerate() { + let index = 1 + ci * chunk + k; let signer = compute_signer(index); let done = completed.fetch_add(1, Ordering::Relaxed) + 1; print!( "\rPrecomputing benchmark signatures (cached after first run): {:.0}%", 100.0 * done as f64 / NUM_BENCHMARK_SIGNERS as f64 ); - signer - }) - .collect(); + *out = Some(signer); + } + }); + let rest: Vec<_> = rest_opt.into_iter().map(Option::unwrap).collect(); println!( "\rGenerating signatures for benchmark (one-time operation): 100% - done ({:.2}s)", @@ -128,7 +132,8 @@ fn gen_benchmark_signers_cache() -> Vec<(XmssPublicKey, XmssSignature)> { #[test] fn test_signature_cache() { let signatures = get_benchmark_signatures(); - signatures.par_iter().enumerate().for_each(|(i, (pk, sig))| { + parallel::for_each_index(signatures.len(), |i| { + let (pk, sig) = &signatures[i]; xmss_verify(pk, &message_for_benchmark(), sig, BENCHMARK_SLOT) .unwrap_or_else(|_| panic!("Signature {} failed to verify", i)); }); diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index d5f69f445..fb363ee47 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -84,13 +84,18 @@ pub fn xmss_key_gen( } let public_param: PublicParam = gen_public_param(&seed); // Level 0: WOTS leaf hashes for slots in [slot_start, slot_end] - let leaves: Vec = (slot_start..=slot_end) - .into_par_iter() - .map(|slot| { - let wots = gen_wots_secret_key(&seed, slot, public_param); - wots.public_key().hash(public_param, slot) - }) - .collect(); + let n_leaves = (slot_end - slot_start + 1) as usize; + let mut leaves: Vec = unsafe { uninitialized_vec(n_leaves) }; + { + let chunk = n_leaves.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut leaves, chunk, |ci, sub| { + for (k, out) in sub.iter_mut().enumerate() { + let slot = slot_start + (ci * chunk + k) as u32; + let wots = gen_wots_secret_key(&seed, slot, public_param); + *out = wots.public_key().hash(public_param, slot); + } + }); + } let mut merkle_tree = vec![leaves]; // Build levels 1..=LOG_LIFETIME. // At level l, we store nodes with index in [(slot_start >> l), (slot_end >> l)]. @@ -102,9 +107,12 @@ pub fn xmss_key_gen( let prev_top: u64 = (slot_end as u64) >> (level - 1); let nodes: Vec = { let prev = &merkle_tree[level - 1]; - (base..=top) - .into_par_iter() - .map(|i| { + let n_nodes = (top - base + 1) as usize; + let mut nodes: Vec = unsafe { uninitialized_vec(n_nodes) }; + let chunk = n_nodes.div_ceil(parallel::num_threads() * 4).max(1); + parallel::par_chunks_mut(&mut nodes, chunk, |ci, sub| { + for (k, out) in sub.iter_mut().enumerate() { + let i = base + (ci * chunk + k) as u64; let left_idx = 2 * i; let right_idx = 2 * i + 1; let left = if left_idx >= prev_base && left_idx <= prev_top { @@ -123,9 +131,10 @@ pub fn xmss_key_gen( &left, &right, ); - poseidon16_compress(merkle_data)[..XMSS_DIGEST_LEN].try_into().unwrap() - }) - .collect() + *out = poseidon16_compress(merkle_data)[..XMSS_DIGEST_LEN].try_into().unwrap(); + } + }); + nodes }; merkle_tree.push(nodes); } diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 0fb08e01d..7abf02312 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -46,17 +46,19 @@ fn encoding_grinding_bits() { merkle_root: Default::default(), public_param: Default::default(), }; - let total_iters = (0..n) - .into_par_iter() - .map(|i| { + let total_iters = parallel::map_reduce( + n, + || 0usize, + |i| { let message: [F; MESSAGE_LEN_FE] = Default::default(); let slot = i as u32; let mut rng = StdRng::seed_from_u64(i as u64); let (_randomness, _encoding, num_iters) = find_randomness_for_wots_encoding(&message, slot, &xmss_pub_key, &mut rng); num_iters - }) - .sum::(); + }, + |a, b| a + b, + ); let grinding = ((total_iters as f64) / (n as f64)).log2(); println!("Average grinding bits: {:.1}", grinding); } From ad9431955cc0811612e91b1fc912d2ce02391792 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 04:19:13 +0200 Subject: [PATCH 11/65] =?UTF-8?q?parallel:=20teardown=20rayon=20=E2=80=94?= =?UTF-8?q?=20drop=20deps,=20flush=5Frayon=20hack,=20backend=20re-export,?= =?UTF-8?q?=20rayon=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 49 ------- Cargo.toml | 1 - crates/backend/Cargo.toml | 1 - crates/backend/koala-bear/Cargo.toml | 1 - crates/backend/src/lib.rs | 2 - .../sumcheck/src/product_computation.rs | 4 +- crates/backend/system-info/Cargo.toml | 1 - crates/backend/system-info/src/lib.rs | 33 ----- crates/backend/zk-alloc/Cargo.toml | 3 - crates/backend/zk-alloc/src/lib.rs | 4 - crates/backend/zk-alloc/tests/test_rayon.rs | 26 ---- crates/lean_compiler/src/c_compile_final.rs | 3 +- crates/lean_prover/src/trace_gen.rs | 122 +++++++++--------- crates/lean_vm/src/execution/runner.rs | 14 +- crates/whir/src/dft.rs | 7 +- 15 files changed, 75 insertions(+), 196 deletions(-) delete mode 100644 crates/backend/zk-alloc/tests/test_rayon.rs diff --git a/Cargo.lock b/Cargo.lock index 7a2dfea80..709edb3be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,7 +105,6 @@ dependencies = [ "mt-utils", "mt-whir", "parallel", - "rayon", "tracing", ] @@ -241,31 +240,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "crypto-common" version = "0.1.7" @@ -650,7 +624,6 @@ dependencies = [ "num-bigint", "paste", "rand", - "rayon", "serde", "tracing", ] @@ -918,26 +891,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "rec_aggregation" version = "0.1.0" @@ -1126,7 +1079,6 @@ name = "system-info" version = "0.1.0" dependencies = [ "libc", - "rayon", ] [[package]] @@ -1487,7 +1439,6 @@ version = "0.1.0" dependencies = [ "libc", "parallel", - "rayon", "system-info", ] diff --git a/Cargo.toml b/Cargo.toml index 047ebec81..e51ec0246 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,6 @@ parallel = { path = "crates/backend/parallel" } sha3 = "0.11.0" clap = { version = "4.5.59", features = ["derive"] } rand = "0.10.0" -rayon = "1.11.0" pest = "2.7" pest_derive = "2.7" itertools = "0.14.0" diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 3bf3dd328..d56cf56a8 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -10,7 +10,6 @@ sumcheck = { path = "sumcheck", package = "mt-sumcheck" } field = { path = "field", package = "mt-field" } air = { path = "air", package = "mt-air" } parallel.workspace = true -rayon.workspace = true whir = { path = "../whir", package = "mt-whir" } tracing.workspace = true fiat-shamir = { path = "fiat-shamir", package = "mt-fiat-shamir" } diff --git a/crates/backend/koala-bear/Cargo.toml b/crates/backend/koala-bear/Cargo.toml index aba2ab231..5ce4ad111 100644 --- a/crates/backend/koala-bear/Cargo.toml +++ b/crates/backend/koala-bear/Cargo.toml @@ -8,7 +8,6 @@ field = { path = "../field", package = "mt-field" } utils = { path = "../utils", package = "mt-utils" } rand.workspace = true -rayon.workspace = true serde.workspace = true itertools.workspace = true tracing.workspace = true diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index 2e7aac735..f4cc2d18f 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -4,8 +4,6 @@ pub use field::*; pub use koala_bear::*; pub use parallel; pub use poly::*; -pub use rayon; -pub use rayon::prelude::*; pub use sumcheck::*; pub use symetric::*; pub use utils::*; diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index 55fb52450..e6247ece8 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -317,8 +317,8 @@ pub fn fold_and_compute_product_sumcheck_polynomial< let x_1 = prev_folding_factor_packed * diff_1 + pol_0[quarter + i]; let y_0 = prev_folding_factor_packed * (pol_1[2 * quarter + i] - pol_1[i]) + pol_1[i]; - let y_1 = prev_folding_factor_packed * (pol_1[3 * quarter + i] - pol_1[quarter + i]) - + pol_1[quarter + i]; + let y_1 = + prev_folding_factor_packed * (pol_1[3 * quarter + i] - pol_1[quarter + i]) + pol_1[quarter + i]; // SAFETY: distinct `i` write disjoint slots `i` and `quarter + i` in // `[0, n/2)`; the dispatcher keeps both buffers borrowed for the call. diff --git a/crates/backend/system-info/Cargo.toml b/crates/backend/system-info/Cargo.toml index c63ee1297..862e36e89 100644 --- a/crates/backend/system-info/Cargo.toml +++ b/crates/backend/system-info/Cargo.toml @@ -5,7 +5,6 @@ edition.workspace = true [dependencies] libc = "0.2" -rayon.workspace = true [lints] workspace = true diff --git a/crates/backend/system-info/src/lib.rs b/crates/backend/system-info/src/lib.rs index 07180559b..5323c1ce4 100644 --- a/crates/backend/system-info/src/lib.rs +++ b/crates/backend/system-info/src/lib.rs @@ -9,36 +9,3 @@ pub fn peak_rss_bytes() -> u64 { // ru_maxrss unit: bytes on macOS, KiB on Linux. if cfg!(target_os = "macos") { max } else { max * 1024 } } - -/// Number of jobs [`flush_rayon`] pushes. Must exceed -/// `crossbeam_deque::deque::BLOCK_CAP` (currently 63 — -/// `crossbeam-deque-0.8.6/src/deque.rs:1191`). -const RAYON_FLUSH_JOBS: usize = 256; - -/// Drain rayon's internal queues so they release any storage allocated during the -/// previous phase. -/// -/// Rayon's global pool owns a `crossbeam_deque::Injector`, internally a linked list -/// of fixed-size blocks (`Block` and `Injector::push` — -/// `crossbeam-deque-0.8.6/src/deque.rs:1219` and `:1371`). A block is freed only -/// once its last slot has been consumed. -/// -/// `rayon::join` from a non-worker thread reaches that injector via -/// `join` (`rayon-core-1.13.0/src/join/mod.rs:132`) -> -/// `registry::in_worker` (`registry.rs:946`) -> -/// `Registry::in_worker_cold` (`:517`) -> -/// `Registry::inject` (`:428`) -> `Injector::push`. -/// -/// Under an arena allocator that recycles memory between phases (e.g. `zk-alloc`), -/// a block allocated *during* a phase points into a slab the next `begin_phase()` -/// will reuse. The next push then writes a `JobRef` straight through whatever the -/// application has placed on top, silently corrupting it. -/// -/// Pushing more than `BLOCK_CAP` jobs while the arena is off forces the Injector -/// to allocate a fresh tail block (which lands in System), and forces workers to -/// steal the last slot of every preceding block (which destroys them). -pub fn flush_rayon() { - for _ in 0..RAYON_FLUSH_JOBS { - rayon::join(|| {}, || {}); - } -} diff --git a/crates/backend/zk-alloc/Cargo.toml b/crates/backend/zk-alloc/Cargo.toml index 8f783af1e..3c02742c9 100644 --- a/crates/backend/zk-alloc/Cargo.toml +++ b/crates/backend/zk-alloc/Cargo.toml @@ -8,9 +8,6 @@ description = "Bump+reset arena allocator for ZK proving workloads" system-info.workspace = true parallel.workspace = true -[dev-dependencies] -rayon.workspace = true - [target.'cfg(not(all(target_os = "linux", target_arch = "x86_64")))'.dependencies] libc = "0.2" diff --git a/crates/backend/zk-alloc/src/lib.rs b/crates/backend/zk-alloc/src/lib.rs index bbdef75ff..1cda9bb9a 100644 --- a/crates/backend/zk-alloc/src/lib.rs +++ b/crates/backend/zk-alloc/src/lib.rs @@ -117,12 +117,8 @@ pub fn begin_phase() { /// Deactivates the arena. New allocations go to the system allocator; existing arena /// pointers stay valid until the next `begin_phase()` resets the slabs. -/// -/// Also calls [`system_info::flush_rayon`] to release any rayon/crossbeam storage -/// still referencing this phase's arena memory. pub fn end_phase() { ARENA_ACTIVE.store(false, Ordering::Release); - system_info::flush_rayon(); } #[cold] diff --git a/crates/backend/zk-alloc/tests/test_rayon.rs b/crates/backend/zk-alloc/tests/test_rayon.rs deleted file mode 100644 index ae084af21..000000000 --- a/crates/backend/zk-alloc/tests/test_rayon.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Regression test for the bug prevented by `system_info::flush_rayon`. - -use rayon::prelude::*; - -#[global_allocator] -static A: zk_alloc::ZkAllocator = zk_alloc::ZkAllocator; - -#[test] -fn rayon_does_not_corrupt_zkalloc() { - zk_alloc::init(); - let _: u64 = (0..1_000_000_u64).into_par_iter().sum(); - - zk_alloc::begin_phase(); - for _ in 0..200 { - rayon::join(|| {}, || {}); - } - zk_alloc::end_phase(); - - zk_alloc::begin_phase(); - let canary = vec![0xAB_u8; 8192]; - rayon::join(|| {}, || {}); - zk_alloc::end_phase(); - - let pos = canary.iter().position(|&b| b != 0xAB); - assert!(pos.is_none(), "canary corrupted at offset {}", pos.unwrap()); -} diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index 75abf3557..a09519dd4 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -132,8 +132,7 @@ pub fn compile_to_low_level_bytecode( validate_instruction(instruction)?; } - let mut instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]> = - unsafe { uninitialized_vec(instructions.len()) }; + let mut instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]> = unsafe { uninitialized_vec(instructions.len()) }; { let chunk = instructions.len().div_ceil(parallel::num_threads() * 4).max(1); parallel::par_chunks_mut(&mut instructions_encoded, chunk, |ci, sub| { diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 399acec38..17b882f99 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -28,70 +28,70 @@ pub fn get_execution_trace( } transposed_par_for_each_mut(&mut main_trace, |i, trace_row| { - let pc = execution_result.pcs[i]; - let fp = execution_result.fps[i]; - let instruction = &bytecode.code[pc].instruction; - let field_repr = &bytecode.instructions_multilinear[pc * N_INSTRUCTION_COLUMNS.next_power_of_two()..] - [..N_INSTRUCTION_COLUMNS]; - - let flag_a = field_repr[instr_idx(EXEC_COL_FLAG_A)]; - let flag_b = field_repr[instr_idx(EXEC_COL_FLAG_B)]; - let flag_c = field_repr[instr_idx(EXEC_COL_FLAG_C)]; - let flag_c_fp = field_repr[instr_idx(EXEC_COL_FLAG_C_FP)]; - let flag_ab_fp = field_repr[instr_idx(EXEC_COL_FLAG_AB_FP)]; - let aux_1 = field_repr[instr_idx(EXEC_COL_AUX_1)]; - let is_deref = aux_1 == F::TWO; - - let mut addr_a = F::ZERO; - if flag_a.is_zero() && flag_ab_fp.is_zero() { - addr_a = F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_A)]; - } - let value_a = memory.0.get(addr_a.to_usize()).copied().flatten().unwrap_or_default(); - - let mut addr_b = F::ZERO; - if flag_b.is_zero() && flag_ab_fp.is_zero() { - addr_b = F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_B)]; - } else if is_deref { - // DEREF: addr_B = value_A + operand_B - addr_b = value_a + field_repr[instr_idx(EXEC_COL_OPERAND_B)]; - } - let value_b = memory.0.get(addr_b.to_usize()).copied().flatten().unwrap_or_default(); + let pc = execution_result.pcs[i]; + let fp = execution_result.fps[i]; + let instruction = &bytecode.code[pc].instruction; + let field_repr = &bytecode.instructions_multilinear[pc * N_INSTRUCTION_COLUMNS.next_power_of_two()..] + [..N_INSTRUCTION_COLUMNS]; + + let flag_a = field_repr[instr_idx(EXEC_COL_FLAG_A)]; + let flag_b = field_repr[instr_idx(EXEC_COL_FLAG_B)]; + let flag_c = field_repr[instr_idx(EXEC_COL_FLAG_C)]; + let flag_c_fp = field_repr[instr_idx(EXEC_COL_FLAG_C_FP)]; + let flag_ab_fp = field_repr[instr_idx(EXEC_COL_FLAG_AB_FP)]; + let aux_1 = field_repr[instr_idx(EXEC_COL_AUX_1)]; + let is_deref = aux_1 == F::TWO; + + let mut addr_a = F::ZERO; + if flag_a.is_zero() && flag_ab_fp.is_zero() { + addr_a = F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_A)]; + } + let value_a = memory.0.get(addr_a.to_usize()).copied().flatten().unwrap_or_default(); + + let mut addr_b = F::ZERO; + if flag_b.is_zero() && flag_ab_fp.is_zero() { + addr_b = F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_B)]; + } else if is_deref { + // DEREF: addr_B = value_A + operand_B + addr_b = value_a + field_repr[instr_idx(EXEC_COL_OPERAND_B)]; + } + let value_b = memory.0.get(addr_b.to_usize()).copied().flatten().unwrap_or_default(); - let mut addr_c = F::ZERO; - if flag_c.is_zero() && flag_c_fp.is_zero() { - addr_c = F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_C)]; - } - let value_c = memory.0.get(addr_c.to_usize()).copied().flatten().unwrap_or_default(); + let mut addr_c = F::ZERO; + if flag_c.is_zero() && flag_c_fp.is_zero() { + addr_c = F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_C)]; + } + let value_c = memory.0.get(addr_c.to_usize()).copied().flatten().unwrap_or_default(); - for (j, field) in field_repr.iter().enumerate() { - *trace_row[j + N_RUNTIME_COLUMNS] = *field; - } + for (j, field) in field_repr.iter().enumerate() { + *trace_row[j + N_RUNTIME_COLUMNS] = *field; + } - let nu_a = flag_a * field_repr[instr_idx(EXEC_COL_OPERAND_A)] - + (F::ONE - flag_a - flag_ab_fp) * value_a - + flag_ab_fp * (F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_A)]); - let nu_b = flag_b * field_repr[instr_idx(EXEC_COL_OPERAND_B)] - + (F::ONE - flag_b - flag_ab_fp) * value_b - + flag_ab_fp * (F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_B)]); - let nu_c = flag_c * field_repr[instr_idx(EXEC_COL_OPERAND_C)] - + (F::ONE - flag_c - flag_c_fp) * value_c - + flag_c_fp * (F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_C)]); - if let Instruction::Precompile(..) = instruction { - *trace_row[EXEC_COL_FLAG_PRECOMPILE] = F::ONE; - } - *trace_row[EXEC_COL_NU_A] = nu_a; - *trace_row[EXEC_COL_NU_B] = nu_b; - *trace_row[EXEC_COL_NU_C] = nu_c; - - *trace_row[EXEC_COL_VALUE_A] = value_a; - *trace_row[EXEC_COL_VALUE_B] = value_b; - *trace_row[EXEC_COL_VALUE_C] = value_c; - *trace_row[EXEC_COL_PC] = F::from_usize(pc); - *trace_row[EXEC_COL_FP] = F::from_usize(fp); - *trace_row[EXEC_COL_ADDR_A] = addr_a; - *trace_row[EXEC_COL_ADDR_B] = addr_b; - *trace_row[EXEC_COL_ADDR_C] = addr_c; - }); + let nu_a = flag_a * field_repr[instr_idx(EXEC_COL_OPERAND_A)] + + (F::ONE - flag_a - flag_ab_fp) * value_a + + flag_ab_fp * (F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_A)]); + let nu_b = flag_b * field_repr[instr_idx(EXEC_COL_OPERAND_B)] + + (F::ONE - flag_b - flag_ab_fp) * value_b + + flag_ab_fp * (F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_B)]); + let nu_c = flag_c * field_repr[instr_idx(EXEC_COL_OPERAND_C)] + + (F::ONE - flag_c - flag_c_fp) * value_c + + flag_c_fp * (F::from_usize(fp) + field_repr[instr_idx(EXEC_COL_OPERAND_C)]); + if let Instruction::Precompile(..) = instruction { + *trace_row[EXEC_COL_FLAG_PRECOMPILE] = F::ONE; + } + *trace_row[EXEC_COL_NU_A] = nu_a; + *trace_row[EXEC_COL_NU_B] = nu_b; + *trace_row[EXEC_COL_NU_C] = nu_c; + + *trace_row[EXEC_COL_VALUE_A] = value_a; + *trace_row[EXEC_COL_VALUE_B] = value_b; + *trace_row[EXEC_COL_VALUE_C] = value_c; + *trace_row[EXEC_COL_PC] = F::from_usize(pc); + *trace_row[EXEC_COL_FP] = F::from_usize(fp); + *trace_row[EXEC_COL_ADDR_A] = addr_a; + *trace_row[EXEC_COL_ADDR_B] = addr_b; + *trace_row[EXEC_COL_ADDR_C] = addr_c; + }); let mut memory_padded: Vec = unsafe { uninitialized_vec(memory.0.len()) }; { diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index adb7f0288..2935f585f 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -333,8 +333,12 @@ fn execute_bytecode_helper( None }; let runtime_memory_size = memory.0.len() - PUBLIC_INPUT_LEN - witness.preamble_memory_len; - let used_memory_cells = - parallel::map_reduce(memory.0.len(), || 0usize, |i| usize::from(memory.0[i].is_some()), |a, b| a + b); + let used_memory_cells = parallel::map_reduce( + memory.0.len(), + || 0usize, + |i| usize::from(memory.0[i].is_some()), + |a, b| a + b, + ); let metadata = ExecutionMetadata { cycles: trace.pcs.len(), memory: memory.0.len(), @@ -471,8 +475,10 @@ fn handle_parallel_batch( cursor.index += i * delta; } } - let seg_start_indices: HashMap<_, _> = - seg_named_hints.iter().map(|(name, c)| (name.clone(), c.index)).collect(); + let seg_start_indices: HashMap<_, _> = seg_named_hints + .iter() + .map(|(name, c)| (name.clone(), c.index)) + .collect(); let mut hints = HintState { diagnostics: None, named_hints: &mut seg_named_hints, diff --git a/crates/whir/src/dft.rs b/crates/whir/src/dft.rs index 527f565df..6dd29b1b1 100644 --- a/crates/whir/src/dft.rs +++ b/crates/whir/src/dft.rs @@ -254,12 +254,7 @@ fn dft_layer_par_double, M: MultiLayerButterfly> .zip(lo_lo_blocks.chunks_exact_mut(width)) .enumerate() .for_each(|(ind, (((hi_hi, hi_lo), lo_hi), lo_lo))| { - multi_butterfly.apply_2_layers( - ((hi_hi, hi_lo), (lo_hi, lo_lo)), - ind, - twiddles_small, - twiddles_large, - ); + multi_butterfly.apply_2_layers(((hi_hi, hi_lo), (lo_hi, lo_lo)), ind, twiddles_small, twiddles_large); }); }); } From 60eb3c2fe1129b98741e051462bc1cbdd4e49c99 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 10:59:00 +0200 Subject: [PATCH 12/65] parallel: guided self-scheduling + flatten dft butterflies + chunk product sumcheck - pool drain: guided self-scheduling (batch claims proportional to remaining work), so million-task kernels don't hammer the shared counter. - dft double/triple/single-layer kernels: flatten (block, inner-group) into one parallel loop so coarse layers parallelize over inner groups instead of going serial (dft_algebra_batch 73ms -> 57ms, matching main). - compute_product_sumcheck_polynomial: chunk the reduction to amortize per-task overhead. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/parallel/src/lib.rs | 31 +++- .../sumcheck/src/product_computation.rs | 20 ++- crates/whir/src/dft.rs | 152 ++++++++++-------- 3 files changed, 129 insertions(+), 74 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 5a3be5af8..3dcea71b6 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -60,6 +60,11 @@ use system_info::NUM_THREADS; /// keep workers hot (no syscalls) while a long idle stretch still lets them sleep. const SPIN_LIMIT: u32 = 1 << 16; +/// Upper bound on a single guided-self-scheduling claim (see [`drain`]). Caps the +/// worst-case load imbalance from one worker grabbing too large an initial batch, while +/// staying large enough that million-task kernels need only a few thousand claims. +const MAX_CLAIM_BATCH: usize = 1 << 12; + /// Total worker count (including the dispatching thread). Equal to build-time `NUM_THREADS`. #[must_use] pub const fn num_threads() -> usize { @@ -200,6 +205,15 @@ fn worker_main(pool: &'static Pool, id: usize) { } /// Claim and run task indices until the counter is exhausted. +/// +/// Uses **guided self-scheduling**: each claim grabs a batch proportional to the work +/// still remaining (`remaining / (NUM_THREADS * 2)`), capped at [`MAX_CLAIM_BATCH`]. +/// This is what rayon's recursive splitting buys implicitly — without it, a kernel that +/// emits one task per element (e.g. a merkle layer with one packed compression per task, +/// millions of tasks) would hammer the shared counter with millions of `fetch_add`s. +/// Large early batches cut that contention to a handful of claims; the proportional +/// shrink toward the end keeps the tail finely divided for load balance, and small +/// dispatches naturally fall back to single-task claims (`max(1)`). fn drain(pool: &Pool) { // SAFETY: the dispatcher published `Some(job)` before the generation bump this // worker just observed, and overwrites it only on the next dispatch (gated on @@ -212,11 +226,22 @@ fn drain(pool: &Pool) { // rather than deadlocking on the dispatch lock. let prev = IN_TASK.replace(true); loop { - let i = pool.counter.fetch_add(1, Ordering::Relaxed); - if i >= n { + // Batch size is computed from a (possibly stale) counter read; this only affects + // granularity, never correctness — `fetch_add` chains claims into a contiguous, + // non-overlapping tiling of `0..n`, and out-of-range tails are clamped/skipped. + let observed = pool.counter.load(Ordering::Relaxed); + if observed >= n { + break; + } + let batch = ((n - observed) / (NUM_THREADS * 2)).clamp(1, MAX_CLAIM_BATCH); + let start = pool.counter.fetch_add(batch, Ordering::Relaxed); + if start >= n { break; } - f(i); + let end = (start + batch).min(n); + for i in start..end { + f(i); + } } IN_TASK.set(prev); } diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index e6247ece8..25eeb8f6c 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -161,11 +161,27 @@ pub fn compute_product_sumcheck_polynomial< (a0 + b0, a2 + b2) }) } else { + // Chunk the reduction so each pool task folds a contiguous range into a local + // accumulator — amortizing the per-task `current_worker_id()`/slot overhead that + // a one-element-per-task `map_reduce` would pay millions of times. let half = n / 2; + let chunk_size = 1024; + let n_chunks = half.div_ceil(chunk_size); parallel::map_reduce( - half, + n_chunks, || (EFPacking::ZERO, EFPacking::ZERO), - |i| sumcheck_quadratic(((&pol_0[i], &pol_0[half + i]), (&pol_1[i], &pol_1[half + i]))), + |c| { + let start = c * chunk_size; + let end = (start + chunk_size).min(half); + let mut a0 = EFPacking::ZERO; + let mut a2 = EFPacking::ZERO; + for i in start..end { + let (b0, b2) = sumcheck_quadratic(((&pol_0[i], &pol_0[half + i]), (&pol_1[i], &pol_1[half + i]))); + a0 = a0 + b0; + a2 = a2 + b2; + } + (a0, a2) + }, |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), ) }; diff --git a/crates/whir/src/dft.rs b/crates/whir/src/dft.rs index 6dd29b1b1..d0e6cde4b 100644 --- a/crates/whir/src/dft.rs +++ b/crates/whir/src/dft.rs @@ -37,6 +37,26 @@ use crate::{Matrix, RowMajorMatrix, RowMajorMatrixViewMut}; /// The number of layers to compute in each parallelization. const LAYERS_PER_GROUP: usize = 3; +/// Raw mutable base pointer shareable across pool tasks. The multi-layer butterfly +/// kernels flatten their (block, inner-group) iteration space into one parallel loop and +/// reconstruct each group's disjoint `width`-row sub-slices from this base — so coarse +/// layers (few blocks) still parallelize over their inner groups instead of going serial. +#[derive(Clone, Copy)] +struct ButterflyPtr(*mut F); +// SAFETY: each task touches only the disjoint rows computed from its group index. +unsafe impl Send for ButterflyPtr {} +unsafe impl Sync for ButterflyPtr {} + +impl ButterflyPtr { + /// Reconstruct the `width`-long row starting at element offset `off`. + /// SAFETY: `off`/`width` must stay in-bounds and the row must be disjoint from every + /// other row any concurrent task reconstructs. + #[inline] + unsafe fn row<'a>(self, off: usize, width: usize) -> &'a mut [F] { + unsafe { std::slice::from_raw_parts_mut(self.0.add(off), width) } + } +} + #[derive(Default, Debug)] pub(crate) struct EvalsDft { twiddles: RwLock>>, @@ -197,18 +217,20 @@ fn dft_layer>(vec: &mut [F], twiddles: &[B], width: us #[inline] fn dft_layer_par>(vec: &mut [F], twiddles: &[B], width: usize) { - let block_size = twiddles.len() * 2 * width; - let n_full = vec.len() / block_size * block_size; - // Outer fan-out over blocks runs on the pool; the inner per-row butterflies run - // sequentially within each task (nested dispatches fall back to sequential). - parallel::par_chunks_mut(&mut vec[..n_full], block_size, |_, block| { - let (left, right) = block.split_at_mut(twiddles.len() * width); - left.chunks_exact_mut(width) - .zip(right.chunks_exact_mut(width)) - .zip(twiddles.iter()) - .for_each(|((hi_chunk, lo_chunk), twiddle)| { - twiddle.apply_to_rows(hi_chunk, lo_chunk); - }); + let ts = twiddles.len(); + let block_size = 2 * ts * width; + let n_blocks = vec.len() / block_size; + // Flatten (block, group) into one parallel loop over `n_blocks * ts` groups so coarse + // layers (few blocks) still parallelize; guided scheduling keeps a worker's batch of + // consecutive groups within the same block, preserving the per-block cache locality. + let base = ButterflyPtr(vec.as_mut_ptr()); + parallel::for_each_index(n_blocks * ts, |g| { + let block_base = (g / ts) * block_size; + let ind = g % ts; + // SAFETY: distinct `g` map to disjoint (hi, lo) `width`-rows. + let hi = unsafe { base.row(block_base + ind * width, width) }; + let lo = unsafe { base.row(block_base + (ts + ind) * width, width) }; + twiddles[ind].apply_to_rows(hi, lo); }); } @@ -238,24 +260,24 @@ fn dft_layer_par_double, M: MultiLayerButterfly> assert_eq!(twiddles_large.len(), twiddles_small.len() * 2); - // TODO optimal workload size with L1 cache - let block_size = twiddles_large.len() * 2 * width; - let n_full = mat.values.len() / block_size * block_size; - // Outer fan-out over blocks runs on the pool; the inner butterflies run sequentially - // within each task (nested dispatches fall back to sequential). - parallel::par_chunks_mut(&mut mat.values[..n_full], block_size, |_, block| { - let (hi_blocks, lo_blocks) = block.split_at_mut(twiddles_small.len() * width * 2); - let (hi_hi_blocks, hi_lo_blocks) = hi_blocks.split_at_mut(twiddles_small.len() * width); - let (lo_hi_blocks, lo_lo_blocks) = lo_blocks.split_at_mut(twiddles_small.len() * width); - hi_hi_blocks - .chunks_exact_mut(width) - .zip(hi_lo_blocks.chunks_exact_mut(width)) - .zip(lo_hi_blocks.chunks_exact_mut(width)) - .zip(lo_lo_blocks.chunks_exact_mut(width)) - .enumerate() - .for_each(|(ind, (((hi_hi, hi_lo), lo_hi), lo_lo))| { - multi_butterfly.apply_2_layers(((hi_hi, hi_lo), (lo_hi, lo_lo)), ind, twiddles_small, twiddles_large); - }); + // Flatten (block, inner-group) into one parallel loop. A block is `4·ts` rows of + // `width`; group `ind` touches the 4 rows at sub-block offsets `k·ts + ind` (k=0..3). + // Coarse layers (few blocks) thus still parallelize over their `ts` inner groups, and + // guided scheduling keeps a worker's consecutive groups within one block (cache-local). + let ts = twiddles_small.len(); + let block_size = 4 * ts * width; // == twiddles_large.len() * 2 * width + let n_blocks = mat.values.len() / block_size; + let base = ButterflyPtr(mat.values.as_mut_ptr()); + parallel::for_each_index(n_blocks * ts, |g| { + let block_base = (g / ts) * block_size; + let ind = g % ts; + let row = |k: usize| block_base + (k * ts + ind) * width; + // SAFETY: distinct `g` map to disjoint sets of 4 `width`-rows. + let hi_hi = unsafe { base.row(row(0), width) }; + let hi_lo = unsafe { base.row(row(1), width) }; + let lo_hi = unsafe { base.row(row(2), width) }; + let lo_lo = unsafe { base.row(row(3), width) }; + multi_butterfly.apply_2_layers(((hi_hi, hi_lo), (lo_hi, lo_lo)), ind, twiddles_small, twiddles_large); }); } @@ -292,45 +314,37 @@ fn dft_layer_par_triple, M: MultiLayerButterfly> // let inner_chunk_size = // (workload_size::().next_power_of_two() / 8).min(eighth_outer_block_size); - let block_size = twiddles_large.len() * 2 * width; - let n_full = mat.values.len() / block_size * block_size; - // Outer fan-out over blocks runs on the pool; the inner butterflies run sequentially - // within each task (nested dispatches fall back to sequential). - parallel::par_chunks_mut(&mut mat.values[..n_full], block_size, |_, block| { - let (hi_blocks, lo_blocks) = block.split_at_mut(twiddles_small.len() * width * 4); - let (hi_hi_blocks, hi_lo_blocks) = hi_blocks.split_at_mut(twiddles_small.len() * width * 2); - let (lo_hi_blocks, lo_lo_blocks) = lo_blocks.split_at_mut(twiddles_small.len() * width * 2); - let (hi_hi_hi_blocks, hi_hi_lo_blocks) = hi_hi_blocks.split_at_mut(twiddles_small.len() * width); - let (hi_lo_hi_blocks, hi_lo_lo_blocks) = hi_lo_blocks.split_at_mut(twiddles_small.len() * width); - let (lo_hi_hi_blocks, lo_hi_lo_blocks) = lo_hi_blocks.split_at_mut(twiddles_small.len() * width); - let (lo_lo_hi_blocks, lo_lo_lo_blocks) = lo_lo_blocks.split_at_mut(twiddles_small.len() * width); - hi_hi_hi_blocks - .chunks_exact_mut(width) - .zip(hi_hi_lo_blocks.chunks_exact_mut(width)) - .zip(hi_lo_hi_blocks.chunks_exact_mut(width)) - .zip(hi_lo_lo_blocks.chunks_exact_mut(width)) - .zip(lo_hi_hi_blocks.chunks_exact_mut(width)) - .zip(lo_hi_lo_blocks.chunks_exact_mut(width)) - .zip(lo_lo_hi_blocks.chunks_exact_mut(width)) - .zip(lo_lo_lo_blocks.chunks_exact_mut(width)) - .enumerate() - .for_each( - |( - ind, - (((((((hi_hi_hi, hi_hi_lo), hi_lo_hi), hi_lo_lo), lo_hi_hi), lo_hi_lo), lo_lo_hi), lo_lo_lo), - )| { - multi_butterfly.apply_3_layers( - ( - ((hi_hi_hi, hi_hi_lo), (hi_lo_hi, hi_lo_lo)), - ((lo_hi_hi, lo_hi_lo), (lo_lo_hi, lo_lo_lo)), - ), - ind, - twiddles_small, - twiddles_med, - twiddles_large, - ); - }, - ); + // Flatten (block, inner-group) into one parallel loop. A block is `8·ts` rows of + // `width`; group `ind` touches the 8 rows at sub-block offsets `k·ts + ind` (k=0..7). + // Coarse layers still parallelize over their `ts` inner groups; guided scheduling keeps + // a worker's consecutive groups within one block (cache-local). + let ts = twiddles_small.len(); + let block_size = 8 * ts * width; // == twiddles_large.len() * 2 * width + let n_blocks = mat.values.len() / block_size; + let base = ButterflyPtr(mat.values.as_mut_ptr()); + parallel::for_each_index(n_blocks * ts, |g| { + let block_base = (g / ts) * block_size; + let ind = g % ts; + let row = |k: usize| block_base + (k * ts + ind) * width; + // SAFETY: distinct `g` map to disjoint sets of 8 `width`-rows. + let hi_hi_hi = unsafe { base.row(row(0), width) }; + let hi_hi_lo = unsafe { base.row(row(1), width) }; + let hi_lo_hi = unsafe { base.row(row(2), width) }; + let hi_lo_lo = unsafe { base.row(row(3), width) }; + let lo_hi_hi = unsafe { base.row(row(4), width) }; + let lo_hi_lo = unsafe { base.row(row(5), width) }; + let lo_lo_hi = unsafe { base.row(row(6), width) }; + let lo_lo_lo = unsafe { base.row(row(7), width) }; + multi_butterfly.apply_3_layers( + ( + ((hi_hi_hi, hi_hi_lo), (hi_lo_hi, hi_lo_lo)), + ((lo_hi_hi, lo_hi_lo), (lo_lo_hi, lo_lo_lo)), + ), + ind, + twiddles_small, + twiddles_med, + twiddles_large, + ); }); } From 258c38edf798ce28abce17baa708985e92d70064 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 11:03:26 +0200 Subject: [PATCH 13/65] parallel: range-based core dispatch (for_each_chunk) Make the pool primitive hand workers contiguous ranges instead of single indices, so map_reduce / map_reduce_with_state look up their per-worker accumulator once per claimed batch rather than once per element (removes a thread-local read + slot branch from the hot inner loop of every reduction). for_each_index now wraps for_each_chunk. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/parallel/src/lib.rs | 66 +++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 3dcea71b6..6e73cd1a1 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -91,7 +91,10 @@ pub fn current_worker_id() -> usize { /// `'static`. Only dereferenced inside a dispatch window during which the dispatcher /// blocks, so the source borrow outlives every call. struct Job { - f: NonNull, + /// Range-based work: `f(start, end)` processes the half-open task range `start..end`. + /// Building the primitive on ranges (rather than single indices) lets reductions look + /// up their per-worker accumulator once per claimed batch instead of once per element. + f: NonNull, n_tasks: usize, } @@ -239,21 +242,24 @@ fn drain(pool: &Pool) { break; } let end = (start + batch).min(n); - for i in start..end { - f(i); - } + f(start, end); } IN_TASK.set(prev); } -/// Run `f(i)` for every `i` in `0..n_tasks`, in parallel across the pool. Blocks until -/// all tasks complete; the dispatching thread participates as worker 0. -pub fn for_each_index(n_tasks: usize, f: F) { +/// Core dispatch: run `f(start, end)` over disjoint contiguous sub-ranges that together +/// tile `0..n_tasks`, in parallel across the pool. The ranges are produced by guided +/// self-scheduling (see [`drain`]); a worker may receive several. Blocks until all +/// complete; the dispatching thread participates as worker 0. +/// +/// This is the primitive everything else is built on. Range-based (rather than per-index) +/// so a reduction can look up its per-worker accumulator once per claimed batch. +pub fn for_each_chunk(n_tasks: usize, f: F) { // Trivial sizes, single-core builds, and nested dispatches (called from within a // pool task) all run sequentially — the last avoids deadlocking on the lock. if NUM_THREADS <= 1 || n_tasks <= 1 || IN_TASK.get() { - for i in 0..n_tasks { - f(i); + if n_tasks > 0 { + f(0, n_tasks); } return; } @@ -262,12 +268,14 @@ pub fn for_each_index(n_tasks: usize, f: F) { let _guard = pool.dispatch.lock().unwrap(); let n = NUM_THREADS; - let f_ref: &(dyn Fn(usize) + Sync) = &f; + let f_ref: &(dyn Fn(usize, usize) + Sync) = &f; // SAFETY: erase the borrow's lifetime to store in the 'static `Job`. The // dispatcher spins on `working` below before returning, so `f` outlives every // worker call that dereferences this pointer. - let f_erased: NonNull = unsafe { - std::mem::transmute::, NonNull>(NonNull::from(f_ref)) + let f_erased: NonNull = unsafe { + std::mem::transmute::, NonNull>( + NonNull::from(f_ref), + ) }; // SAFETY: all workers finished the previous dispatch (we waited for `working == 0`) @@ -296,6 +304,16 @@ pub fn for_each_index(n_tasks: usize, f: F) { } } +/// Run `f(i)` for every `i` in `0..n_tasks`, in parallel across the pool. Blocks until +/// all tasks complete; the dispatching thread participates as worker 0. +pub fn for_each_index(n_tasks: usize, f: F) { + for_each_chunk(n_tasks, |start, end| { + for i in start..end { + f(i); + } + }); +} + /// Wrapper holding a raw base pointer that is safe to share across workers because /// each worker only touches a disjoint sub-slice computed from its task index. struct SendPtr(*mut T); @@ -355,16 +373,20 @@ where let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); let ptr = SendPtr(slots.as_mut_ptr()); - for_each_index(n_tasks, |i| { + for_each_chunk(n_tasks, |start, end| { + // One worker-slot lookup per claimed batch, then fold the whole range into it — + // amortizing the thread-local read over the batch instead of paying it per element. let wid = current_worker_id(); // SAFETY: `wid` is unique per live worker and < NUM_THREADS; slots disjoint; // `slots` outlives the dispatch. let slot = unsafe { &mut *ptr.add(wid) }; - let v = map(i); - *slot = Some(match slot.take() { - Some(acc) => reduce(acc, v), - None => v, - }); + for i in start..end { + let v = map(i); + *slot = Some(match slot.take() { + Some(acc) => reduce(acc, v), + None => v, + }); + } }); slots.into_iter().flatten().fold(identity(), &reduce) @@ -396,13 +418,17 @@ where let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); let ptr = SendPtr(slots.as_mut_ptr()); - for_each_index(n_tasks, |i| { + for_each_chunk(n_tasks, |start, end| { + // One worker-slot lookup per claimed batch; the reusable scratch and accumulator + // are then threaded through every element of the range. let wid = current_worker_id(); // SAFETY: `wid` unique per live worker and < NUM_THREADS; slots disjoint; // `slots` outlives the dispatch. let slot = unsafe { &mut *ptr.add(wid) }; let (state, acc) = slot.get_or_insert_with(|| (init_state(), init_acc())); - fold(state, acc, i); + for i in start..end { + fold(state, acc, i); + } }); slots From 36f74121059a6df2c5746a0235eed426663f7112 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 11:18:11 +0200 Subject: [PATCH 14/65] parallel: lower spin limit to 2^12 to reduce variance during sequential phases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workers parked only after ~65us of spinning, so they burned all cores through the prover's sequential stretches (Fiat-Shamir, sampling) — preventing the active core from clocking up and adding run-to-run variance. ~4us of spin keeps them hot across tight dispatch loops while letting them sleep during longer gaps. Merge-proof variance dropped (888-893ms vs 885-928ms); throughput unchanged. Also a clippy += cleanup. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/parallel/src/lib.rs | 9 ++++++--- crates/backend/sumcheck/src/product_computation.rs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 6e73cd1a1..979b14f3d 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -56,9 +56,12 @@ use std::thread::Thread; use system_info::NUM_THREADS; -/// Spins an idle worker performs before parking. Tuned so back-to-back dispatches -/// keep workers hot (no syscalls) while a long idle stretch still lets them sleep. -const SPIN_LIMIT: u32 = 1 << 16; +/// Spins an idle worker performs before parking (~a few µs). Tuned so back-to-back +/// dispatches keep workers hot (no syscalls), while the prover's *sequential* stretches +/// — longer than this — let the workers sleep, freeing their cores so the active thread +/// can clock up instead of competing with 15 spinners. (A much larger limit kept workers +/// spinning through those gaps, adding run-to-run variance for no throughput gain.) +const SPIN_LIMIT: u32 = 1 << 12; /// Upper bound on a single guided-self-scheduling claim (see [`drain`]). Caps the /// worst-case load imbalance from one worker grabbing too large an initial batch, while diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index 25eeb8f6c..2f1bbc865 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -177,8 +177,8 @@ pub fn compute_product_sumcheck_polynomial< let mut a2 = EFPacking::ZERO; for i in start..end { let (b0, b2) = sumcheck_quadratic(((&pol_0[i], &pol_0[half + i]), (&pol_1[i], &pol_1[half + i]))); - a0 = a0 + b0; - a2 = a2 + b2; + a0 += b0; + a2 += b2; } (a0, a2) }, From da01c45f0d086c5fdaf9e5cb3c1adfca33d7b382 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 12:23:07 +0200 Subject: [PATCH 15/65] sumcheck: product reductions use in-place per-worker accumulation Replace the chunk-of-1024 + per-chunk-tuple-reduce in compute/fold_and_compute with map_reduce_with_state in-place accumulation (same pattern as the GKR folds): the worker folds its claimed range straight into its (c0,c2) accumulator. Perf-neutral, removes the magic chunk constant and the per-chunk tuple. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../sumcheck/src/product_computation.rs | 87 ++++++++----------- 1 file changed, 37 insertions(+), 50 deletions(-) diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index 2f1bbc865..7c4a3d8ed 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -161,26 +161,18 @@ pub fn compute_product_sumcheck_polynomial< (a0 + b0, a2 + b2) }) } else { - // Chunk the reduction so each pool task folds a contiguous range into a local - // accumulator — amortizing the per-task `current_worker_id()`/slot overhead that - // a one-element-per-task `map_reduce` would pay millions of times. + // Per-worker in-place accumulation: each worker folds the contiguous range it + // claims straight into its own `(c0, c2)` accumulator (no per-chunk tuple to build + // and reduce, worker-slot lookup amortized once per batch by `for_each_chunk`). let half = n / 2; - let chunk_size = 1024; - let n_chunks = half.div_ceil(chunk_size); - parallel::map_reduce( - n_chunks, + parallel::map_reduce_with_state( + half, + || (), || (EFPacking::ZERO, EFPacking::ZERO), - |c| { - let start = c * chunk_size; - let end = (start + chunk_size).min(half); - let mut a0 = EFPacking::ZERO; - let mut a2 = EFPacking::ZERO; - for i in start..end { - let (b0, b2) = sumcheck_quadratic(((&pol_0[i], &pol_0[half + i]), (&pol_1[i], &pol_1[half + i]))); - a0 += b0; - a2 += b2; - } - (a0, a2) + |(), acc, i| { + let (b0, b2) = sumcheck_quadratic(((&pol_0[i], &pol_0[half + i]), (&pol_1[i], &pol_1[half + i]))); + acc.0 += b0; + acc.1 += b2; }, |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), ) @@ -312,43 +304,38 @@ pub fn fold_and_compute_product_sumcheck_polynomial< (a0 + b0, a2 + b2) }) } else { - // Fused single pass: fold both polynomials (writing the disjoint `i` / `quarter + i` - // slots of the output buffers) and reduce the per-index quadratic contributions. + // Fused single pass with per-worker in-place accumulation: fold both polynomials + // (writing the disjoint `i` / `quarter + i` output slots) and accumulate the + // per-index quadratic straight into the worker's `(c0, c2)` — no per-chunk tuple. let quarter = n / 4; let p0f = SyncMutPtr(pol_0_folded.as_mut_ptr()); let p1f = SyncMutPtr(pol_1_folded.as_mut_ptr()); - let chunk_size = 1024.min(quarter).max(1); - let n_chunks = quarter.div_ceil(chunk_size); - parallel::map_reduce( - n_chunks, + parallel::map_reduce_with_state( + quarter, + || (), || (EFPacking::ZERO, EFPacking::ZERO), - |c| { - let start = c * chunk_size; - let end = (start + chunk_size).min(quarter); - let mut acc = (EFPacking::ZERO, EFPacking::ZERO); - for i in start..end { - let diff_0 = pol_0[2 * quarter + i] - pol_0[i]; - let diff_1 = pol_0[3 * quarter + i] - pol_0[quarter + i]; - let x_0 = prev_folding_factor_packed * diff_0 + pol_0[i]; - let x_1 = prev_folding_factor_packed * diff_1 + pol_0[quarter + i]; - - let y_0 = prev_folding_factor_packed * (pol_1[2 * quarter + i] - pol_1[i]) + pol_1[i]; - let y_1 = - prev_folding_factor_packed * (pol_1[3 * quarter + i] - pol_1[quarter + i]) + pol_1[quarter + i]; - - // SAFETY: distinct `i` write disjoint slots `i` and `quarter + i` in - // `[0, n/2)`; the dispatcher keeps both buffers borrowed for the call. - unsafe { - *p0f.add(i) = x_0; - *p0f.add(quarter + i) = x_1; - *p1f.add(i) = y_0; - *p1f.add(quarter + i) = y_1; - } - - let (b0, b2) = sumcheck_quadratic(((&x_0, &x_1), (&y_0, &y_1))); - acc = (acc.0 + b0, acc.1 + b2); + |(), acc, i| { + let diff_0 = pol_0[2 * quarter + i] - pol_0[i]; + let diff_1 = pol_0[3 * quarter + i] - pol_0[quarter + i]; + let x_0 = prev_folding_factor_packed * diff_0 + pol_0[i]; + let x_1 = prev_folding_factor_packed * diff_1 + pol_0[quarter + i]; + + let y_0 = prev_folding_factor_packed * (pol_1[2 * quarter + i] - pol_1[i]) + pol_1[i]; + let y_1 = + prev_folding_factor_packed * (pol_1[3 * quarter + i] - pol_1[quarter + i]) + pol_1[quarter + i]; + + // SAFETY: distinct `i` write disjoint slots `i` and `quarter + i` in + // `[0, n/2)`; the dispatcher keeps both buffers borrowed for the call. + unsafe { + *p0f.add(i) = x_0; + *p0f.add(quarter + i) = x_1; + *p1f.add(i) = y_0; + *p1f.add(quarter + i) = y_1; } - acc + + let (b0, b2) = sumcheck_quadratic(((&x_0, &x_1), (&y_0, &y_1))); + acc.0 += b0; + acc.1 += b2; }, |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), ) From acdf8acc4a63739d3e4f5f28429023edc39e7a9c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 13:47:49 +0200 Subject: [PATCH 16/65] gitignore: ignore sibling /target-* build dirs and /.lm-* scratch binaries --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 55d8a4c3a..2c2fca099 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /target +/target-* +/.lm-* .vscode /docs/benchmark_graphs/.venv minimal_zkVM.synctex.gz From e638df9f6eb4c18195876ea192a5e4ad6ca97876 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 14:14:11 +0200 Subject: [PATCH 17/65] lean_vm: in-place get_slice_into for Poseidon inputs (avoid ~100K Vec allocs/proof) The Poseidon precompile read 3 small Vec per instruction via get_slice and copied them into a stack array. Fill the array directly via a new MemoryAccess::get_slice_into, eliminating the dominant small-alloc source (the 32-byte bucket dropped from 166K to 65K in a leaf census). Perf-neutral on the arena, reduces allocator pressure on any allocator. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/lean_vm/src/execution/memory.rs | 10 ++++++++++ crates/lean_vm/src/tables/poseidon/mod.rs | 14 +++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/lean_vm/src/execution/memory.rs b/crates/lean_vm/src/execution/memory.rs index 954dd9a10..38b5a41e2 100644 --- a/crates/lean_vm/src/execution/memory.rs +++ b/crates/lean_vm/src/execution/memory.rs @@ -11,6 +11,16 @@ pub trait MemoryAccess { (0..len).map(|i| self.get(start + i)).collect() } + /// In-place version of [`get_slice`] that writes into a caller-provided buffer, + /// avoiding a per-call heap allocation on the hot interpreter path (Poseidon / + /// extension-op slice reads run hundreds of thousands of times per proof). + fn get_slice_into(&self, start: usize, dest: &mut [F]) -> Result<(), RunnerError> { + for (i, d) in dest.iter_mut().enumerate() { + *d = self.get(start + i)?; + } + Ok(()) + } + fn set_slice(&mut self, start: usize, values: &[F]) -> Result<(), RunnerError> { for (i, v) in values.iter().enumerate() { self.set(start + i, *v)?; diff --git a/crates/lean_vm/src/tables/poseidon/mod.rs b/crates/lean_vm/src/tables/poseidon/mod.rs index f90e4646a..8dbd84561 100644 --- a/crates/lean_vm/src/tables/poseidon/mod.rs +++ b/crates/lean_vm/src/tables/poseidon/mod.rs @@ -237,14 +237,14 @@ impl TableT for Poseidon16Precompile { } else { arg_a_usize + HALF_DIGEST_LEN }; - let arg0_first = ctx.memory.get_slice(left_first_addr, HALF_DIGEST_LEN)?; - let arg0_second = ctx.memory.get_slice(left_second_addr, HALF_DIGEST_LEN)?; - let arg1 = ctx.memory.get_slice(arg_b.to_usize(), DIGEST_LEN)?; - + // Fill the Poseidon input array directly from memory — no per-call Vec allocation + // (this runs once per Poseidon instruction, the dominant small-alloc source). let mut input = [F::ZERO; DIGEST_LEN * 2]; - input[..HALF_DIGEST_LEN].copy_from_slice(&arg0_first); - input[HALF_DIGEST_LEN..DIGEST_LEN].copy_from_slice(&arg0_second); - input[DIGEST_LEN..].copy_from_slice(&arg1); + ctx.memory + .get_slice_into(left_first_addr, &mut input[..HALF_DIGEST_LEN])?; + ctx.memory + .get_slice_into(left_second_addr, &mut input[HALF_DIGEST_LEN..DIGEST_LEN])?; + ctx.memory.get_slice_into(arg_b.to_usize(), &mut input[DIGEST_LEN..])?; let res_addr = index_res_a.to_usize(); if permute { From 22c66f761c528740631bd97ac3e506d89a44de28 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 15:42:38 +0200 Subject: [PATCH 18/65] alloc: replace zk-alloc bump arena with tuned mimalloc Remove the per-phase zk-alloc bump arena entirely and run on mimalloc, a production allocator. The arena was fast but fragile: any allocation outliving a phase, or a pointer retained across begin_phase's slab reset (from a background thread, tracing, or a stray clone), silently corrupted memory. The arena was fast only because it reused the same pages instead of re-faulting. We recover that with mimalloc by disabling page purging (mi_option_purge_delay = -1, via tune_allocator()), so freed large blocks are retained rather than returned to the OS. This matches and beats the arena: ~11.96s vs 12.84s on `recursion --n 4` (interleaved, 3 rounds, ~7% faster than main) with none of the arena's fragility. - delete crates/backend/zk-alloc (crate + workspace member + dep) - src/main.rs: mimalloc global allocator (standard-alloc feature opts out) - src/lib.rs: tune_allocator(); parallel::init() warms the pool up front - benchmark.rs: drop begin_phase/end_phase/clone ceremony - replace test_zk_alloc with allocator-agnostic test_aggregation - refresh stale arena references in parallel/sumcheck doc comments Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 60 +++++- Cargo.toml | 8 +- crates/backend/parallel/src/lib.rs | 15 +- crates/backend/sumcheck/src/sc_computation.rs | 4 +- crates/backend/zk-alloc/Cargo.toml | 15 -- crates/backend/zk-alloc/src/lib.rs | 201 ------------------ crates/backend/zk-alloc/src/syscall.rs | 163 -------------- crates/rec_aggregation/Cargo.toml | 2 - crates/rec_aggregation/src/benchmark.rs | 10 - src/lib.rs | 31 +-- src/main.rs | 13 +- tests/test_aggregation.rs | 21 ++ tests/test_zk_alloc.rs | 31 --- 13 files changed, 111 insertions(+), 463 deletions(-) delete mode 100644 crates/backend/zk-alloc/Cargo.toml delete mode 100644 crates/backend/zk-alloc/src/lib.rs delete mode 100644 crates/backend/zk-alloc/src/syscall.rs create mode 100644 tests/test_aggregation.rs delete mode 100644 tests/test_zk_alloc.rs diff --git a/Cargo.lock b/Cargo.lock index 709edb3be..8f7248a2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,16 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -259,6 +269,12 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + [[package]] name = "digest" version = "0.10.7" @@ -304,6 +320,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "foldhash" version = "0.1.5" @@ -468,6 +490,8 @@ dependencies = [ "backend", "clap", "lean_vm", + "libmimalloc-sys", + "mimalloc", "rand", "rec_aggregation", "serde_json", @@ -475,7 +499,6 @@ dependencies = [ "system-info", "utils", "xmss", - "zk-alloc", ] [[package]] @@ -540,6 +563,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", + "cty", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -579,6 +612,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mt-air" version = "0.1.0" @@ -910,7 +952,6 @@ dependencies = [ "tracing", "utils", "xmss", - "zk-alloc", ] [[package]] @@ -1024,6 +1065,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "smallvec" version = "1.15.1" @@ -1433,15 +1480,6 @@ dependencies = [ "utils", ] -[[package]] -name = "zk-alloc" -version = "0.1.0" -dependencies = [ - "libc", - "parallel", - "system-info", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index e51ec0246..229c6f896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ members = [ "crates/backend/fiat-shamir", "crates/backend/sumcheck", "crates/backend/system-info", - "crates/backend/zk-alloc", "crates/backend/parallel", ] @@ -62,7 +61,6 @@ lean_compiler = { path = "crates/lean_compiler" } lean_prover = { path = "crates/lean_prover" } rec_aggregation = { path = "crates/rec_aggregation" } backend = { path = "crates/backend" } -zk-alloc = { path = "crates/backend/zk-alloc" } system-info = { path = "crates/backend/system-info" } parallel = { path = "crates/backend/parallel" } @@ -84,12 +82,14 @@ include_dir = "0.7" [features] prox-gaps-conjecture = ["rec_aggregation/prox-gaps-conjecture"] -standard-alloc = ["rec_aggregation/standard-alloc"] +# Build with the plain system allocator instead of mimalloc (for comparison/debugging). +standard-alloc = [] [dependencies] clap.workspace = true rec_aggregation.workspace = true -zk-alloc.workspace = true +mimalloc = "0.1" +libmimalloc-sys = { version = "0.1", features = ["extended"] } rand.workspace = true sub_protocols.workspace = true utils.workspace = true diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 979b14f3d..7f05e86e5 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -4,8 +4,8 @@ //! hot paths: "split a range/slice into pieces, run a closure on each." Unlike rayon //! it does **no** work-stealing of nested tasks and allocates **nothing** per //! dispatch — the whole point is to own the runtime so we can (a) attach per-worker -//! scratch buffers (eliminating the per-task allocations that justify the `zk-alloc` -//! arena), and (b) drop rayon entirely along with its `flush_rayon` injector hack. +//! scratch buffers (eliminating per-task allocations on hot paths), and (b) drop +//! rayon entirely along with its `flush_rayon` injector hack. //! //! ## Model //! @@ -129,12 +129,11 @@ unsafe impl Send for Pool {} /// Construct the pool and exercise its dispatch path once, now. /// -/// **Must be called before any arena allocator that recycles memory between phases is -/// active** (e.g. before `zk_alloc::begin_phase()`); idempotent. The leaked `Pool` -/// and the `dispatch` mutex's lazily-allocated `pthread_mutex_t` (macOS allocates it -/// on first lock, not construction) must live in the system allocator, not a slab the -/// next reset recycles. Running one real dispatch forces those allocations while the -/// arena is inactive. (Worker `Parker`s are allocated eagerly at spawn, also here.) +/// Optional warm-up: idempotent, it spawns the worker threads and runs one real +/// dispatch so the leaked `Pool`, the worker `Parker`s, and the `dispatch` mutex's +/// lazily-allocated `pthread_mutex_t` (macOS allocates it on first lock, not +/// construction) are all created up front, before any timed proving work. Without it +/// the pool initializes lazily on the first parallel call. pub fn init() { static INIT: Once = Once::new(); INIT.call_once(|| { diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index dbd1e7be1..0a6d02ed3 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -418,8 +418,8 @@ where { // Per-worker scratch: `rows` (the [lo, diff, hi] triples) and `point` (the // evaluation point handed to `eval_fn`) are reused across every task a worker - // owns, so the hot loop allocates nothing — recovering what the zk-alloc arena - // would otherwise absorb. `acc` (length `degree`) is the per-worker partial sum. + // owns, so the hot loop allocates nothing. `acc` (length `degree`) is the + // per-worker partial sum. let n_mult = multilinears.len(); let sums = parallel::map_reduce_with_state( fold_size, diff --git a/crates/backend/zk-alloc/Cargo.toml b/crates/backend/zk-alloc/Cargo.toml deleted file mode 100644 index 3c02742c9..000000000 --- a/crates/backend/zk-alloc/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "zk-alloc" -version.workspace = true -edition.workspace = true -description = "Bump+reset arena allocator for ZK proving workloads" - -[dependencies] -system-info.workspace = true -parallel.workspace = true - -[target.'cfg(not(all(target_os = "linux", target_arch = "x86_64")))'.dependencies] -libc = "0.2" - -[lints] -workspace = true diff --git a/crates/backend/zk-alloc/src/lib.rs b/crates/backend/zk-alloc/src/lib.rs deleted file mode 100644 index 1cda9bb9a..000000000 --- a/crates/backend/zk-alloc/src/lib.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Bump-pointer arena allocator. -//! -//! One mmap region split into per-thread slabs. Allocation = increment a thread-local -//! pointer; free = no-op. `begin_phase()` resets the arena: each thread's next -//! allocation starts over at the beginning of its slab, overwriting the previous -//! phase's data. Allocations that don't fit (too large, or beyond `MAX_THREADS`) fall -//! back to the system allocator. -//! -//! ```ignore -//! init(); // once, at process start -//! loop { -//! begin_phase(); // arena ON; slabs reset lazily -//! let res = heavy_work(); // fast increments -//! end_phase(); // arena OFF; new allocations go to System -//! let copy = res.clone(); // detach from arena before next phase resets it -//! } -//! ``` - -use std::alloc::{GlobalAlloc, Layout}; -use std::cell::Cell; -use std::sync::Once; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - -use system_info::NUM_THREADS; - -mod syscall; - -const SLAB_SIZE: usize = 8 << 30; // 8GB -const SLACK: usize = 4; // SLACK absorbs the main thread and any non-rayon helpers. -const MAX_THREADS: usize = NUM_THREADS + SLACK; -const REGION_SIZE: usize = SLAB_SIZE * MAX_THREADS; - -#[derive(Debug)] -pub struct ZkAllocator; - -/// Incremented by `begin_phase()`. Every thread caches the last value it saw in -/// `ARENA_GEN`; when they differ, the thread resets its allocation cursor to the start -/// of its slab on the next allocation. This is how a single store on the main thread -/// "resets" every other thread's slab without any cross-thread synchronization. -static GENERATION: AtomicUsize = AtomicUsize::new(0); - -/// Master switch for the arena. `true` (set by `begin_phase`) routes allocations -/// through the arena; `false` (set by `end_phase`) routes them to the system allocator. -static ARENA_ACTIVE: AtomicBool = AtomicBool::new(false); - -/// Base address of the mmap'd region, or `0` before `ensure_region` runs. Read on -/// every `dealloc` to test whether a pointer belongs to us. -static REGION_BASE: AtomicUsize = AtomicUsize::new(0); - -/// Synchronizes the one-time mmap so concurrent first-allocators don't race. -static REGION_INIT: Once = Once::new(); - -/// Monotonic counter handed out to threads to pick their slab. `fetch_add`'d once per -/// thread on its first arena allocation. Threads that get `idx >= MAX_THREADS` mark -/// themselves `ARENA_NO_SLAB` and permanently fall through to the system allocator. -static THREAD_IDX: AtomicUsize = AtomicUsize::new(0); - -thread_local! { - /// Where this thread's next allocation lands. Advanced past each allocation. - static ARENA_PTR: Cell = const { Cell::new(0) }; - /// One past the last byte of this thread's slab. An alloc fits iff - /// `aligned + size <= ARENA_END`. - static ARENA_END: Cell = const { Cell::new(0) }; - /// Base address of this thread's slab (`0` = not yet claimed). On reset, - /// `ARENA_PTR` is set back to this value. - static ARENA_BASE: Cell = const { Cell::new(0) }; - /// Last `GENERATION` value this thread observed. When the global moves past - /// this, the next allocation resets `ARENA_PTR` to `ARENA_BASE` and updates - /// this field. - static ARENA_GEN: Cell = const { Cell::new(0) }; - /// `true` if this thread was created after `MAX_THREADS` was already exhausted. - /// Such threads skip arena logic entirely and always go to the system allocator. - static ARENA_NO_SLAB: Cell = const { Cell::new(false) }; -} - -/// Returns the base address of the mmap'd region, mapping it on the first call. -fn ensure_region() -> usize { - REGION_INIT.call_once(|| { - // SAFETY: mmap_anonymous returns a page-aligned pointer or null. MAP_NORESERVE - // means no physical memory is committed until pages are touched. - let ptr = unsafe { syscall::mmap_anonymous(REGION_SIZE) }; - if ptr.is_null() { - std::process::abort(); - } - unsafe { syscall::madvise(ptr, REGION_SIZE, syscall::MADV_NOHUGEPAGE) }; - REGION_BASE.store(ptr as usize, Ordering::Release); - }); - REGION_BASE.load(Ordering::Acquire) -} - -/// Call once at process start, before any `begin_phase()`. -pub fn init() { - let actual_num_threads = std::thread::available_parallelism().unwrap().get(); - assert_eq!( - actual_num_threads, NUM_THREADS, - "built for {NUM_THREADS} threads but this machine reports {actual_num_threads} -> please rebuild`" - ); -} - -/// Activates the arena and resets every thread's slab. All allocations until the next -/// `end_phase()` go to the arena; the previous phase's data is overwritten in place. -pub fn begin_phase() { - // Ensure the `parallel` thread pool is fully constructed *before* the arena goes - // active. Its persistent state must live in the system allocator: if it were - // lazily created during a phase it would land in a slab that the next - // `begin_phase()` recycles, corrupting the workers' barriers. Idempotent, so the - // cost after the first call is a single atomic load. - parallel::init(); - - let prev_active = ARENA_ACTIVE.swap(true, Ordering::Release); - assert!( - !prev_active, - "begin_phase() called while another phase is already active — phases must not nest" - ); - GENERATION.fetch_add(1, Ordering::Release); -} - -/// Deactivates the arena. New allocations go to the system allocator; existing arena -/// pointers stay valid until the next `begin_phase()` resets the slabs. -pub fn end_phase() { - ARENA_ACTIVE.store(false, Ordering::Release); -} - -#[cold] -#[inline(never)] -unsafe fn arena_alloc_cold(size: usize, align: usize) -> *mut u8 { - let generation = GENERATION.load(Ordering::Relaxed); - if !ARENA_NO_SLAB.get() && ARENA_GEN.get() != generation { - let mut base = ARENA_BASE.get(); - if base == 0 { - let region = ensure_region(); - let idx = THREAD_IDX.fetch_add(1, Ordering::Relaxed); - if idx >= MAX_THREADS { - ARENA_NO_SLAB.set(true); - return unsafe { std::alloc::System.alloc(Layout::from_size_align_unchecked(size, align)) }; - } - base = region + idx * SLAB_SIZE; - ARENA_BASE.set(base); - ARENA_END.set(base + SLAB_SIZE); - } - ARENA_PTR.set(base); - ARENA_GEN.set(generation); - let aligned = base.next_multiple_of(align); - let new_ptr = aligned + size; - if new_ptr <= ARENA_END.get() { - ARENA_PTR.set(new_ptr); - return aligned as *mut u8; - } - } - unsafe { std::alloc::System.alloc(Layout::from_size_align_unchecked(size, align)) } -} - -// SAFETY: All pointers returned are either from our mmap'd region (valid, aligned, -// non-overlapping per thread) or from System. The arena is thread-local so no data -// races. Relaxed ordering on ARENA_ACTIVE/GENERATION is sound: worst case a thread -// sees a stale value and does one extra system-alloc before picking up the new -// generation on the next call. -unsafe impl GlobalAlloc for ZkAllocator { - #[inline(always)] - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - if ARENA_ACTIVE.load(Ordering::Relaxed) { - let generation = GENERATION.load(Ordering::Relaxed); - if ARENA_GEN.get() == generation { - let align = layout.align(); - let aligned = (ARENA_PTR.get() + align - 1) & !(align - 1); - let new_ptr = aligned + layout.size(); - if new_ptr <= ARENA_END.get() { - ARENA_PTR.set(new_ptr); - return aligned as *mut u8; - } - } - return unsafe { arena_alloc_cold(layout.size(), layout.align()) }; - } - unsafe { std::alloc::System.alloc(layout) } - } - - #[inline(always)] - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - let addr = ptr as usize; - let base = REGION_BASE.load(Ordering::Relaxed); - if base != 0 && addr >= base && addr < base + REGION_SIZE { - return; // arena-owned pointer — free is a no-op - } - unsafe { std::alloc::System.dealloc(ptr, layout) }; - } - - #[inline(always)] - unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - if new_size <= layout.size() { - return ptr; - } - // SAFETY: new_size > layout.size() > 0, align unchanged from valid layout. - let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) }; - let new_ptr = unsafe { self.alloc(new_layout) }; - if !new_ptr.is_null() { - unsafe { std::ptr::copy(ptr, new_ptr, layout.size()) }; - unsafe { self.dealloc(ptr, layout) }; - } - new_ptr - } -} diff --git a/crates/backend/zk-alloc/src/syscall.rs b/crates/backend/zk-alloc/src/syscall.rs deleted file mode 100644 index 13d71531d..000000000 --- a/crates/backend/zk-alloc/src/syscall.rs +++ /dev/null @@ -1,163 +0,0 @@ -// Raw syscalls instead of libc wrappers to avoid reentrancy: libc's mmap/madvise -// may internally call malloc, which would deadlock when called from inside -// #[global_allocator]. - -#[cfg(all(target_os = "linux", target_arch = "x86_64"))] -mod imp { - use std::ptr; - - const SYS_MMAP: usize = 9; - const SYS_MADVISE: usize = 28; - - const PROT_READ: usize = 1; - const PROT_WRITE: usize = 2; - const MAP_PRIVATE: usize = 0x02; - const MAP_ANONYMOUS: usize = 0x20; - const MAP_NORESERVE: usize = 0x4000; - - pub const MADV_NOHUGEPAGE: usize = 15; - - #[inline] - unsafe fn syscall6(nr: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) -> isize { - let ret: isize; - unsafe { - std::arch::asm!( - "syscall", - inlateout("rax") nr as isize => ret, - in("rdi") a1, - in("rsi") a2, - in("rdx") a3, - in("r10") a4, - in("r8") a5, - in("r9") a6, - lateout("rcx") _, - lateout("r11") _, - options(nostack), - ); - } - ret - } - - #[inline] - unsafe fn syscall3(nr: usize, a1: usize, a2: usize, a3: usize) -> isize { - let ret: isize; - unsafe { - std::arch::asm!( - "syscall", - inlateout("rax") nr as isize => ret, - in("rdi") a1, - in("rsi") a2, - in("rdx") a3, - lateout("rcx") _, - lateout("r11") _, - lateout("r10") _, - options(nostack), - ); - } - ret - } - - #[inline] - pub unsafe fn mmap_anonymous(size: usize) -> *mut u8 { - let flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE; - let ret = unsafe { syscall6(SYS_MMAP, 0, size, PROT_READ | PROT_WRITE, flags, usize::MAX, 0) }; - if ret < 0 { ptr::null_mut() } else { ret as *mut u8 } - } - - #[inline] - pub unsafe fn madvise(ptr: *mut u8, size: usize, advice: usize) { - unsafe { syscall3(SYS_MADVISE, ptr as usize, size, advice) }; - } -} - -#[cfg(all(target_os = "linux", target_arch = "aarch64"))] -mod imp { - use std::ptr; - - const SYS_MMAP: usize = 222; - const SYS_MADVISE: usize = 233; - - const PROT_READ: usize = 1; - const PROT_WRITE: usize = 2; - const MAP_PRIVATE: usize = 0x02; - const MAP_ANONYMOUS: usize = 0x20; - const MAP_NORESERVE: usize = 0x4000; - - pub const MADV_NOHUGEPAGE: usize = 15; - - #[inline] - unsafe fn syscall6(nr: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) -> isize { - let ret: isize; - unsafe { - std::arch::asm!( - "svc 0", - in("x8") nr, - inlateout("x0") a1 as isize => ret, - in("x1") a2, - in("x2") a3, - in("x3") a4, - in("x4") a5, - in("x5") a6, - options(nostack), - ); - } - ret - } - - #[inline] - unsafe fn syscall3(nr: usize, a1: usize, a2: usize, a3: usize) -> isize { - let ret: isize; - unsafe { - std::arch::asm!( - "svc 0", - in("x8") nr, - inlateout("x0") a1 as isize => ret, - in("x1") a2, - in("x2") a3, - options(nostack), - ); - } - ret - } - - #[inline] - pub unsafe fn mmap_anonymous(size: usize) -> *mut u8 { - let flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE; - let ret = unsafe { syscall6(SYS_MMAP, 0, size, PROT_READ | PROT_WRITE, flags, usize::MAX, 0) }; - if ret < 0 { ptr::null_mut() } else { ret as *mut u8 } - } - - #[inline] - pub unsafe fn madvise(ptr: *mut u8, size: usize, advice: usize) { - unsafe { syscall3(SYS_MADVISE, ptr as usize, size, advice) }; - } -} - -#[cfg(not(all(target_os = "linux", any(target_arch = "x86_64", target_arch = "aarch64"))))] -mod imp { - use std::ptr; - - pub const MADV_NOHUGEPAGE: usize = 15; - - #[inline] - pub unsafe fn mmap_anonymous(size: usize) -> *mut u8 { - // MAP_NORESERVE is Linux-only. macOS lazily backs anonymous mappings - // with physical memory by default, so the large virtual reservation - // is fine without NORESERVE. - let prot = libc::PROT_READ | libc::PROT_WRITE; - let flags = libc::MAP_PRIVATE | libc::MAP_ANON; - let ret = unsafe { libc::mmap(ptr::null_mut(), size, prot, flags, -1, 0) }; - if ret == libc::MAP_FAILED { - ptr::null_mut() - } else { - ret.cast::() - } - } - - #[inline] - pub unsafe fn madvise(_ptr: *mut u8, _size: usize, _advice: usize) { - // The advice values we pass are Linux-specific. - } -} - -pub use imp::{MADV_NOHUGEPAGE, madvise, mmap_anonymous}; diff --git a/crates/rec_aggregation/Cargo.toml b/crates/rec_aggregation/Cargo.toml index ac111ba3f..a28af7b9b 100644 --- a/crates/rec_aggregation/Cargo.toml +++ b/crates/rec_aggregation/Cargo.toml @@ -8,7 +8,6 @@ workspace = true [features] prox-gaps-conjecture = ["lean_prover/prox-gaps-conjecture"] -standard-alloc = [] [dependencies] utils.workspace = true @@ -25,7 +24,6 @@ backend.workspace = true postcard.workspace = true lz4_flex.workspace = true serde.workspace = true -zk-alloc.workspace = true [target.'cfg(target_os = "macos")'.dependencies] objc2 = { version = "0.6.4", default-features = false, features = ["std"] } diff --git a/crates/rec_aggregation/src/benchmark.rs b/crates/rec_aggregation/src/benchmark.rs index 920d9f4c3..673616590 100644 --- a/crates/rec_aggregation/src/benchmark.rs +++ b/crates/rec_aggregation/src/benchmark.rs @@ -395,9 +395,6 @@ fn build_aggregation( let mut last_result: Option = None; let own_display_index = display_index + count_nodes(topology) - 1; for _ in 0..repeat { - #[cfg(not(feature = "standard-alloc"))] - zk_alloc::begin_phase(); - let time = Instant::now(); let result = aggregate_type_1( &children, @@ -409,13 +406,6 @@ fn build_aggregation( .unwrap(); let elapsed = time.elapsed(); - // Clone the outputs out of the arena before the next phase resets its slabs. - #[cfg(not(feature = "standard-alloc"))] - let result = { - zk_alloc::end_phase(); - result.clone() - }; - times.push(elapsed.as_secs_f64()); last_result = Some(result); diff --git a/src/lib.rs b/src/lib.rs index d75581759..e484e3764 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,26 @@ pub use xmss::{MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, xmss pub type F = KoalaBear; +/// Tune the default (mimalloc) allocator for the prover's churn of huge short-lived buffers. +/// +/// Disables purging so freed large blocks are **retained** rather than returned to the OS +/// and re-faulted on the next allocation. This is what made the old bump arena fast (it +/// reused the same pages); mimalloc-with-retention matches *and beats* it here, without the +/// arena's fragility. Idempotent; call before any heavy proving allocation. +pub fn tune_allocator() { + // mimalloc v3 option index `mi_option_purge_delay` = 15; value -1 = never purge + // (equivalent to `MIMALLOC_PURGE_DELAY=-1`). No-op under the `standard-alloc` (plain + // system allocator) build. + #[cfg(not(feature = "standard-alloc"))] + unsafe { + libmimalloc_sys::mi_option_set(15, -1); + } +} + /// Call once before proving. Compiles the aggregation program and precomputes DFT twiddles. pub fn setup_prover() { + tune_allocator(); + parallel::init(); // construct the thread pool up front (was done by `zk_alloc::begin_phase`) rec_aggregation::init_aggregation_bytecode(); precompute_dft_twiddles::(1 << 24); } @@ -19,16 +37,3 @@ pub fn setup_prover() { pub fn setup_verifier() { rec_aggregation::init_aggregation_bytecode(); } - -/// Bump-arena allocator. -/// -/// **Optional.** -/// -/// To enable, set it as the `#[global_allocator]` in your binary and call -/// [`init_allocator`] once at startup. Then bracket each proving call with -/// [`begin_phase`] / [`end_phase`] and **clone the outputs after -/// [`end_phase`]** so the cloned copy lands in the system allocator before the -/// next [`begin_phase`] resets the arena slabs. -/// -/// See `tests/test_zk_alloc.rs` for a runnable end-to-end example. -pub use zk_alloc::{ZkAllocator, begin_phase, end_phase, init as init_allocator}; diff --git a/src/main.rs b/src/main.rs index 646fc6f64..4328b882b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,15 @@ use clap::Parser; use rec_aggregation::benchmark::{AggregationTopology, biggest_leaf, run_aggregation_benchmark}; +// Allocator: mimalloc — a robust production allocator, tuned to retain freed memory (see +// `lean_multisig::tune_allocator`). Replaces the former `zk-alloc` bump arena, which was +// fast but fragile: any allocation outliving a phase, or a pointer retained across +// `begin_phase`'s slab reset (e.g. from a background thread, tracing, or a stray clone), +// silently corrupted memory. mimalloc-with-retention is both **faster** here and stable. +// The `standard-alloc` feature selects the plain system allocator for comparison. #[cfg(not(feature = "standard-alloc"))] #[global_allocator] -static ALLOC: zk_alloc::ZkAllocator = zk_alloc::ZkAllocator; +static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; #[derive(Parser)] enum Cli { @@ -67,8 +73,9 @@ fn run_with_warmup(topology: &AggregationTopology, tracing: bool, json: bool, re #[allow(clippy::too_many_lines)] fn main() { - #[cfg(not(feature = "standard-alloc"))] - zk_alloc::init(); + // Retain freed memory (no purging) so the prover's huge buffer churn reuses pages + // instead of re-faulting — the property that made the old arena fast. Before any work. + lean_multisig::tune_allocator(); let cli = Cli::parse(); diff --git a/tests/test_aggregation.rs b/tests/test_aggregation.rs new file mode 100644 index 000000000..6d0ea9f15 --- /dev/null +++ b/tests/test_aggregation.rs @@ -0,0 +1,21 @@ +use lean_multisig::{aggregate_type_1, setup_prover, verify_type_1}; +use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; + +// End-to-end prove+verify under the default (mimalloc) allocator. Repeated to catch any +// cross-run state corruption. (Replaces the former `test_zk_alloc` arena regression test; +// the bump arena and its phase ceremony were removed in favor of a robust allocator.) +#[test] +fn test_aggregation_prove_verify() { + setup_prover(); + + let log_inv_rate = 2; + let message = message_for_benchmark(); + let slot: u32 = BENCHMARK_SLOT; + let signatures = get_benchmark_signatures(); + let raw_xmss = signatures[0..6].to_vec(); + + for _ in 0..2 { + let aggregated = aggregate_type_1(&[], raw_xmss.clone(), message, slot, log_inv_rate).unwrap(); + verify_type_1(&aggregated).unwrap(); + } +} diff --git a/tests/test_zk_alloc.rs b/tests/test_zk_alloc.rs deleted file mode 100644 index 1177db876..000000000 --- a/tests/test_zk_alloc.rs +++ /dev/null @@ -1,31 +0,0 @@ -use lean_multisig::{ZkAllocator, aggregate_type_1, begin_phase, end_phase, setup_prover, verify_type_1}; -use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; - -#[global_allocator] -static ALLOC: ZkAllocator = ZkAllocator; - -#[test] -#[allow(clippy::redundant_clone)] -fn test_aggregation_with_zk_alloc() { - setup_prover(); - - let log_inv_rate = 2; - let message = message_for_benchmark(); - let slot: u32 = BENCHMARK_SLOT; - let signatures = get_benchmark_signatures(); - let raw_xmss = signatures[0..6].to_vec(); - - // Run TWO phases. The first phase lazily initializes anything that allocates - // during proving (notably the `parallel` thread pool); the second `begin_phase()` - // resets the arena slabs. Persistent state created in phase 1 must survive that - // reset — a single-phase test would not catch a regression where it doesn't. - for _ in 0..2 { - begin_phase(); - let aggregated = aggregate_type_1(&[], raw_xmss.clone(), message, slot, log_inv_rate).unwrap(); - end_phase(); - // IMPORTANT: clone to move the data out of the arena memory - let aggregated = aggregated.clone(); - - verify_type_1(&aggregated).unwrap(); - } -} From 29bb74b0ae5363b611a9148e33d5797ec905ba1f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 20:09:31 +0400 Subject: [PATCH 19/65] poseidon: stack-allocate per-row column pointer arrays in trace gen Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/lean_vm/src/tables/poseidon/trace_gen.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/lean_vm/src/tables/poseidon/trace_gen.rs b/crates/lean_vm/src/tables/poseidon/trace_gen.rs index 14f1a785d..a5756e492 100644 --- a/crates/lean_vm/src/tables/poseidon/trace_gen.rs +++ b/crates/lean_vm/src/tables/poseidon/trace_gen.rs @@ -18,12 +18,12 @@ pub fn fill_trace_poseidon_16(trace: &mut [Vec]) { let m = n - (n % packing_width::()); let trace_packed: Vec<_> = trace.iter().map(|col| FPacking::::pack_slice(&col[..m])).collect(); + const N_COLS: usize = super::num_cols_poseidon_16(); + // fill the packed rows parallel::for_each_index(m / packing_width::(), |i| { - let ptrs: Vec<*mut FPacking> = trace_packed - .iter() - .map(|col| unsafe { (col.as_ptr() as *mut FPacking).add(i) }) - .collect(); + let ptrs: [*mut FPacking; N_COLS] = + std::array::from_fn(|c| unsafe { (trace_packed[c].as_ptr() as *mut FPacking).add(i) }); let perm: &mut Poseidon1Cols16<&mut FPacking> = unsafe { &mut *(ptrs.as_ptr() as *mut Poseidon1Cols16<&mut FPacking>) }; @@ -32,10 +32,7 @@ pub fn fill_trace_poseidon_16(trace: &mut [Vec]) { // fill the remaining rows (non packed) for i in m..n { - let ptrs: Vec<*mut F> = trace - .iter() - .map(|col| unsafe { (col.as_ptr() as *mut F).add(i) }) - .collect(); + let ptrs: [*mut F; N_COLS] = std::array::from_fn(|c| unsafe { (trace[c].as_ptr() as *mut F).add(i) }); let perm: &mut Poseidon1Cols16<&mut F> = unsafe { &mut *(ptrs.as_ptr() as *mut Poseidon1Cols16<&mut F>) }; generate_trace_rows_for_perm(perm); } From c7af020ae3ff422c6a7d5eb5cc5edeeeaf49e645 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 20:10:12 +0400 Subject: [PATCH 20/65] whir: stack-allocate single-element dimensions array in verify Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/whir/src/verify.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/whir/src/verify.rs b/crates/whir/src/verify.rs index 7e607a3d9..53ed173f7 100644 --- a/crates/whir/src/verify.rs +++ b/crates/whir/src/verify.rs @@ -251,7 +251,7 @@ where // dbg!(&stir_challenges_indexes); // dbg!(verifier_state.challenger().state()); - let dimensions = vec![Dimensions { + let dimensions = [Dimensions { height: params.domain_size >> params.folding_factor, width: 1 << params.folding_factor, }]; From 6eabeeff21d188bfa9a834e40f100ed3a37b12cf Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 20:25:50 +0400 Subject: [PATCH 21/65] sumcheck: reuse per-worker point/rows scratch in fold+split-eq compute paths Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/sumcheck/src/sc_computation.rs | 260 +++++++++--------- 1 file changed, 131 insertions(+), 129 deletions(-) diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index 0a6d02ed3..2c0f24c56 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -57,25 +57,6 @@ where } } -fn parallel_sum(size: usize, n: usize, compute_iteration: F) -> Vec -where - T: PrimeCharacteristicRing + Send + Sync, - F: Fn(usize) -> Vec + Sync + Send, -{ - let accumulate = |mut acc: Vec, sums: Vec| { - for (j, sum) in sums.into_iter().enumerate() { - acc[j] += sum; - } - acc - }; - - if size < PARALLEL_THRESHOLD { - (0..size).fold(T::zero_vec(n), |acc, i| accumulate(acc, compute_iteration(i))) - } else { - parallel::map_reduce(size, || T::zero_vec(n), compute_iteration, accumulate) - } -} - fn build_evals>>( sums: impl IntoIterator, missing_mul_factor: Option, @@ -206,7 +187,7 @@ where extra_data, missing_mul_factor, packed_fold_size, - |sc, pf, ed| sc.eval_packed_extension(&pf, ed), + |sc, pf, ed| sc.eval_packed_extension(pf, ed), packing_unpack_sum, ) } @@ -312,7 +293,7 @@ where missing_mul_factor, compute_fold_size, |m, id| (m[id + prev_folded_size] - m[id]) * prev_folding_factor + m[id], - |sc, pf, ed| sc.eval_packed_extension(&pf, ed), + |sc, pf, ed| sc.eval_packed_extension(pf, ed), packing_unpack_sum, MleGroupOwned::ExtensionPacked, ) @@ -328,7 +309,7 @@ where missing_mul_factor, compute_fold_size, |m, id| (m[id + prev_folded_size] - m[id]) * prev_folding_factor + m[id], - |sc, pf, ed| sc.eval_packed_extension(&pf, ed), + |sc, pf, ed| sc.eval_packed_extension(pf, ed), packing_unpack_sum, MleGroupOwned::ExtensionPacked, ) @@ -351,7 +332,7 @@ where missing_mul_factor, compute_fold_size, |m, id| prev_folding_factor_packed * (m[id + prev_folded_size] - m[id]) + m[id], - |sc, pf, ed| sc.eval_packed_extension(&pf, ed), + |sc, pf, ed| sc.eval_packed_extension(pf, ed), packing_unpack_sum, MleGroupOwned::ExtensionPacked, ) @@ -367,7 +348,7 @@ where missing_mul_factor, compute_fold_size, |m, id| prev_folding_factor * (m[id + prev_folded_size] - m[id]) + m[id], - |sc, pf, ed| sc.eval_extension(&pf, ed), + |sc, pf, ed| sc.eval_extension(pf, ed), |s| s, MleGroupOwned::Extension, ) @@ -383,7 +364,7 @@ where missing_mul_factor, compute_fold_size, |m, id| (m[id + prev_folded_size] - m[id]) * prev_folding_factor + m[id], - |sc, pf, ed| sc.eval_extension(&pf, ed), + |sc, pf, ed| sc.eval_extension(pf, ed), |s| s, MleGroupOwned::Extension, ) @@ -480,7 +461,7 @@ fn sumcheck_fold_and_compute_core( missing_mul_factor: Option, compute_fold_size: usize, fold_f: impl Fn(&[IF], usize) -> FT + Sync + Send, - eval_fn: impl Fn(&SC, Vec, &SC::ExtraData) -> FT + Sync + Send, + eval_fn: impl Fn(&SC, &[FT], &SC::ExtraData) -> FT + Sync + Send, unpack_sum: impl Fn(FT) -> EF, wrap_f: impl FnOnce(Vec>) -> MleGroupOwned, ) -> (Vec, MleGroupOwned) @@ -496,13 +477,20 @@ where .map(|_| FT::zero_vec(prev_folded_size)) .collect(); - let compute_iteration = |i: usize| -> Vec { - let eq_mle_eval = eq_at(i); - - let mut rows_f: Vec<[FT; 3]> = multilinears - .iter() - .enumerate() - .map(|(j, m)| { + // Per-worker scratch: `rows_f` (the [lo, diff, hi] triples) and `point` (the + // evaluation point handed to `eval_fn`) are reused across every task a worker + // owns, so the hot loop allocates nothing. `acc` (length `degree`) is the + // per-worker partial sum. + let n_mult = multilinears.len(); + let sums = parallel::map_reduce_with_state( + compute_fold_size, + || (Vec::<[FT; 3]>::with_capacity(n_mult), Vec::::with_capacity(n_mult)), + || FT::zero_vec(degree), + |(rows_f, point), acc, i| { + let eq_mle_eval = eq_at(i); + + rows_f.clear(); + rows_f.extend(multilinears.iter().enumerate().map(|(j, m)| { let lo = fold_f(m, i); let hi = fold_f(m, i + compute_fold_size); unsafe { @@ -510,37 +498,39 @@ where *ptr.add(i) = lo; *ptr.add(i + compute_fold_size) = hi; } - let diff_hi_lo = hi - lo; - [lo, diff_hi_lo, hi] - }) - .collect(); - - // z = 0 - let point_0 = rows_f.iter().map(|row| row[0]).collect::>(); - let mut eval_0 = eval_fn(computation, point_0, extra_data); - if let Some(eq) = eq_mle_eval { - eval_0 *= eq; - } - - let mut evals = Vec::with_capacity(degree); - evals.push(eval_0); + [lo, hi - lo, hi] + })); - // z = 2, 3, ... - for _ in 1..degree { - for [_, diff_hi_lo, running] in &mut rows_f { - *running += *diff_hi_lo; - } - let point_f = rows_f.iter().map(|row| row[2]).collect::>(); - let mut eval = eval_fn(computation, point_f, extra_data); + // z = 0 + point.clear(); + point.extend(rows_f.iter().map(|row| row[0])); + let mut eval_0 = eval_fn(computation, point, extra_data); if let Some(eq) = eq_mle_eval { - eval *= eq; + eval_0 *= eq; } - evals.push(eval); - } - evals - }; + acc[0] += eval_0; - let sums = parallel_sum(compute_fold_size, degree, compute_iteration); + // z = 2, 3, ... + for acc_d in acc.iter_mut().skip(1) { + for [_, diff_hi_lo, running] in rows_f.iter_mut() { + *running += *diff_hi_lo; + } + point.clear(); + point.extend(rows_f.iter().map(|row| row[2])); + let mut eval = eval_fn(computation, point, extra_data); + if let Some(eq) = eq_mle_eval { + eval *= eq; + } + *acc_d += eval; + } + }, + |mut a: Vec, b: Vec| { + for (x, y) in a.iter_mut().zip(b) { + *x += y; + } + a + }, + ); let unpacked_sums = sums.into_iter().map(&unpack_sum); (build_evals(unpacked_sums, missing_mul_factor), wrap_f(folded_f)) } @@ -554,7 +544,7 @@ fn sumcheck_compute_with_split_eq( extra_data: &SC::ExtraData, missing_mul_factor: Option, fold_size: usize, - eval_fn: impl Fn(&SC, Vec>, &SC::ExtraData) -> EFPacking + Sync + Send, + eval_fn: impl Fn(&SC, &[EFPacking], &SC::ExtraData) -> EFPacking + Sync + Send, unpack_sum: impl Fn(EFPacking) -> EF, ) -> Vec where @@ -567,58 +557,64 @@ where let eq_lo = &split_eq.eq_lo; let eq_hi = &split_eq.eq_hi_packed; - let zero = || EFPacking::::zero_vec(degree); - let accumulate = |mut acc: Vec>, vals: Vec>| -> Vec> { - for (a, v) in acc.iter_mut().zip(vals.iter()) { - *a += *v; - } - acc - }; - - let sums: Vec> = parallel::map_reduce( + // Per-worker scratch reused across every `b_lo` task: `rows` ([lo, diff, hi] + // triples), `point` (handed to `eval_fn`), and `block_acc` (per-`b_lo` partial + // sum, scaled by `eq_lo` before folding into the worker accumulator `acc`). + let n_mult = multilinears.len(); + let sums: Vec> = parallel::map_reduce_with_state( n_lo, - zero, - |b_lo| { + || { + ( + Vec::<[EFPacking; 3]>::with_capacity(n_mult), + Vec::>::with_capacity(n_mult), + EFPacking::::zero_vec(degree), + ) + }, + || EFPacking::::zero_vec(degree), + |(rows, point, block_acc), acc, b_lo| { let eq_lo_bc = EFPacking::::from(eq_lo[b_lo]); let base = b_lo << log_packed_hi; - let mut block_acc = zero(); + block_acc.iter_mut().for_each(|x| *x = EFPacking::::ZERO); for k in 0..packed_hi { let i = base + k; let eq_val = eq_hi[k]; - let mut rows = multilinears - .iter() - .map(|m| { - let lo = m[i]; - let hi = m[i + fold_size]; - let diff = hi - lo; - [lo, diff, hi] - }) - .collect::>(); + rows.clear(); + rows.extend(multilinears.iter().map(|m| { + let lo = m[i]; + let hi = m[i + fold_size]; + [lo, hi - lo, hi] + })); // z = 0 - let p0 = rows.iter().map(|r| r[0]).collect(); - let mut e0 = eval_fn(computation, p0, extra_data); + point.clear(); + point.extend(rows.iter().map(|r| r[0])); + let mut e0 = eval_fn(computation, point, extra_data); e0 *= eq_val; block_acc[0] += e0; // z = 2, 3, ... for d in 1..degree { - for [_, diff, running] in &mut rows { + for [_, diff, running] in rows.iter_mut() { *running += *diff; } - let pf = rows.iter().map(|r| r[2]).collect(); - let mut ev = eval_fn(computation, pf, extra_data); + point.clear(); + point.extend(rows.iter().map(|r| r[2])); + let mut ev = eval_fn(computation, point, extra_data); ev *= eq_val; block_acc[d] += ev; } } - for a in &mut block_acc { - *a *= eq_lo_bc; + for (a, b) in acc.iter_mut().zip(block_acc.iter()) { + *a += *b * eq_lo_bc; + } + }, + |mut a: Vec>, b: Vec>| { + for (x, y) in a.iter_mut().zip(b) { + *x += y; } - block_acc + a }, - accumulate, ); let unpacked = sums.into_iter().map(&unpack_sum); @@ -636,7 +632,7 @@ fn sumcheck_fold_and_compute_with_split_eq( missing_mul_factor: Option, compute_fold_size: usize, fold_f: impl Fn(&[IF], usize) -> EFPacking + Sync + Send, - eval_fn: impl Fn(&SC, Vec>, &SC::ExtraData) -> EFPacking + Sync + Send, + eval_fn: impl Fn(&SC, &[EFPacking], &SC::ExtraData) -> EFPacking + Sync + Send, unpack_sum: impl Fn(EFPacking) -> EF, wrap_f: impl FnOnce(Vec>>) -> MleGroupOwned, ) -> (Vec, MleGroupOwned) @@ -656,62 +652,68 @@ where let eq_lo = &split_eq.eq_lo; let eq_hi = &split_eq.eq_hi_packed; - let zero = || EFPacking::::zero_vec(degree); - let accumulate = |mut acc: Vec>, vals: Vec>| -> Vec> { - for (a, v) in acc.iter_mut().zip(vals.iter()) { - *a += *v; - } - acc - }; - - let sums: Vec> = parallel::map_reduce( + // Per-worker scratch reused across every `b_lo` task (see `sumcheck_compute_with_split_eq`): + // `rows_f` triples, `point` for `eval_fn`, and the per-`b_lo` `block_acc`. + let n_mult = multilinears.len(); + let sums: Vec> = parallel::map_reduce_with_state( n_lo, - zero, - |b_lo| { + || { + ( + Vec::<[EFPacking; 3]>::with_capacity(n_mult), + Vec::>::with_capacity(n_mult), + EFPacking::::zero_vec(degree), + ) + }, + || EFPacking::::zero_vec(degree), + |(rows_f, point, block_acc), acc, b_lo| { let eq_lo_bc = EFPacking::::from(eq_lo[b_lo]); let base = b_lo << log_packed_hi; - let mut block_acc = zero(); + block_acc.iter_mut().for_each(|x| *x = EFPacking::::ZERO); for k in 0..packed_hi { let i = base + k; let eq_val = eq_hi[k]; - let mut rows_f: Vec<[EFPacking; 3]> = multilinears - .iter() - .enumerate() - .map(|(j, m)| { - let lo = fold_f(m, i); - let hi = fold_f(m, i + compute_fold_size); - unsafe { - let ptr = folded_f[j].as_ptr() as *mut EFPacking; - *ptr.add(i) = lo; - *ptr.add(i + compute_fold_size) = hi; - } - let diff = hi - lo; - [lo, diff, hi] - }) - .collect(); - - let p0 = rows_f.iter().map(|r| r[0]).collect(); - let mut e0 = eval_fn(computation, p0, extra_data); + rows_f.clear(); + rows_f.extend(multilinears.iter().enumerate().map(|(j, m)| { + let lo = fold_f(m, i); + let hi = fold_f(m, i + compute_fold_size); + unsafe { + let ptr = folded_f[j].as_ptr() as *mut EFPacking; + *ptr.add(i) = lo; + *ptr.add(i + compute_fold_size) = hi; + } + [lo, hi - lo, hi] + })); + + // z = 0 + point.clear(); + point.extend(rows_f.iter().map(|r| r[0])); + let mut e0 = eval_fn(computation, point, extra_data); e0 *= eq_val; block_acc[0] += e0; + // z = 2, 3, ... for d in 1..degree { - for [_, diff, running] in &mut rows_f { + for [_, diff, running] in rows_f.iter_mut() { *running += *diff; } - let pf = rows_f.iter().map(|r| r[2]).collect(); - let mut ev = eval_fn(computation, pf, extra_data); + point.clear(); + point.extend(rows_f.iter().map(|r| r[2])); + let mut ev = eval_fn(computation, point, extra_data); ev *= eq_val; block_acc[d] += ev; } } - for a in &mut block_acc { - *a *= eq_lo_bc; + for (a, b) in acc.iter_mut().zip(block_acc.iter()) { + *a += *b * eq_lo_bc; } - block_acc }, - accumulate, + |mut a: Vec>, b: Vec>| { + for (x, y) in a.iter_mut().zip(b) { + *x += y; + } + a + }, ); let unpacked = sums.into_iter().map(&unpack_sum); From c594a796af51c20a7aa17753baeb5aaab0037e7b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 21:15:21 +0400 Subject: [PATCH 22/65] poseidon: bind fixed-size array ref to elide per-row bounds checks in trace gen Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/lean_vm/src/tables/poseidon/trace_gen.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/lean_vm/src/tables/poseidon/trace_gen.rs b/crates/lean_vm/src/tables/poseidon/trace_gen.rs index a5756e492..dc3963b75 100644 --- a/crates/lean_vm/src/tables/poseidon/trace_gen.rs +++ b/crates/lean_vm/src/tables/poseidon/trace_gen.rs @@ -20,10 +20,12 @@ pub fn fill_trace_poseidon_16(trace: &mut [Vec]) { const N_COLS: usize = super::num_cols_poseidon_16(); - // fill the packed rows + // fill the packed rows. Bind a fixed-size array ref so the per-row `array::from_fn` + // indexing elides bounds checks (one length check here, none in the hot loop). + let cols: &[&[FPacking]; N_COLS] = (&trace_packed[..N_COLS]).try_into().unwrap(); parallel::for_each_index(m / packing_width::(), |i| { let ptrs: [*mut FPacking; N_COLS] = - std::array::from_fn(|c| unsafe { (trace_packed[c].as_ptr() as *mut FPacking).add(i) }); + std::array::from_fn(|c| unsafe { (cols[c].as_ptr() as *mut FPacking).add(i) }); let perm: &mut Poseidon1Cols16<&mut FPacking> = unsafe { &mut *(ptrs.as_ptr() as *mut Poseidon1Cols16<&mut FPacking>) }; @@ -31,8 +33,9 @@ pub fn fill_trace_poseidon_16(trace: &mut [Vec]) { }); // fill the remaining rows (non packed) + let cols: &[Vec; N_COLS] = (&trace[..N_COLS]).try_into().unwrap(); for i in m..n { - let ptrs: [*mut F; N_COLS] = std::array::from_fn(|c| unsafe { (trace[c].as_ptr() as *mut F).add(i) }); + let ptrs: [*mut F; N_COLS] = std::array::from_fn(|c| unsafe { (cols[c].as_ptr() as *mut F).add(i) }); let perm: &mut Poseidon1Cols16<&mut F> = unsafe { &mut *(ptrs.as_ptr() as *mut Poseidon1Cols16<&mut F>) }; generate_trace_rows_for_perm(perm); } From 96c0ee9a9657ca7937dd9479e4357ca5ffe2dddc Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 19:29:40 +0200 Subject: [PATCH 23/65] alloc: disable THP for the prover (fixes ~50% intermittent slowdown on Zen4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On x86 Linux with physically-indexed L2/L3 (observed on Ryzen 8700GE / Zen4), the kernel promoting the allocator's large arenas to 2 MB transparent huge pages collapses the prover's strided multilinear/NTT array access into a few cache sets. Measured on `fancy-aggregation`: +217% cache-misses, IPC 0.85 -> 0.51, +60% cycles, +50% wall time (16s -> 24s). It was intermittent — THP only promotes when 2 MB-contiguous memory is free, so a machine fragmented by prior build/benchmark churn never triggered it, while a fresher one reliably did. The old zk-alloc bump arena never hit this (different layout/access); Apple silicon has no THP, so M4 Max was always fast. tune_allocator() now calls prctl(PR_SET_THP_DISABLE, 1) on Linux — process-local, overrides even a system-wide THP=always, no-op off Linux. Confirmed: THP-backed HEAD 24.09s -> 16.09s with the prctl. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 1 + Cargo.toml | 1 + src/lib.rs | 25 ++++++++++++++++++++----- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f7248a2e..0bb8c2bce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,7 @@ dependencies = [ "backend", "clap", "lean_vm", + "libc", "libmimalloc-sys", "mimalloc", "rand", diff --git a/Cargo.toml b/Cargo.toml index 229c6f896..ce778c312 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ clap.workspace = true rec_aggregation.workspace = true mimalloc = "0.1" libmimalloc-sys = { version = "0.1", features = ["extended"] } +libc = "0.2" rand.workspace = true sub_protocols.workspace = true utils.workspace = true diff --git a/src/lib.rs b/src/lib.rs index 1ee869d1f..79234dccc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,12 +10,22 @@ pub use xmss::{MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, xmss pub type F = KoalaBear; -/// Tune the default (mimalloc) allocator for the prover's churn of huge short-lived buffers. +/// Tune the allocator and VM memory policy for the prover's churn of huge buffers. /// -/// Disables purging so freed large blocks are **retained** rather than returned to the OS -/// and re-faulted on the next allocation. This is what made the old bump arena fast (it -/// reused the same pages); mimalloc-with-retention matches *and beats* it here, without the -/// arena's fragility. Idempotent; call before any heavy proving allocation. +/// Two things, both before any heavy proving allocation (idempotent): +/// +/// 1. **Disable mimalloc purging** so freed large blocks are *retained* rather than returned +/// to the OS and re-faulted on the next allocation — what made the old bump arena fast +/// (page reuse); mimalloc-with-retention matches and beats it. +/// +/// 2. **Disable Transparent Huge Pages for this process.** On Zen4 (and likely other x86 with +/// physically-indexed L2/L3), when the kernel promotes the allocator's large arenas to +/// 2 MB huge pages, the prover's strided multilinear/NTT array access collapses into a few +/// cache sets — measured **+217% cache-misses, IPC 0.85 → 0.51, +50% wall time** on +/// `fancy-aggregation`. It's intermittent (only fires when 2 MB-contiguous memory is free +/// for THP promotion), which is what made it so hard to pin down. `prctl(PR_SET_THP_DISABLE)` +/// is process-local and overrides even a system-wide `THP=always`. No-op off Linux (macOS +/// has no THP — Apple silicon was never affected). Applies under any allocator. pub fn tune_allocator() { // mimalloc v3 option index `mi_option_purge_delay` = 15; value -1 = never purge // (equivalent to `MIMALLOC_PURGE_DELAY=-1`). No-op under the `standard-alloc` (plain @@ -24,6 +34,11 @@ pub fn tune_allocator() { unsafe { libmimalloc_sys::mi_option_set(15, -1); } + // Keep allocator arenas on 4 KB pages (see point 2 above). + #[cfg(target_os = "linux")] + unsafe { + libc::prctl(libc::PR_SET_THP_DISABLE, 1, 0, 0, 0); + } } /// Call once before proving. Compiles the aggregation program and precomputes DFT twiddles. From cb3957373bf678112c129c469d19f8add81cdfa2 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 20:26:36 +0200 Subject: [PATCH 24/65] parallel: centralize per-worker-slot unsafe; tidy dispatch Pure cleanup of the pool, no behavior change (pool unit tests + testall green, fancy-aggregation timing unchanged): - map_reduce / map_reduce_with_state duplicated the same cross-worker slot dance (SendPtr + per-call slots Vec + worker-id lookup). Extract it into one `drain_into_slots` helper that is now the single home of that `unsafe`; both reducers become trivial readers over it. - map_reduce folds each batch into a local accumulator and touches the shared slot only at the ends, matching map_reduce_with_state and keeping per-element writes off the cross-worker pointer. - Replace the self-referential `transmute::, NonNull>` with a plain lifetime-erasing `transmute(NonNull::from(..))` driven by the binding type. - Fix stale `Job` doc (`Fn(usize)` -> `Fn(usize, usize)`). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/parallel/src/lib.rs | 82 +++++++++++++----------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 7f05e86e5..0d522ad9e 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -90,8 +90,8 @@ pub fn current_worker_id() -> usize { WORKER_ID.with(Cell::get) } -/// Type-erased unit of work: a `&(dyn Fn(usize) + Sync)` whose lifetime is erased to -/// `'static`. Only dereferenced inside a dispatch window during which the dispatcher +/// Type-erased unit of work: a `&(dyn Fn(usize, usize) + Sync)` whose lifetime is erased +/// to `'static`. Only dereferenced inside a dispatch window during which the dispatcher /// blocks, so the source borrow outlives every call. struct Job { /// Range-based work: `f(start, end)` processes the half-open task range `start..end`. @@ -270,15 +270,11 @@ pub fn for_each_chunk(n_tasks: usize, f: F) { let _guard = pool.dispatch.lock().unwrap(); let n = NUM_THREADS; + // SAFETY: erase the borrow's lifetime so the closure can live in the `'static` + // `Job`. The dispatcher blocks on `working` below before returning, so `f` + // outlives every worker dereference of this pointer. let f_ref: &(dyn Fn(usize, usize) + Sync) = &f; - // SAFETY: erase the borrow's lifetime to store in the 'static `Job`. The - // dispatcher spins on `working` below before returning, so `f` outlives every - // worker call that dereferences this pointer. - let f_erased: NonNull = unsafe { - std::mem::transmute::, NonNull>( - NonNull::from(f_ref), - ) - }; + let f_erased: NonNull = unsafe { std::mem::transmute(NonNull::from(f_ref)) }; // SAFETY: all workers finished the previous dispatch (we waited for `working == 0`) // and none observes this one until the generation bump, so we are the sole writer. @@ -351,6 +347,23 @@ where }); } +/// Give each worker exclusive, persistent access to its own `Option` slot while it +/// drains `0..n_tasks`: `run(slot, start, end)` is called once per claimed batch, always +/// with the same slot for a given worker, so state accumulates across the batches it +/// claims. Returns the per-worker slots (one per worker that ran, rest `None`) for the +/// caller to combine. This is the single home of the cross-worker slot `unsafe`. +fn drain_into_slots(n_tasks: usize, run: impl Fn(&mut Option, usize, usize) + Sync) -> Vec> { + let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); + let ptr = SendPtr(slots.as_mut_ptr()); + for_each_chunk(n_tasks, |start, end| { + // SAFETY: `current_worker_id()` is unique per live worker and < NUM_THREADS, so + // workers touch disjoint slots; `slots` outlives the dispatch. + let slot = unsafe { &mut *ptr.add(current_worker_id()) }; + run(slot, start, end); + }); + slots +} + /// Parallel map-reduce over `0..n_tasks`, equivalent to /// `(0..n_tasks).into_par_iter().map(map).reduce(identity, reduce)`. /// @@ -365,32 +378,19 @@ where R: Fn(T, T) -> T + Sync, { if NUM_THREADS <= 1 || n_tasks <= 1 { - let mut acc = identity(); - for i in 0..n_tasks { - acc = reduce(acc, map(i)); - } - return acc; + return (0..n_tasks).fold(identity(), |acc, i| reduce(acc, map(i))); } - - let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); - let ptr = SendPtr(slots.as_mut_ptr()); - - for_each_chunk(n_tasks, |start, end| { - // One worker-slot lookup per claimed batch, then fold the whole range into it — - // amortizing the thread-local read over the batch instead of paying it per element. - let wid = current_worker_id(); - // SAFETY: `wid` is unique per live worker and < NUM_THREADS; slots disjoint; - // `slots` outlives the dispatch. - let slot = unsafe { &mut *ptr.add(wid) }; - for i in start..end { - let v = map(i); - *slot = Some(match slot.take() { - Some(acc) => reduce(acc, v), - None => v, - }); - } + let slots = drain_into_slots(n_tasks, |slot, start, end| { + // Fold the batch into the worker's accumulator, taking/replacing the shared slot + // just once so per-element writes stay off the cross-worker pointer. + let acc = (start..end).fold(slot.take(), |acc, i| { + Some(match acc { + Some(a) => reduce(a, map(i)), + None => map(i), + }) + }); + *slot = acc; }); - slots.into_iter().flatten().fold(identity(), &reduce) } @@ -416,23 +416,13 @@ where } return acc; } - - let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); - let ptr = SendPtr(slots.as_mut_ptr()); - - for_each_chunk(n_tasks, |start, end| { - // One worker-slot lookup per claimed batch; the reusable scratch and accumulator - // are then threaded through every element of the range. - let wid = current_worker_id(); - // SAFETY: `wid` unique per live worker and < NUM_THREADS; slots disjoint; - // `slots` outlives the dispatch. - let slot = unsafe { &mut *ptr.add(wid) }; + let slots = drain_into_slots(n_tasks, |slot, start, end| { + // Scratch and accumulator are created once and threaded through every batch. let (state, acc) = slot.get_or_insert_with(|| (init_state(), init_acc())); for i in start..end { fold(state, acc, i); } }); - slots .into_iter() .flatten() From c7d68838c04652d8d714ce4bb49c666fb33aae2c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 20:44:33 +0200 Subject: [PATCH 25/65] parallel: centralize SendPtr + chunk-size heuristic across the workspace Codebase-wide dedup of the two threading idioms that had been copy-pasted everywhere. No behavior change (testall green, fancy-aggregation unchanged). - `recommended_chunk_size(n)` in the pool replaces 15 hand-rolled copies of `n.div_ceil(num_threads() * 4).max(1)` scattered across 9 crates, plus the redundant `poly::par_chunk_size` wrapper. One named home for the heuristic. - `SendPtr` (the pool's "share a *mut across a dispatch" wrapper) is now public and is the single definition of that pattern; deletes the two identical `SyncMutPtr` redefinitions in sub_protocols and the sumcheck crate. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/parallel/src/lib.rs | 28 +++++++++++++++---- crates/backend/poly/src/utils.rs | 18 ++++-------- .../sumcheck/src/product_computation.rs | 20 ++----------- crates/lean_compiler/src/c_compile_final.rs | 2 +- crates/lean_prover/src/trace_gen.rs | 2 +- crates/sub_protocols/src/logup.rs | 2 +- .../sub_protocols/src/quotient_gkr/layers.rs | 25 +++-------------- .../src/quotient_gkr/sumcheck_utils.rs | 10 +++---- crates/utils/src/multilinear.rs | 2 +- crates/whir/src/open.rs | 2 +- crates/whir/src/utils.rs | 4 +-- crates/xmss/src/signers_cache.rs | 2 +- crates/xmss/src/xmss.rs | 4 +-- 13 files changed, 49 insertions(+), 72 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 0d522ad9e..c85f66e0f 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -74,6 +74,16 @@ pub const fn num_threads() -> usize { NUM_THREADS } +/// The standard chunk size for a flat fan-out of `n_items` over the pool: a handful of +/// chunks per worker so the atomic counter can rebalance heterogeneous cores, while +/// staying coarse enough to amortize dispatch. The canonical divisor for `par_chunks_mut` +/// — prefer this over hand-rolling `n.div_ceil(num_threads() * 4)` at each call site. +#[must_use] +#[inline] +pub fn recommended_chunk_size(n_items: usize) -> usize { + n_items.div_ceil(NUM_THREADS * 4).max(1) +} + thread_local! { /// Stable id of this thread within the pool. Set once per background worker; /// stays `0` on the dispatching thread (worker 0) and on any non-worker thread. @@ -312,16 +322,24 @@ pub fn for_each_index(n_tasks: usize, f: F) { }); } -/// Wrapper holding a raw base pointer that is safe to share across workers because -/// each worker only touches a disjoint sub-slice computed from its task index. -struct SendPtr(*mut T); +/// A raw base pointer made shareable across pool workers. Sound only because every +/// caller partitions the backing allocation by task index, so each worker touches a +/// disjoint region. This is the one home for the "share a `*mut` across a dispatch" +/// pattern — reach for it (with `par_chunks_mut`) instead of redefining it per crate. +#[derive(Debug)] +pub struct SendPtr(pub *mut T); // SAFETY: accesses are partitioned by task index (see callers). unsafe impl Send for SendPtr {} unsafe impl Sync for SendPtr {} impl SendPtr { - /// SAFETY: `n` must keep the result within the original allocation. - unsafe fn add(&self, n: usize) -> *mut T { + /// Offset the base pointer by `n` elements. + /// + /// # Safety + /// `n` must keep the result within the original allocation, and any write through it + /// must target a slot no other concurrent task touches. + #[inline] + pub unsafe fn add(&self, n: usize) -> *mut T { unsafe { self.0.add(n) } } } diff --git a/crates/backend/poly/src/utils.rs b/crates/backend/poly/src/utils.rs index 97d6366c7..200454d5b 100644 --- a/crates/backend/poly/src/utils.rs +++ b/crates/backend/poly/src/utils.rs @@ -9,14 +9,6 @@ use crate::{EFPacking, PF, PFPacking}; pub const PARALLEL_THRESHOLD: usize = 1 << 9; -/// Number of elements each pool task should handle in a flat fan-out. Aims for a few -/// chunks per worker so the pool's atomic counter can load-balance heterogeneous cores, -/// while keeping chunks coarse enough to amortize dispatch. -#[inline] -fn par_chunk_size(n_items: usize) -> usize { - n_items.div_ceil(parallel::num_threads() * 4).max(1) -} - pub fn pack_extension>>(slice: &[EF]) -> Vec> { let width = packing_width::(); let n_packed = slice.len() / width; @@ -29,7 +21,7 @@ pub fn pack_extension>>(slice: &[EF]) -> Vec>>(vec: &[EFPacking]) -> Ve } else { // One pool task per group of `group` packed elements, each writing `group * width` // contiguous output scalars from a disjoint slice of `vec`. - let group = par_chunk_size(vec.len()); + let group = parallel::recommended_chunk_size(vec.len()); parallel::par_chunks_mut(&mut out, group * width, |ci, out_chunk| { for (k, sub) in out_chunk.chunks_exact_mut(width).enumerate() { write(sub, &vec[ci * group + k]); @@ -123,7 +115,7 @@ pub fn fold_multilinear_lsb< if new_size < PARALLEL_THRESHOLD { m.chunks_exact(2).zip(res.iter_mut()).for_each(compute); } else { - let chunk = par_chunk_size(new_size); + let chunk = parallel::recommended_chunk_size(new_size); parallel::par_chunks_mut(&mut res, chunk, |ci, res_chunk| { for (k, r_v) in res_chunk.iter_mut().enumerate() { let j = ci * chunk + k; @@ -169,7 +161,7 @@ pub fn fold_multilinear_at_bit< *res_v = compute(new_j); } } else { - let chunk = par_chunk_size(new_size); + let chunk = parallel::recommended_chunk_size(new_size); parallel::par_chunks_mut(&mut res, chunk, |ci, res_chunk| { for (k, res_v) in res_chunk.iter_mut().enumerate() { *res_v = compute(ci * chunk + k); @@ -197,7 +189,7 @@ pub fn fold_multilinear< res[i] = mul_if_of(m[i + new_size] - m[i], alpha) + m[i]; } } else { - let chunk = par_chunk_size(new_size); + let chunk = parallel::recommended_chunk_size(new_size); parallel::par_chunks_mut(&mut res, chunk, |ci, res_chunk| { for (k, res_v) in res_chunk.iter_mut().enumerate() { let i = ci * chunk + k; diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index 7c4a3d8ed..4f93ad176 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -5,22 +5,6 @@ use tracing::instrument; use crate::{SumcheckComputation, sumcheck_prove_many_rounds}; -/// Raw mutable base pointer shareable across pool tasks; each task writes only the -/// disjoint folded slots computed from its index range. -struct SyncMutPtr(*mut T); -// SAFETY: writes are partitioned by task index (see `fold_and_compute_*`). -unsafe impl Send for SyncMutPtr {} -unsafe impl Sync for SyncMutPtr {} - -impl SyncMutPtr { - /// SAFETY: `n` must keep the result within the original allocation, and writes - /// through it must target slots no other task touches. - #[inline] - unsafe fn add(&self, n: usize) -> *mut T { - unsafe { self.0.add(n) } - } -} - #[derive(Debug)] pub struct ProductComputation; @@ -308,8 +292,8 @@ pub fn fold_and_compute_product_sumcheck_polynomial< // (writing the disjoint `i` / `quarter + i` output slots) and accumulate the // per-index quadratic straight into the worker's `(c0, c2)` — no per-chunk tuple. let quarter = n / 4; - let p0f = SyncMutPtr(pol_0_folded.as_mut_ptr()); - let p1f = SyncMutPtr(pol_1_folded.as_mut_ptr()); + let p0f = parallel::SendPtr(pol_0_folded.as_mut_ptr()); + let p1f = parallel::SendPtr(pol_1_folded.as_mut_ptr()); parallel::map_reduce_with_state( quarter, || (), diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index a09519dd4..b2676ce6c 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -134,7 +134,7 @@ pub fn compile_to_low_level_bytecode( let mut instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]> = unsafe { uninitialized_vec(instructions.len()) }; { - let chunk = instructions.len().div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(instructions.len()); parallel::par_chunks_mut(&mut instructions_encoded, chunk, |ci, sub| { for (k, out) in sub.iter_mut().enumerate() { *out = field_representation(&instructions[ci * chunk + k]); diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 17b882f99..31d44ec94 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -95,7 +95,7 @@ pub fn get_execution_trace( let mut memory_padded: Vec = unsafe { uninitialized_vec(memory.0.len()) }; { - let chunk = memory_padded.len().div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(memory_padded.len()); parallel::par_chunks_mut(&mut memory_padded, chunk, |ci, sub| { for (k, slot) in sub.iter_mut().enumerate() { *slot = memory.0[ci * chunk + k].unwrap_or(F::ZERO); diff --git a/crates/sub_protocols/src/logup.rs b/crates/sub_protocols/src/logup.rs index 57830351d..7c5e3e750 100644 --- a/crates/sub_protocols/src/logup.rs +++ b/crates/sub_protocols/src/logup.rs @@ -538,7 +538,7 @@ where /// global index. Replaces the rayon `par_iter_mut().enumerate()` constant/index fills. #[inline] fn par_fill T + Sync>(dst: &mut [T], build: Build) { - let chunk = dst.len().div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(dst.len()); parallel::par_chunks_mut(dst, chunk, |ci, sub| { for (k, slot) in sub.iter_mut().enumerate() { *slot = build(ci * chunk + k); diff --git a/crates/sub_protocols/src/quotient_gkr/layers.rs b/crates/sub_protocols/src/quotient_gkr/layers.rs index 0c9245b26..d9b5c482c 100644 --- a/crates/sub_protocols/src/quotient_gkr/layers.rs +++ b/crates/sub_protocols/src/quotient_gkr/layers.rs @@ -3,23 +3,6 @@ use std::borrow::Cow; use backend::*; -/// Raw mutable base pointer shareable across pool tasks; each task writes only the -/// disjoint slot computed from its index (the paired second output buffer of the -/// quotient-fold kernels, while the pool iterates the first via `par_chunks_mut`). -pub(super) struct SyncMutPtr(pub *mut T); -// SAFETY: writes are partitioned by task index (see `sum_quotients_*`). -unsafe impl Send for SyncMutPtr {} -unsafe impl Sync for SyncMutPtr {} - -impl SyncMutPtr { - /// SAFETY: `n` must stay within the original allocation and target a slot no - /// other task writes. - #[inline] - pub(super) unsafe fn add(&self, n: usize) -> *mut T { - unsafe { self.0.add(n) } - } -} - pub(super) enum LayerStorage<'a, EF: ExtensionField>> { Initial { nums: Cow<'a, [PFPacking]>, @@ -147,8 +130,8 @@ fn sum_quotients_2_by_2>>(nums: &[EF], dens: &[EF]) -> let mut new_dens: Vec = unsafe { uninitialized_vec(new_active) }; { - let dp = SyncMutPtr(new_dens.as_mut_ptr()); - let chunk = full_pairs.div_ceil(parallel::num_threads() * 4).max(1); + let dp = parallel::SendPtr(new_dens.as_mut_ptr()); + let chunk = parallel::recommended_chunk_size(full_pairs); parallel::par_chunks_mut(&mut new_nums[..full_pairs], chunk, |ci, num_chunk| { for (k, num) in num_chunk.iter_mut().enumerate() { let i = ci * chunk + k; @@ -193,8 +176,8 @@ where let mut new_dens: Vec> = unsafe { uninitialized_vec(nums.len() >> 1) }; { - let dp = SyncMutPtr(new_dens.as_mut_ptr()); - let chunk = new_nums.len().div_ceil(parallel::num_threads() * 4).max(1); + let dp = parallel::SendPtr(new_dens.as_mut_ptr()); + let chunk = parallel::recommended_chunk_size(new_nums.len()); parallel::par_chunks_mut(&mut new_nums, chunk, |ci, num_chunk| { for (k, num_out) in num_chunk.iter_mut().enumerate() { let new_j = ci * chunk + k; diff --git a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs index 18ec8e9c7..c299cfb20 100644 --- a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs +++ b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs @@ -5,7 +5,7 @@ use std::{ use backend::*; -use crate::quotient_gkr::layers::{SyncMutPtr, unpack_and_unreverse_active}; +use crate::quotient_gkr::layers::unpack_and_unreverse_active; pub(super) fn even_odd_split(v: &[T]) -> (Vec, Vec) { ( @@ -360,7 +360,7 @@ pub(super) fn run_phase2_sumcheck>>( let fold_eq = |i: usize| eq_table[2 * i] + eq_table[2 * i + 1]; eq_table = if new_eq_len >= PARALLEL_THRESHOLD { let mut out: Vec = unsafe { uninitialized_vec(new_eq_len) }; - let chunk = new_eq_len.div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(new_eq_len); parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { for (k, slot) in sub.iter_mut().enumerate() { *slot = fold_eq(ci * chunk + k); @@ -396,7 +396,7 @@ fn fold_normal_with_padding>>(m: &[EF], r: EF, pad_val if new_active < PARALLEL_THRESHOLD { out.iter_mut().enumerate().for_each(compute); } else { - let chunk = new_active.div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(new_active); parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { for (k, slot) in sub.iter_mut().enumerate() { compute((ci * chunk + k, slot)); @@ -482,8 +482,8 @@ where let prev_r_packed: EFPacking = as From>::from(prev_r); let n_chunks = nums.len() / in_packed; - let nn = SyncMutPtr(new_nums.as_mut_ptr()); - let nd = SyncMutPtr(new_dens.as_mut_ptr()); + let nn = parallel::SendPtr(new_nums.as_mut_ptr()); + let nd = parallel::SendPtr(new_dens.as_mut_ptr()); let coeffs = parallel::map_reduce( n_chunks, RoundCoeffs::zero, diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 5ae33fd68..7a13faa88 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -15,7 +15,7 @@ pub fn multilinears_linear_combination, P: Borro assert!(pols.iter().all(|p| log2_strict_usize(p.borrow().len()) == n_vars)); let n = 1usize << n_vars; let mut out: Vec = unsafe { uninitialized_vec(n) }; - let chunk = n.div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(n); parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { for (k, slot) in sub.iter_mut().enumerate() { let i = ci * chunk + k; diff --git a/crates/whir/src/open.rs b/crates/whir/src/open.rs index 1dee5b196..9e1807521 100644 --- a/crates/whir/src/open.rs +++ b/crates/whir/src/open.rs @@ -598,7 +598,7 @@ where for (out_buff, &(origin_index, _)) in chunks_mut.iter_mut().zip(&indexed_smt_values) { let out = &mut out_buff[..1 << shift]; let scalar = next_gamma_powers[origin_index]; - let chunk = out.len().div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(out.len()); parallel::par_chunks_mut(out, chunk, |ci, sub| { for (k, out_elem) in sub.iter_mut().enumerate() { *out_elem += inner_poly[ci * chunk + k] * scalar; diff --git a/crates/whir/src/utils.rs b/crates/whir/src/utils.rs index e80b72cca..b06409597 100644 --- a/crates/whir/src/utils.rs +++ b/crates/whir/src/utils.rs @@ -138,7 +138,7 @@ fn prepare_evals_for_fft_unpacked( let out_len = block_size * dft_n_cols; let mut out: Vec = unsafe { uninitialized_vec(out_len) }; - let chunk = out_len.div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(out_len); parallel::par_chunks_mut(&mut out, chunk, |ci, out_chunk| { for (k, slot) in out_chunk.iter_mut().enumerate() { let i = ci * chunk + k; @@ -166,7 +166,7 @@ fn prepare_evals_for_fft_packed_extension>>( let packing_mask = (1 << log_packing) - 1; let mut out: Vec = unsafe { uninitialized_vec(full_len) }; - let chunk = full_len.div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(full_len); parallel::par_chunks_mut(&mut out, chunk, |ci, out_chunk| { for (k, slot) in out_chunk.iter_mut().enumerate() { let i = ci * chunk + k; diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 63cd67f12..85616bc5e 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -91,7 +91,7 @@ fn gen_benchmark_signers_cache() -> Vec<(XmssPublicKey, XmssSignature)> { let time = Instant::now(); let n_rest = NUM_BENCHMARK_SIGNERS - 1; let mut rest_opt: Vec> = (0..n_rest).map(|_| None).collect(); - let chunk = n_rest.div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(n_rest); parallel::par_chunks_mut(&mut rest_opt, chunk, |ci, sub| { for (k, out) in sub.iter_mut().enumerate() { let index = 1 + ci * chunk + k; diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index fb363ee47..64bcb25fb 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -87,7 +87,7 @@ pub fn xmss_key_gen( let n_leaves = (slot_end - slot_start + 1) as usize; let mut leaves: Vec = unsafe { uninitialized_vec(n_leaves) }; { - let chunk = n_leaves.div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(n_leaves); parallel::par_chunks_mut(&mut leaves, chunk, |ci, sub| { for (k, out) in sub.iter_mut().enumerate() { let slot = slot_start + (ci * chunk + k) as u32; @@ -109,7 +109,7 @@ pub fn xmss_key_gen( let prev = &merkle_tree[level - 1]; let n_nodes = (top - base + 1) as usize; let mut nodes: Vec = unsafe { uninitialized_vec(n_nodes) }; - let chunk = n_nodes.div_ceil(parallel::num_threads() * 4).max(1); + let chunk = parallel::recommended_chunk_size(n_nodes); parallel::par_chunks_mut(&mut nodes, chunk, |ci, sub| { for (k, out) in sub.iter_mut().enumerate() { let i = base + (ci * chunk + k) as u64; From c5e90c88f9f9f3c32dca22b6ae0543f42c851a04 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 22:03:06 +0200 Subject: [PATCH 26/65] eq_mle: trim the duplicated 10-line packing-width caveat to 2 lines Pure comment cleanup (no codegen change): the verbose Goldilocks/Neon 'don't assume packing_width > 1' note was duplicated verbatim across the compute_eval_eq variants. Keep the actionable warning, drop the prose. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/poly/src/eq_mle.rs | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/crates/backend/poly/src/eq_mle.rs b/crates/backend/poly/src/eq_mle.rs index 9c3c1b188..bb9bddbf3 100644 --- a/crates/backend/poly/src/eq_mle.rs +++ b/crates/backend/poly/src/eq_mle.rs @@ -121,16 +121,8 @@ where F: Field, EF: ExtensionField, { - // It's possible for this to be called with F = EF (Despite F actually being an extension field). - // - // IMPORTANT: We previously checked here that `packing_width > 1`, - // but this check is **not viable** for Goldilocks on Neon or when not using `target-cpu=native`. - // - // Why? Because Neon SIMD vectors are 128 bits and Goldilocks elements are already 64 bits, - // so no packing happens (width stays 1), and there's no performance advantage. - // - // Be careful: this means code relying on packing optimizations should **not assume** - // `packing_width > 1` is always true. + // `packing_width` may be 1 (e.g. Goldilocks on Neon, or without `target-cpu=native`), + // so nothing here may assume it is > 1. let log_packing_width = log2_strict_usize(F::Packing::WIDTH); // Ensure that the output buffer size is correct: @@ -180,16 +172,8 @@ pub fn compute_eval_eq_packed(eval: &[EF], out: &mu where EF: ExtensionField>, { - // It's possible for this to be called with F = EF (Despite F actually being an extension field). - // - // IMPORTANT: We previously checked here that `packing_width > 1`, - // but this check is **not viable** for Goldilocks on Neon or when not using `target-cpu=native`. - // - // Why? Because Neon SIMD vectors are 128 bits and Goldilocks elements are already 64 bits, - // so no packing happens (width stays 1), and there's no performance advantage. - // - // Be careful: this means code relying on packing optimizations should **not assume** - // `packing_width > 1` is always true. + // `packing_width` may be 1 (e.g. Goldilocks on Neon, or without `target-cpu=native`), + // so nothing here may assume it is > 1. let packing_width = packing_width::(); let log_packing_width = log2_strict_usize(packing_width); From d3fdad1143e255ee338a0aa5af653e38185e8041 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 22:39:39 +0200 Subject: [PATCH 27/65] eq_mle: extract shared par_eval_eq tail from the compute_eval_eq variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four compute_eval_eq{,_packed,_base,_base_packed} functions each open-coded the same buffer-build + parallel-dispatch tail (and duplicated split commentary). Extract it into one `par_eval_eq` helper; each variant now shows only its unique parts (seed scalar, kernel, small-case). Net -43 lines. Perf-neutral: the kernel closure fires once per chunk (~64x), not per element, so unlike the fold_multilinear case it doesn't inhibit specialization. Confirmed over 10 alternating-order runs at low load: Δ mean +0.07% (0.011s, within ~0.04s stdev), median actually lower for the new code. `#[inline]` on the helper is load-bearing for that parity. Tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/poly/src/eq_mle.rs | 247 ++++++++++++------------------ 1 file changed, 102 insertions(+), 145 deletions(-) diff --git a/crates/backend/poly/src/eq_mle.rs b/crates/backend/poly/src/eq_mle.rs index bb9bddbf3..3c3f661fd 100644 --- a/crates/backend/poly/src/eq_mle.rs +++ b/crates/backend/poly/src/eq_mle.rs @@ -43,6 +43,37 @@ where parallel::par_chunks_mut(out, chunk, |i, c| g(c, &buf[i])); } +/// Shared parallel tail of the `compute_eval_eq*` family. With `eval` split into +/// `log_chunks` leading variables (handled one-per-chunk), `log_packing_width` trailing +/// variables already folded into `seed = buffer[0]`, and the middle variables left for +/// `kernel`, this builds the per-chunk equality buffer and runs +/// `kernel(middle, out_chunk, buffer_val)` over `out` in parallel. `kernel` fires once +/// per chunk (not per element), so threading it through a closure costs nothing. +#[inline] +fn par_eval_eq( + eval: &[In], + out: &mut [Out], + log_chunks: usize, + n_chunks: usize, + log_packing_width: usize, + seed: Buf, + kernel: impl Fn(&[In], &mut [Out], Buf) + Sync, +) where + In: Field, + Buf: Algebra + Copy + Send + Sync, + Out: Send, +{ + let mut buffer = Buf::zero_vec(n_chunks); + buffer[0] = seed; + fill_buffer(eval[..log_chunks].iter().rev(), &mut buffer); + + let out_chunk_size = out.len() / n_chunks; + let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; + par_chunks_zip(out, out_chunk_size, &buffer, |out_chunk, buffer_val| { + kernel(middle, out_chunk, *buffer_val); + }); +} + /// Given `evals` = (α_1, ..., α_n), returns a multilinear polynomial P in n variables, /// defined on the boolean hypercube by: ∀ (x_1, ..., x_n) ∈ {0, 1}^n, /// P(x_1, ..., x_n) = Π_{i=1}^{n} (x_i.α_i + (1 - x_i).(1 - α_i)) @@ -124,47 +155,30 @@ where // `packing_width` may be 1 (e.g. Goldilocks on Neon, or without `target-cpu=native`), // so nothing here may assume it is > 1. let log_packing_width = log2_strict_usize(F::Packing::WIDTH); - - // Ensure that the output buffer size is correct: - // It should be of size `2^n`, where `n` is the number of variables. debug_assert_eq!(out.len(), 1 << eval.len()); - // If the number of variables is small, there is no need to use - // parallelization or packings. let (log_chunks, n_chunks) = parallel_split(); if eval.len() <= log_packing_width + 1 + log_chunks { - // A basic recursive approach. + // Too small to be worth packing/parallelizing. eval_eq_basic::<_, _, _, INITIALIZED>(eval, out, scalar); return; } - let eval_len_min_packing = eval.len() - log_packing_width; - - // We split eval into three parts: - // - eval[..log_chunks] (the first log_chunks elements) - // - eval[log_chunks..eval_len_min_packing] (the middle elements) - // - eval[eval_len_min_packing..] (the last log_packing_width elements) - - // The middle elements are the ones which will be computed in parallel. - // The last log_packing_width elements are the ones which will be packed. - - // We make a buffer with one entry per parallel chunk. - let mut parallel_buffer = EF::ExtensionPacking::zero_vec(n_chunks); - let out_chunk_size = out.len() / n_chunks; - - // Compute the equality polynomial corresponding to the last log_packing_width elements - // and pack these. - parallel_buffer[0] = packed_eq_poly(&eval[eval_len_min_packing..], scalar); - - // Update the buffer so it contains the evaluations of the equality polynomial - // with respect to parts one and three. - fill_buffer(eval[..log_chunks].iter().rev(), &mut parallel_buffer); - - // Finally do all computations involving the middle elements in parallel. - let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; - par_chunks_zip(out, out_chunk_size, ¶llel_buffer, |out_chunk, buffer_val| { - eval_eq_with_packed_scalar::<_, _, INITIALIZED>(middle, out_chunk, *buffer_val); - }); + // Split `eval` into [leading `log_chunks` | middle | trailing `log_packing_width`]: the + // trailing vars fold into the per-chunk seed, the leading vars index the chunks, the + // middle runs in parallel. + let seed = packed_eq_poly(&eval[eval.len() - log_packing_width..], scalar); + par_eval_eq( + eval, + out, + log_chunks, + n_chunks, + log_packing_width, + seed, + |middle, out_chunk, buffer_val| { + eval_eq_with_packed_scalar::<_, _, INITIALIZED>(middle, out_chunk, buffer_val); + }, + ); } #[inline] @@ -184,11 +198,11 @@ where // parallelization or packings. let (log_chunks, n_chunks) = parallel_split(); if eval.len() <= log_packing_width + 1 + log_chunks { - // A basic recursive approach. Small case: pack the result sequentially. - let mut output_no_packing = EF::zero_vec(1 << eval.len()); - eval_eq_basic::<_, _, _, false>(eval, &mut output_no_packing, scalar); + // Small case: evaluate unpacked, then pack lanes into `out`. + let mut unpacked = EF::zero_vec(1 << eval.len()); + eval_eq_basic::<_, _, _, false>(eval, &mut unpacked, scalar); out.iter_mut() - .zip(output_no_packing.chunks_exact(packing_width)) + .zip(unpacked.chunks_exact(packing_width)) .for_each(|(out_elem, chunk)| { if INITIALIZED { *out_elem += EF::ExtensionPacking::from_ext_slice(chunk); @@ -196,35 +210,22 @@ where *out_elem = EF::ExtensionPacking::from_ext_slice(chunk); } }); - } else { - let eval_len_min_packing = eval.len() - log_packing_width; - - // We split eval into three parts: - // - eval[..log_chunks] (the first log_chunks elements) - // - eval[log_chunks..eval_len_min_packing] (the middle elements) - // - eval[eval_len_min_packing..] (the last log_packing_width elements) - - // The middle elements are the ones which will be computed in parallel. - // The last log_packing_width elements are the ones which will be packed. - - // We make a buffer with one entry per parallel chunk. - let mut parallel_buffer = EF::ExtensionPacking::zero_vec(n_chunks); - let out_chunk_size = out.len() / n_chunks; - - // Compute the equality polynomial corresponding to the last log_packing_width elements - // and pack these. - parallel_buffer[0] = packed_eq_poly(&eval[eval_len_min_packing..], scalar); - - // Update the buffer so it contains the evaluations of the equality polynomial - // with respect to parts one and three. - fill_buffer(eval[..log_chunks].iter().rev(), &mut parallel_buffer); - - // Finally do all computations involving the middle elements in parallel. - let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; - par_chunks_zip(out, out_chunk_size, ¶llel_buffer, |out_chunk, buffer_val| { - eval_eq_with_packed_output::<_, _, INITIALIZED>(middle, out_chunk, *buffer_val); - }); + return; } + + // See `compute_eval_eq` for the leading/middle/trailing split. + let seed = packed_eq_poly(&eval[eval.len() - log_packing_width..], scalar); + par_eval_eq( + eval, + out, + log_chunks, + n_chunks, + log_packing_width, + seed, + |middle, out_chunk, buffer_val| { + eval_eq_with_packed_output::<_, _, INITIALIZED>(middle, out_chunk, buffer_val); + }, + ); } /// Computes the equality polynomial evaluations efficiently. @@ -250,52 +251,30 @@ where F: Field, EF: ExtensionField, { - // we assume that packing_width is a power of 2. let log_packing_width = log2_strict_usize(F::Packing::WIDTH); - - // Ensure that the output buffer size is correct: - // It should be of size `2^n`, where `n` is the number of variables. debug_assert_eq!(out.len(), 1 << eval.len()); - // If the number of variables is small, there is no need to use - // parallelization or packings. let (log_chunks, n_chunks) = parallel_split(); if eval.len() <= log_packing_width + 1 + log_chunks { - // A basic recursive approach. eval_eq_basic::<_, _, _, INITIALIZED>(eval, out, scalar); return; } - let eval_len_min_packing = eval.len() - log_packing_width; - - // We split eval into three parts: - // - eval[..log_chunks] (the first log_chunks elements) - // - eval[log_chunks..eval_len_min_packing] (the middle elements) - // - eval[eval_len_min_packing..] (the last log_packing_width elements) - - // The middle elements are the ones which will be computed in parallel. - // The last log_packing_width elements are the ones which will be packed. - - // We make a buffer of PackedField elements, one entry per parallel chunk. - // Note that this is a slightly different strategy to `eval_eq` which instead - // uses PackedExtensionField elements. Whilst this involves slightly more mathematical - // operations, it seems to be faster in practice due to less data moving around. - let mut parallel_buffer = F::Packing::zero_vec(n_chunks); - let out_chunk_size = out.len() / n_chunks; - - // Compute the equality polynomial corresponding to the last log_packing_width elements - // and pack these. - parallel_buffer[0] = packed_eq_poly(&eval[eval_len_min_packing..], F::ONE); - - // Update the buffer so it contains the evaluations of the equality polynomial - // with respect to parts one and three. - fill_buffer(eval[..log_chunks].iter().rev(), &mut parallel_buffer); - - // Finally do all computations involving the middle elements in parallel. - let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; - par_chunks_zip(out, out_chunk_size, ¶llel_buffer, |out_chunk, buffer_val| { - base_eval_eq_packed::<_, _, INITIALIZED>(middle, out_chunk, *buffer_val, scalar); - }); + // Base-field input: seed the per-chunk buffer with `F::Packing` (not `EF::ExtensionPacking`) + // and apply `scalar` inside the kernel — slightly more ops but less data movement, which is + // faster here in practice. See `compute_eval_eq` for the leading/middle/trailing split. + let seed = packed_eq_poly(&eval[eval.len() - log_packing_width..], F::ONE); + par_eval_eq( + eval, + out, + log_chunks, + n_chunks, + log_packing_width, + seed, + |middle, out_chunk, buffer_val| { + base_eval_eq_packed::<_, _, INITIALIZED>(middle, out_chunk, buffer_val, scalar); + }, + ); } #[inline] @@ -307,25 +286,18 @@ pub fn compute_eval_eq_base_packed( F: Field, EF: ExtensionField, { - // we assume that packing_width is a power of 2. let packing_width = F::Packing::WIDTH; let log_packing_width = log2_strict_usize(packing_width); assert!(log_packing_width <= eval.len()); assert_eq!(out.len(), 1 << (eval.len() - log_packing_width)); - // Ensure that the output buffer size is correct: - // It should be of size `2^n`, where `n` is the number of variables. - debug_assert_eq!(out.len(), 1 << (eval.len() - log_packing_width)); - - // If the number of variables is small, there is no need to use - // parallelization or packings. let (log_chunks, n_chunks) = parallel_split(); if eval.len() <= log_packing_width + 1 + log_chunks { - // A basic recursive approach. Small case: pack the result sequentially. - let mut output_no_packing = EF::zero_vec(1 << eval.len()); - eval_eq_basic::<_, _, _, false>(eval, &mut output_no_packing, scalar); + // Small case: evaluate unpacked, then pack lanes into `out`. + let mut unpacked = EF::zero_vec(1 << eval.len()); + eval_eq_basic::<_, _, _, false>(eval, &mut unpacked, scalar); out.iter_mut() - .zip(output_no_packing.chunks_exact(packing_width)) + .zip(unpacked.chunks_exact(packing_width)) .for_each(|(out_elem, chunk)| { if INITIALIZED { *out_elem += EF::ExtensionPacking::from_ext_slice(chunk); @@ -333,39 +305,24 @@ pub fn compute_eval_eq_base_packed( *out_elem = EF::ExtensionPacking::from_ext_slice(chunk); } }); - } else { - let eval_len_min_packing = eval.len() - log_packing_width; - - // We split eval into three parts: - // - eval[..log_chunks] (the first log_chunks elements) - // - eval[log_chunks..eval_len_min_packing] (the middle elements) - // - eval[eval_len_min_packing..] (the last log_packing_width elements) - - // The middle elements are the ones which will be computed in parallel. - // The last log_packing_width elements are the ones which will be packed. - - // We make a buffer of PackedField elements, one entry per parallel chunk. - // Note that this is a slightly different strategy to `eval_eq` which instead - // uses PackedExtensionField elements. Whilst this involves slightly more mathematical - // operations, it seems to be faster in practice due to less data moving around. - let mut parallel_buffer = F::Packing::zero_vec(n_chunks); - let out_chunk_size = out.len() / n_chunks; - - // Compute the equality polynomial corresponding to the last log_packing_width elements - // and pack these. - parallel_buffer[0] = packed_eq_poly(&eval[eval_len_min_packing..], F::ONE); - - // Update the buffer so it contains the evaluations of the equality polynomial - // with respect to parts one and three. - fill_buffer(eval[..log_chunks].iter().rev(), &mut parallel_buffer); - - // Finally do all computations involving the middle elements in parallel. - let scalar_packed = EF::ExtensionPacking::from(scalar); - let middle = &eval[log_chunks..(eval.len() - log_packing_width)]; - par_chunks_zip(out, out_chunk_size, ¶llel_buffer, |out_chunk, buffer_val| { - base_eval_eq_packed_with_packed_output::(middle, out_chunk, *buffer_val, scalar_packed); - }); + return; } + + // Base-field input: seed with `F::Packing` and apply `scalar` in the kernel (less data + // movement — see `compute_eval_eq_base`). See `compute_eval_eq` for the split. + let scalar_packed = EF::ExtensionPacking::from(scalar); + let seed = packed_eq_poly(&eval[eval.len() - log_packing_width..], F::ONE); + par_eval_eq( + eval, + out, + log_chunks, + n_chunks, + log_packing_width, + seed, + |middle, out_chunk, buffer_val| { + base_eval_eq_packed_with_packed_output::(middle, out_chunk, buffer_val, scalar_packed); + }, + ); } #[inline] From 39d2cf9e44437dd25c7576187c72e23726a7bb9f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 22:45:04 +0200 Subject: [PATCH 28/65] sumcheck: tighten build_evals body to a one-line map_or Verbose if-let-mut scale collapsed to an idiomatic map_or; same behavior, -8 lines. Cold post-reduction path (maps ~degree elements per round). testall green. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/backend/sumcheck/src/sc_computation.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index 2c0f24c56..2ccf160bd 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -61,14 +61,7 @@ fn build_evals>>( sums: impl IntoIterator, missing_mul_factor: Option, ) -> Vec { - sums.into_iter() - .map(|mut sum| { - if let Some(factor) = missing_mul_factor { - sum *= factor; - } - sum - }) - .collect() + sums.into_iter().map(|sum| missing_mul_factor.map_or(sum, |f| sum * f)).collect() } #[inline(always)] From b6d26c2361b436d1fb6ade0722c4a919c39fe5a1 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 23:09:12 +0200 Subject: [PATCH 29/65] fmt --- crates/backend/sumcheck/src/sc_computation.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index 2ccf160bd..e31b2ea3d 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -61,7 +61,9 @@ fn build_evals>>( sums: impl IntoIterator, missing_mul_factor: Option, ) -> Vec { - sums.into_iter().map(|sum| missing_mul_factor.map_or(sum, |f| sum * f)).collect() + sums.into_iter() + .map(|sum| missing_mul_factor.map_or(sum, |f| sum * f)) + .collect() } #[inline(always)] From 8be26e51a23cfa4309c1671258560ab27975e7d2 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 29 May 2026 23:11:02 +0200 Subject: [PATCH 30/65] lean_compiler: collapse if into match guard (clippy -Dwarnings) The only lint blocking `cargo clippy --workspace --all-targets -- -Dwarnings`. The outer match has a `_ => {}` arm, so the guarded arm is behavior-identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/lean_compiler/src/a_simplify_lang/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/lean_compiler/src/a_simplify_lang/mod.rs b/crates/lean_compiler/src/a_simplify_lang/mod.rs index b36419ff5..2af3eb99a 100644 --- a/crates/lean_compiler/src/a_simplify_lang/mod.rs +++ b/crates/lean_compiler/src/a_simplify_lang/mod.rs @@ -3160,10 +3160,8 @@ fn replace_vars_by_const_in_expr(expr: &mut Expression, map: &BTreeMap) fn replace_vars_by_const_in_lines(lines: &mut [Line], map: &BTreeMap) -> Result<(), String> { for line in lines { match line { - Line::ForwardDeclaration { var, .. } => { - if map.contains_key(var) { - return Err(format!("Variable {var} is a constant")); - } + Line::ForwardDeclaration { var, .. } if map.contains_key(var) => { + return Err(format!("Variable {var} is a constant")); } Line::Statement { targets, .. } => { for target in targets.iter() { From 5313b1a0bff964c8e0a76b98dba743c5b4decfcf Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 30 May 2026 02:51:02 +0200 Subject: [PATCH 31/65] simplify --- crates/backend/parallel/src/lib.rs | 23 +++++++ crates/backend/poly/src/evals.rs | 9 +-- crates/backend/poly/src/utils.rs | 31 +++------- crates/backend/sumcheck/src/sc_computation.rs | 35 ++++------- crates/lean_compiler/src/c_compile_final.rs | 11 +--- crates/lean_prover/src/trace_gen.rs | 11 +--- crates/sub_protocols/src/logup.rs | 7 +-- .../sub_protocols/src/quotient_gkr/layers.rs | 40 +++++-------- .../src/quotient_gkr/sumcheck_utils.rs | 14 +---- crates/utils/src/multilinear.rs | 8 +-- crates/whir/src/open.rs | 7 +-- crates/whir/src/utils.rs | 42 ++++++------- crates/xmss/src/signers_cache.rs | 20 +++---- crates/xmss/src/xmss.rs | 60 ++++++++----------- 14 files changed, 124 insertions(+), 194 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index c85f66e0f..eab467a1c 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -365,6 +365,29 @@ where }); } +/// Parallel `data.iter_mut().enumerate().for_each(|(i, x)| f(i, x))`: run `f` for every +/// element across the pool, sized with [`recommended_chunk_size`]. Hands the closure each +/// element's **global** index, so call sites stop reconstructing `chunk_index * chunk + k` +/// (and the chunk-size dance) by hand — the one home for the "write `out[i]` from a +/// function of `i`" fan-out. +/// +/// `#[inline]` so this thin wrapper folds into the caller — it is invoked once per parallel +/// region (e.g. every `fold_multilinear`, thousands of times per proof), and inlining +/// recovers exactly the codegen of the hand-written `par_chunks_mut` + index loop it replaces. +#[inline] +pub fn par_for_each_mut(data: &mut [T], f: F) +where + F: Fn(usize, &mut T) + Sync, +{ + let chunk = recommended_chunk_size(data.len()); + par_chunks_mut(data, chunk, |ci, sub| { + let base = ci * chunk; + for (k, slot) in sub.iter_mut().enumerate() { + f(base + k, slot); + } + }); +} + /// Give each worker exclusive, persistent access to its own `Option` slot while it /// drains `0..n_tasks`: `run(slot, start, end)` is called once per claimed batch, always /// with the same slot for a given worker, so state accumulates across the batches it diff --git a/crates/backend/poly/src/evals.rs b/crates/backend/poly/src/evals.rs index 64c03f870..9eaa065b0 100644 --- a/crates/backend/poly/src/evals.rs +++ b/crates/backend/poly/src/evals.rs @@ -88,13 +88,8 @@ pub fn scale_poly>(poly: &[F], factor: EF) -> Ve poly.iter().map(|&e| factor * e).collect() } else { let mut out: Vec = unsafe { uninitialized_vec(poly.len()) }; - let n_chunks = (parallel::num_threads() * 4).min(poly.len()); - let chunk = poly.len().div_ceil(n_chunks); - parallel::par_chunks_mut(&mut out, chunk, |i, c| { - let start = i * chunk; - for (j, o) in c.iter_mut().enumerate() { - *o = factor * poly[start + j]; - } + parallel::par_for_each_mut(&mut out, |i, o| { + *o = factor * poly[i]; }); out } diff --git a/crates/backend/poly/src/utils.rs b/crates/backend/poly/src/utils.rs index 200454d5b..4f45f2826 100644 --- a/crates/backend/poly/src/utils.rs +++ b/crates/backend/poly/src/utils.rs @@ -21,12 +21,8 @@ pub fn pack_extension>>(slice: &[EF]) -> Vec(mut a: Vec, b: Vec) -> Vec { + for (x, y) in a.iter_mut().zip(b) { + *x += y; + } + a +} + pub trait SumcheckComputation>>: Sync { type ExtraData: Send + Sync + 'static; @@ -434,12 +441,7 @@ where *acc_d += eval; } }, - |mut a: Vec, b: Vec| { - for (x, y) in a.iter_mut().zip(b) { - *x += y; - } - a - }, + add_assign_vec, ); let unpacked_sums = sums.into_iter().map(&unpack_sum); build_evals(unpacked_sums, missing_mul_factor) @@ -519,12 +521,7 @@ where *acc_d += eval; } }, - |mut a: Vec, b: Vec| { - for (x, y) in a.iter_mut().zip(b) { - *x += y; - } - a - }, + add_assign_vec, ); let unpacked_sums = sums.into_iter().map(&unpack_sum); (build_evals(unpacked_sums, missing_mul_factor), wrap_f(folded_f)) @@ -604,12 +601,7 @@ where *a += *b * eq_lo_bc; } }, - |mut a: Vec>, b: Vec>| { - for (x, y) in a.iter_mut().zip(b) { - *x += y; - } - a - }, + add_assign_vec, ); let unpacked = sums.into_iter().map(&unpack_sum); @@ -703,12 +695,7 @@ where *a += *b * eq_lo_bc; } }, - |mut a: Vec>, b: Vec>| { - for (x, y) in a.iter_mut().zip(b) { - *x += y; - } - a - }, + add_assign_vec, ); let unpacked = sums.into_iter().map(&unpack_sum); diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index b2676ce6c..b6067e224 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -133,14 +133,9 @@ pub fn compile_to_low_level_bytecode( } let mut instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]> = unsafe { uninitialized_vec(instructions.len()) }; - { - let chunk = parallel::recommended_chunk_size(instructions.len()); - parallel::par_chunks_mut(&mut instructions_encoded, chunk, |ci, sub| { - for (k, out) in sub.iter_mut().enumerate() { - *out = field_representation(&instructions[ci * chunk + k]); - } - }); - } + parallel::par_for_each_mut(&mut instructions_encoded, |i, out| { + *out = field_representation(&instructions[i]); + }); let mut instructions_multilinear = vec![]; for instr in &instructions_encoded { diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 31d44ec94..d54ec4b4c 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -94,14 +94,9 @@ pub fn get_execution_trace( }); let mut memory_padded: Vec = unsafe { uninitialized_vec(memory.0.len()) }; - { - let chunk = parallel::recommended_chunk_size(memory_padded.len()); - parallel::par_chunks_mut(&mut memory_padded, chunk, |ci, sub| { - for (k, slot) in sub.iter_mut().enumerate() { - *slot = memory.0[ci * chunk + k].unwrap_or(F::ZERO); - } - }); - } + parallel::par_for_each_mut(&mut memory_padded, |i, slot| { + *slot = memory.0[i].unwrap_or(F::ZERO); + }); // Write [0000000000000000 | poseidon_compress(0000000000000000)] (to make lookups work on padding-rows). let padding_zero_vec_ptr = memory_padded.len(); diff --git a/crates/sub_protocols/src/logup.rs b/crates/sub_protocols/src/logup.rs index 7c5e3e750..85578a1d7 100644 --- a/crates/sub_protocols/src/logup.rs +++ b/crates/sub_protocols/src/logup.rs @@ -538,10 +538,5 @@ where /// global index. Replaces the rayon `par_iter_mut().enumerate()` constant/index fills. #[inline] fn par_fill T + Sync>(dst: &mut [T], build: Build) { - let chunk = parallel::recommended_chunk_size(dst.len()); - parallel::par_chunks_mut(dst, chunk, |ci, sub| { - for (k, slot) in sub.iter_mut().enumerate() { - *slot = build(ci * chunk + k); - } - }); + parallel::par_for_each_mut(dst, |i, slot| *slot = build(i)); } diff --git a/crates/sub_protocols/src/quotient_gkr/layers.rs b/crates/sub_protocols/src/quotient_gkr/layers.rs index d9b5c482c..b6c4502e1 100644 --- a/crates/sub_protocols/src/quotient_gkr/layers.rs +++ b/crates/sub_protocols/src/quotient_gkr/layers.rs @@ -131,18 +131,14 @@ fn sum_quotients_2_by_2>>(nums: &[EF], dens: &[EF]) -> { let dp = parallel::SendPtr(new_dens.as_mut_ptr()); - let chunk = parallel::recommended_chunk_size(full_pairs); - parallel::par_chunks_mut(&mut new_nums[..full_pairs], chunk, |ci, num_chunk| { - for (k, num) in num_chunk.iter_mut().enumerate() { - let i = ci * chunk + k; - let n0 = nums[2 * i]; - let n1 = nums[2 * i + 1]; - let d0 = dens[2 * i]; - let d1 = dens[2 * i + 1]; - *num = d1 * n0 + d0 * n1; - // SAFETY: each `i` writes a distinct slot in `new_dens`, a separate buffer. - unsafe { *dp.add(i) = d0 * d1 }; - } + parallel::par_for_each_mut(&mut new_nums[..full_pairs], |i, num| { + let n0 = nums[2 * i]; + let n1 = nums[2 * i + 1]; + let d0 = dens[2 * i]; + let d1 = dens[2 * i + 1]; + *num = d1 * n0 + d0 * n1; + // SAFETY: each `i` writes a distinct slot in `new_dens`, a separate buffer. + unsafe { *dp.add(i) = d0 * d1 }; }); } @@ -177,18 +173,14 @@ where { let dp = parallel::SendPtr(new_dens.as_mut_ptr()); - let chunk = parallel::recommended_chunk_size(new_nums.len()); - parallel::par_chunks_mut(&mut new_nums, chunk, |ci, num_chunk| { - for (k, num_out) in num_chunk.iter_mut().enumerate() { - let new_j = ci * chunk + k; - let i_hi = new_j >> bit; - let i_lo = new_j & lo_mask; - let i0 = (i_hi << (bit + 1)) | i_lo; - let i1 = i0 | stride; - *num_out = dens[i1] * nums[i0] + dens[i0] * nums[i1]; - // SAFETY: each `new_j` writes a distinct slot in `new_dens`, a separate buffer. - unsafe { *dp.add(new_j) = dens[i0] * dens[i1] }; - } + parallel::par_for_each_mut(&mut new_nums, |new_j, num_out| { + let i_hi = new_j >> bit; + let i_lo = new_j & lo_mask; + let i0 = (i_hi << (bit + 1)) | i_lo; + let i1 = i0 | stride; + *num_out = dens[i1] * nums[i0] + dens[i0] * nums[i1]; + // SAFETY: each `new_j` writes a distinct slot in `new_dens`, a separate buffer. + unsafe { *dp.add(new_j) = dens[i0] * dens[i1] }; }); } diff --git a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs index c299cfb20..7c85d0e2f 100644 --- a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs +++ b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs @@ -360,12 +360,7 @@ pub(super) fn run_phase2_sumcheck>>( let fold_eq = |i: usize| eq_table[2 * i] + eq_table[2 * i + 1]; eq_table = if new_eq_len >= PARALLEL_THRESHOLD { let mut out: Vec = unsafe { uninitialized_vec(new_eq_len) }; - let chunk = parallel::recommended_chunk_size(new_eq_len); - parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { - for (k, slot) in sub.iter_mut().enumerate() { - *slot = fold_eq(ci * chunk + k); - } - }); + parallel::par_for_each_mut(&mut out, |i, slot| *slot = fold_eq(i)); out } else { (0..new_eq_len).map(fold_eq).collect() @@ -396,12 +391,7 @@ fn fold_normal_with_padding>>(m: &[EF], r: EF, pad_val if new_active < PARALLEL_THRESHOLD { out.iter_mut().enumerate().for_each(compute); } else { - let chunk = parallel::recommended_chunk_size(new_active); - parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { - for (k, slot) in sub.iter_mut().enumerate() { - compute((ci * chunk + k, slot)); - } - }); + parallel::par_for_each_mut(&mut out, |i, slot| compute((i, slot))); } out } diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 7a13faa88..0147d28a0 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -15,12 +15,8 @@ pub fn multilinears_linear_combination, P: Borro assert!(pols.iter().all(|p| log2_strict_usize(p.borrow().len()) == n_vars)); let n = 1usize << n_vars; let mut out: Vec = unsafe { uninitialized_vec(n) }; - let chunk = parallel::recommended_chunk_size(n); - parallel::par_chunks_mut(&mut out, chunk, |ci, sub| { - for (k, slot) in sub.iter_mut().enumerate() { - let i = ci * chunk + k; - *slot = dot_product(scalars.iter().copied(), pols.iter().map(|p| p.borrow()[i])); - } + parallel::par_for_each_mut(&mut out, |i, slot| { + *slot = dot_product(scalars.iter().copied(), pols.iter().map(|p| p.borrow()[i])); }); out } diff --git a/crates/whir/src/open.rs b/crates/whir/src/open.rs index 9e1807521..6b737b9c4 100644 --- a/crates/whir/src/open.rs +++ b/crates/whir/src/open.rs @@ -598,11 +598,8 @@ where for (out_buff, &(origin_index, _)) in chunks_mut.iter_mut().zip(&indexed_smt_values) { let out = &mut out_buff[..1 << shift]; let scalar = next_gamma_powers[origin_index]; - let chunk = parallel::recommended_chunk_size(out.len()); - parallel::par_chunks_mut(out, chunk, |ci, sub| { - for (k, out_elem) in sub.iter_mut().enumerate() { - *out_elem += inner_poly[ci * chunk + k] * scalar; - } + parallel::par_for_each_mut(out, |i, out_elem| { + *out_elem += inner_poly[i] * scalar; }); } gamma_pow = *next_gamma_powers.last().unwrap() * gamma; diff --git a/crates/whir/src/utils.rs b/crates/whir/src/utils.rs index b06409597..af39b6a3c 100644 --- a/crates/whir/src/utils.rs +++ b/crates/whir/src/utils.rs @@ -138,15 +138,11 @@ fn prepare_evals_for_fft_unpacked( let out_len = block_size * dft_n_cols; let mut out: Vec = unsafe { uninitialized_vec(out_len) }; - let chunk = parallel::recommended_chunk_size(out_len); - parallel::par_chunks_mut(&mut out, chunk, |ci, out_chunk| { - for (k, slot) in out_chunk.iter_mut().enumerate() { - let i = ci * chunk + k; - let block_index = i % dft_n_cols; - let offset_in_block = i / dft_n_cols; - let src_index = ((block_index << log_block_size) + offset_in_block) >> log_inv_rate; - *slot = unsafe { *evals.get_unchecked(src_index) }; - } + parallel::par_for_each_mut(&mut out, |i, slot| { + let block_index = i % dft_n_cols; + let offset_in_block = i / dft_n_cols; + let src_index = ((block_index << log_block_size) + offset_in_block) >> log_inv_rate; + *slot = unsafe { *evals.get_unchecked(src_index) }; }); out } @@ -166,22 +162,18 @@ fn prepare_evals_for_fft_packed_extension>>( let packing_mask = (1 << log_packing) - 1; let mut out: Vec = unsafe { uninitialized_vec(full_len) }; - let chunk = parallel::recommended_chunk_size(full_len); - parallel::par_chunks_mut(&mut out, chunk, |ci, out_chunk| { - for (k, slot) in out_chunk.iter_mut().enumerate() { - let i = ci * chunk + k; - let block_index = i & n_blocks_mask; - let offset_in_block = i >> folding_factor; - let src_index = ((block_index << log_block_size) + offset_in_block) >> log_inv_rate; - let packed_src_index = src_index >> log_packing; - let offset_in_packing = src_index & packing_mask; - let packed = unsafe { evals.get_unchecked(packed_src_index) }; - let unpacked: &[PFPacking] = packed.as_basis_coefficients_slice(); - *slot = EF::from_basis_coefficients_fn(|j| unsafe { - let u: &PFPacking = unpacked.get_unchecked(j); - *u.as_slice().get_unchecked(offset_in_packing) - }); - } + parallel::par_for_each_mut(&mut out, |i, slot| { + let block_index = i & n_blocks_mask; + let offset_in_block = i >> folding_factor; + let src_index = ((block_index << log_block_size) + offset_in_block) >> log_inv_rate; + let packed_src_index = src_index >> log_packing; + let offset_in_packing = src_index & packing_mask; + let packed = unsafe { evals.get_unchecked(packed_src_index) }; + let unpacked: &[PFPacking] = packed.as_basis_coefficients_slice(); + *slot = EF::from_basis_coefficients_fn(|j| unsafe { + let u: &PFPacking = unpacked.get_unchecked(j); + *u.as_slice().get_unchecked(offset_in_packing) + }); }); out } diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 85616bc5e..27c73dd15 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -91,18 +91,14 @@ fn gen_benchmark_signers_cache() -> Vec<(XmssPublicKey, XmssSignature)> { let time = Instant::now(); let n_rest = NUM_BENCHMARK_SIGNERS - 1; let mut rest_opt: Vec> = (0..n_rest).map(|_| None).collect(); - let chunk = parallel::recommended_chunk_size(n_rest); - parallel::par_chunks_mut(&mut rest_opt, chunk, |ci, sub| { - for (k, out) in sub.iter_mut().enumerate() { - let index = 1 + ci * chunk + k; - let signer = compute_signer(index); - let done = completed.fetch_add(1, Ordering::Relaxed) + 1; - print!( - "\rPrecomputing benchmark signatures (cached after first run): {:.0}%", - 100.0 * done as f64 / NUM_BENCHMARK_SIGNERS as f64 - ); - *out = Some(signer); - } + parallel::par_for_each_mut(&mut rest_opt, |i, out| { + let signer = compute_signer(1 + i); + let done = completed.fetch_add(1, Ordering::Relaxed) + 1; + print!( + "\rPrecomputing benchmark signatures (cached after first run): {:.0}%", + 100.0 * done as f64 / NUM_BENCHMARK_SIGNERS as f64 + ); + *out = Some(signer); }); let rest: Vec<_> = rest_opt.into_iter().map(Option::unwrap).collect(); diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 64bcb25fb..e56771cc8 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -86,16 +86,11 @@ pub fn xmss_key_gen( // Level 0: WOTS leaf hashes for slots in [slot_start, slot_end] let n_leaves = (slot_end - slot_start + 1) as usize; let mut leaves: Vec = unsafe { uninitialized_vec(n_leaves) }; - { - let chunk = parallel::recommended_chunk_size(n_leaves); - parallel::par_chunks_mut(&mut leaves, chunk, |ci, sub| { - for (k, out) in sub.iter_mut().enumerate() { - let slot = slot_start + (ci * chunk + k) as u32; - let wots = gen_wots_secret_key(&seed, slot, public_param); - *out = wots.public_key().hash(public_param, slot); - } - }); - } + parallel::par_for_each_mut(&mut leaves, |i, out| { + let slot = slot_start + i as u32; + let wots = gen_wots_secret_key(&seed, slot, public_param); + *out = wots.public_key().hash(public_param, slot); + }); let mut merkle_tree = vec![leaves]; // Build levels 1..=LOG_LIFETIME. // At level l, we store nodes with index in [(slot_start >> l), (slot_end >> l)]. @@ -109,30 +104,27 @@ pub fn xmss_key_gen( let prev = &merkle_tree[level - 1]; let n_nodes = (top - base + 1) as usize; let mut nodes: Vec = unsafe { uninitialized_vec(n_nodes) }; - let chunk = parallel::recommended_chunk_size(n_nodes); - parallel::par_chunks_mut(&mut nodes, chunk, |ci, sub| { - for (k, out) in sub.iter_mut().enumerate() { - let i = base + (ci * chunk + k) as u64; - let left_idx = 2 * i; - let right_idx = 2 * i + 1; - let left = if left_idx >= prev_base && left_idx <= prev_top { - prev[(left_idx - prev_base) as usize] - } else { - gen_random_node(&seed, level - 1, left_idx) - }; - let right = if right_idx >= prev_base && right_idx <= prev_top { - prev[(right_idx - prev_base) as usize] - } else { - gen_random_node(&seed, level - 1, right_idx) - }; - let merkle_data = build_merkle_data( - make_tweak(TWEAK_TYPE_MERKLE, level, i as u32), - &public_param, - &left, - &right, - ); - *out = poseidon16_compress(merkle_data)[..XMSS_DIGEST_LEN].try_into().unwrap(); - } + parallel::par_for_each_mut(&mut nodes, |k, out| { + let i = base + k as u64; + let left_idx = 2 * i; + let right_idx = 2 * i + 1; + let left = if left_idx >= prev_base && left_idx <= prev_top { + prev[(left_idx - prev_base) as usize] + } else { + gen_random_node(&seed, level - 1, left_idx) + }; + let right = if right_idx >= prev_base && right_idx <= prev_top { + prev[(right_idx - prev_base) as usize] + } else { + gen_random_node(&seed, level - 1, right_idx) + }; + let merkle_data = build_merkle_data( + make_tweak(TWEAK_TYPE_MERKLE, level, i as u32), + &public_param, + &left, + &right, + ); + *out = poseidon16_compress(merkle_data)[..XMSS_DIGEST_LEN].try_into().unwrap(); }); nodes }; From a9e53eafdd87f8a99a9a2e83087653e063476ce4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 30 May 2026 02:56:32 +0200 Subject: [PATCH 32/65] remove tests --- crates/backend/parallel/src/lib.rs | 110 ----------------------------- 1 file changed, 110 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index eab467a1c..58d9d92ea 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -470,113 +470,3 @@ where .map(|(_, acc)| acc) .fold(init_acc(), &combine) } - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::atomic::AtomicU64; - - #[test] - fn for_each_index_runs_all() { - let n = 10_000; - let sum = AtomicU64::new(0); - for_each_index(n, |i| { - sum.fetch_add(i as u64, Ordering::Relaxed); - }); - assert_eq!(sum.load(Ordering::Relaxed), (0..n as u64).sum()); - } - - #[test] - fn par_chunks_mut_writes_disjoint() { - let mut data = vec![0usize; 100_000]; - par_chunks_mut(&mut data, 64, |i, chunk| { - for (j, x) in chunk.iter_mut().enumerate() { - *x = i * 64 + j; - } - }); - for (idx, &v) in data.iter().enumerate() { - assert_eq!(v, idx); - } - } - - #[test] - fn map_reduce_matches_sequential() { - for n in [0usize, 1, 2, 1000, 100_000] { - let got = map_reduce(n, || 0u64, |i| i as u64, |a, b| a + b); - assert_eq!(got, (0..n as u64).sum::(), "scalar sum n={n}"); - } - let n = 5000; - let got = map_reduce( - n, - || vec![0u64; 3], - |i| vec![i as u64, (i * 2) as u64, (i * 3) as u64], - |mut a, b| { - for (x, y) in a.iter_mut().zip(b) { - *x += y; - } - a - }, - ); - let s: u64 = (0..n as u64).sum(); - assert_eq!(got, vec![s, 2 * s, 3 * s]); - } - - #[test] - fn map_reduce_with_state_matches_sequential() { - for n in [0usize, 1, 3, 1000, 50_000] { - let got = map_reduce_with_state( - n, - Vec::::new, - || vec![0u64; 2], - |scratch: &mut Vec, acc: &mut Vec, i| { - scratch.clear(); - scratch.push(i as u64); - scratch.push((i * i) as u64); - acc[0] += scratch[0]; - acc[1] += scratch[1]; - }, - |mut a: Vec, b: Vec| { - for (x, y) in a.iter_mut().zip(b) { - *x += y; - } - a - }, - ); - let s0: u64 = (0..n as u64).sum(); - let s1: u64 = (0..n as u64).map(|i| i * i).sum(); - assert_eq!(got, vec![s0, s1], "n={n}"); - } - } - - #[test] - fn nested_dispatch_does_not_deadlock() { - // Outer parallel loop whose body itself dispatches — must run (inner goes - // sequential) and produce correct results, not hang. - let mut data = vec![0u64; 1000]; - par_chunks_mut(&mut data, 50, |outer, chunk| { - // Nested dispatch from inside a pool task. - let sum = AtomicU64::new(0); - for_each_index(chunk.len(), |i| { - sum.fetch_add((outer * 50 + i) as u64, Ordering::Relaxed); - }); - chunk[0] = sum.load(Ordering::Relaxed); - }); - for (c, chunk) in data.chunks(50).enumerate() { - let expected: u64 = (0..50).map(|i| (c * 50 + i) as u64).sum(); - assert_eq!(chunk[0], expected, "chunk {c}"); - } - } - - #[test] - fn repeated_dispatch_is_stable() { - for _ in 0..50 { - let mut data = vec![0u32; 8192]; - par_chunks_mut(&mut data, 16, |_, chunk| { - for x in chunk.iter_mut() { - *x += 1; - } - }); - assert!(data.iter().all(|&x| x == 1)); - } - } -} From e27782feaac645be08a3394bb61414b00a32b92a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 30 May 2026 15:02:42 +0400 Subject: [PATCH 33/65] muucch faster prepare_evals_for_fft_unpacked --- crates/whir/src/utils.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/whir/src/utils.rs b/crates/whir/src/utils.rs index af39b6a3c..64f78994e 100644 --- a/crates/whir/src/utils.rs +++ b/crates/whir/src/utils.rs @@ -138,11 +138,25 @@ fn prepare_evals_for_fft_unpacked( let out_len = block_size * dft_n_cols; let mut out: Vec = unsafe { uninitialized_vec(out_len) }; - parallel::par_for_each_mut(&mut out, |i, slot| { - let block_index = i % dft_n_cols; - let offset_in_block = i / dft_n_cols; - let src_index = ((block_index << log_block_size) + offset_in_block) >> log_inv_rate; - *slot = unsafe { *evals.get_unchecked(src_index) }; + if block_size == 0 || dft_n_cols == 0 { + return out; + } + + let rows_per_band = ((system_info::L1_CACHE_SIZE / 2) / (dft_n_cols * size_of::())).clamp(1, block_size); + let band_len = rows_per_band * dft_n_cols; + + parallel::par_chunks_mut(&mut out, band_len, |band_idx, band| { + let row0 = band_idx * rows_per_band; + let n_rows = band.len() / dft_n_cols; + for col in 0..dft_n_cols { + let col_base = col << log_block_size; + for r in 0..n_rows { + let src = (col_base + row0 + r) >> log_inv_rate; + unsafe { + *band.get_unchecked_mut(r * dft_n_cols + col) = *evals.get_unchecked(src); + } + } + } }); out } From 4bddf8dbed4c7288181386634fbf657ae630a9ef Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 30 May 2026 15:42:45 +0400 Subject: [PATCH 34/65] simplify --- crates/backend/parallel/src/lib.rs | 130 +++++++++---------------- crates/backend/poly/src/eq_mle.rs | 16 +-- crates/backend/poly/src/evals.rs | 1 - crates/lean_vm/src/execution/runner.rs | 11 +-- crates/whir/src/dft.rs | 54 ++++------ 5 files changed, 70 insertions(+), 142 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 58d9d92ea..e47ec1350 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -1,52 +1,30 @@ -//! Minimal fixed-size thread pool for flat, static data-parallel kernels. -//! -//! A deliberately tiny alternative to rayon for the one shape the prover uses on its -//! hot paths: "split a range/slice into pieces, run a closure on each." Unlike rayon -//! it does **no** work-stealing of nested tasks and allocates **nothing** per -//! dispatch — the whole point is to own the runtime so we can (a) attach per-worker -//! scratch buffers (eliminating per-task allocations on hot paths), and (b) drop -//! rayon entirely along with its `flush_rayon` injector hack. +//! Minimal fixed-size thread pool for flat data-parallel kernels: "split a range into +//! pieces, run a closure on each." No nested work-stealing, no per-dispatch allocation — +//! owning the runtime lets us attach per-worker scratch buffers and drop rayon. //! //! ## Model //! -//! The pool owns exactly `NUM_THREADS - 1` background workers with stable ids -//! `1..NUM_THREADS`; the dispatching thread acts as worker `0` and runs its share -//! inline (so a dispatch keeps all `NUM_THREADS` hardware threads busy with only -//! `NUM_THREADS - 1` extra threads — no oversubscription). Tasks are claimed from a -//! shared atomic counter, giving dynamic load balancing for free. -//! -//! ## Dispatch is lock-free on the hot path -//! -//! A `std::Barrier` (mutex + condvar) wake-up costs ~2x rayon per dispatch, which the -//! prover's many dispatches turn into a real regression. Instead, dispatch bumps a -//! `generation` counter that idle workers watch by **spinning** (so back-to-back -//! dispatches never pay a syscall), parking only after `SPIN_LIMIT` unrewarded spins. -//! Completion is a lock-free atomic countdown (`working`) the dispatcher spins on. -//! Park/unpark uses a per-worker `parked` flag with SeqCst ordering against -//! `generation` to avoid lost wake-ups, so the unpark syscall is skipped while -//! workers are hot. Measured ~7.5µs/dispatch vs rayon's ~37µs. +//! `NUM_THREADS - 1` background workers (ids `1..NUM_THREADS`); the dispatching thread is +//! worker `0` and runs its share inline (no oversubscription). Tasks are claimed from a +//! shared atomic counter for dynamic load balancing. //! -//! ## Coexistence caveat +//! ## Lock-free dispatch //! -//! Running this pool *alongside* rayon (a partial migration) regresses the prover -//! ~30% — work bounces between two disjoint thread sets, thrashing caches and -//! oversubscribing at region boundaries. This pool only pays off once rayon is gone -//! everywhere. Treat partial-migration benchmarks accordingly. +//! A mutex+condvar wake-up costs ~2x per dispatch. Instead, dispatch bumps a `generation` +//! counter that idle workers **spin** on (back-to-back dispatches pay no syscall), parking +//! only after `SPIN_LIMIT` unrewarded spins. Completion is a lock-free `working` countdown +//! the dispatcher spins on. The per-worker `parked` flag is ordered SeqCst against +//! `generation` so no wake-up is lost and the unpark syscall is skipped while workers spin. //! //! ## Nesting falls back to sequential //! -//! Some kernels nest parallelism (e.g. the NTT fans out over blocks, then over rows -//! within a block). A flat pool can't dispatch from inside a task — that would -//! deadlock on the dispatch lock. So a `for_each_index` call made from a thread -//! already running a pool task runs **sequentially inline** instead. Correct, never -//! deadlocks; the inner level loses parallelism but the outer level has usually -//! already saturated all cores. (rayon work-steals through nesting instead; this is -//! the one place we trade a little potential utilization for a vastly simpler pool.) +//! A flat pool can't dispatch from inside a task (would deadlock the dispatch lock), so a +//! `for_each_index` issued from within a pool task runs sequentially inline. The outer +//! level has usually already saturated all cores. //! //! ## Constraint //! -//! - **One dispatcher at a time.** Concurrent (non-nested) dispatches from different -//! threads are serialized by a mutex. +//! One dispatcher at a time: concurrent (non-nested) dispatches are serialized by a mutex. use std::cell::{Cell, UnsafeCell}; use std::ptr::NonNull; @@ -56,16 +34,12 @@ use std::thread::Thread; use system_info::NUM_THREADS; -/// Spins an idle worker performs before parking (~a few µs). Tuned so back-to-back -/// dispatches keep workers hot (no syscalls), while the prover's *sequential* stretches -/// — longer than this — let the workers sleep, freeing their cores so the active thread -/// can clock up instead of competing with 15 spinners. (A much larger limit kept workers -/// spinning through those gaps, adding run-to-run variance for no throughput gain.) +/// Idle spins before a worker parks. Tuned so back-to-back dispatches stay hot but +/// sequential gaps let workers sleep, freeing cores for the active thread. const SPIN_LIMIT: u32 = 1 << 12; -/// Upper bound on a single guided-self-scheduling claim (see [`drain`]). Caps the -/// worst-case load imbalance from one worker grabbing too large an initial batch, while -/// staying large enough that million-task kernels need only a few thousand claims. +/// Cap on a single guided-self-scheduling claim (see [`drain`]). Bounds worst-case load +/// imbalance while keeping million-task kernels to a few thousand claims. const MAX_CLAIM_BATCH: usize = 1 << 12; /// Total worker count (including the dispatching thread). Equal to build-time `NUM_THREADS`. @@ -74,10 +48,8 @@ pub const fn num_threads() -> usize { NUM_THREADS } -/// The standard chunk size for a flat fan-out of `n_items` over the pool: a handful of -/// chunks per worker so the atomic counter can rebalance heterogeneous cores, while -/// staying coarse enough to amortize dispatch. The canonical divisor for `par_chunks_mut` -/// — prefer this over hand-rolling `n.div_ceil(num_threads() * 4)` at each call site. +/// Chunk size for a flat fan-out: a few chunks per worker so the atomic counter can +/// rebalance heterogeneous cores, coarse enough to amortize dispatch. #[must_use] #[inline] pub fn recommended_chunk_size(n_items: usize) -> usize { @@ -137,13 +109,9 @@ struct Pool { unsafe impl Sync for Pool {} unsafe impl Send for Pool {} -/// Construct the pool and exercise its dispatch path once, now. -/// -/// Optional warm-up: idempotent, it spawns the worker threads and runs one real -/// dispatch so the leaked `Pool`, the worker `Parker`s, and the `dispatch` mutex's -/// lazily-allocated `pthread_mutex_t` (macOS allocates it on first lock, not -/// construction) are all created up front, before any timed proving work. Without it -/// the pool initializes lazily on the first parallel call. +/// Idempotent warm-up: spawn workers and run one dispatch so the pool, parkers, and the +/// mutex's lazily-allocated `pthread_mutex_t` (macOS) exist before timed proving work. +/// Without it the pool initializes lazily on the first parallel call. pub fn init() { static INIT: Once = Once::new(); INIT.call_once(|| { @@ -219,16 +187,10 @@ fn worker_main(pool: &'static Pool, id: usize) { } } -/// Claim and run task indices until the counter is exhausted. -/// -/// Uses **guided self-scheduling**: each claim grabs a batch proportional to the work -/// still remaining (`remaining / (NUM_THREADS * 2)`), capped at [`MAX_CLAIM_BATCH`]. -/// This is what rayon's recursive splitting buys implicitly — without it, a kernel that -/// emits one task per element (e.g. a merkle layer with one packed compression per task, -/// millions of tasks) would hammer the shared counter with millions of `fetch_add`s. -/// Large early batches cut that contention to a handful of claims; the proportional -/// shrink toward the end keeps the tail finely divided for load balance, and small -/// dispatches naturally fall back to single-task claims (`max(1)`). +/// Claim and run task indices until the counter is exhausted, using **guided +/// self-scheduling**: each claim grabs `remaining / (NUM_THREADS * 2)`, capped at +/// [`MAX_CLAIM_BATCH`]. Large early batches keep counter contention low; the proportional +/// shrink keeps the tail finely divided for load balance. fn drain(pool: &Pool) { // SAFETY: the dispatcher published `Some(job)` before the generation bump this // worker just observed, and overwrites it only on the next dispatch (gated on @@ -241,9 +203,8 @@ fn drain(pool: &Pool) { // rather than deadlocking on the dispatch lock. let prev = IN_TASK.replace(true); loop { - // Batch size is computed from a (possibly stale) counter read; this only affects - // granularity, never correctness — `fetch_add` chains claims into a contiguous, - // non-overlapping tiling of `0..n`, and out-of-range tails are clamped/skipped. + // Stale counter read only affects batch granularity, not correctness: `fetch_add` + // tiles `0..n` into non-overlapping claims, and out-of-range tails are clamped. let observed = pool.counter.load(Ordering::Relaxed); if observed >= n { break; @@ -322,10 +283,9 @@ pub fn for_each_index(n_tasks: usize, f: F) { }); } -/// A raw base pointer made shareable across pool workers. Sound only because every -/// caller partitions the backing allocation by task index, so each worker touches a -/// disjoint region. This is the one home for the "share a `*mut` across a dispatch" -/// pattern — reach for it (with `par_chunks_mut`) instead of redefining it per crate. +/// A raw base pointer shareable across pool workers. Sound only because callers partition +/// the allocation by task index, so each worker touches a disjoint region. Reuse this for +/// the "share a `*mut` across a dispatch" pattern instead of redefining it per crate. #[derive(Debug)] pub struct SendPtr(pub *mut T); // SAFETY: accesses are partitioned by task index (see callers). @@ -342,6 +302,16 @@ impl SendPtr { pub unsafe fn add(&self, n: usize) -> *mut T { unsafe { self.0.add(n) } } + + /// Reconstruct the `len`-long slice starting at element offset `off`. + /// + /// # Safety + /// `off`/`len` must stay in-bounds and the slice must be disjoint from every other + /// slice any concurrent task reconstructs. + #[inline] + pub unsafe fn slice<'a>(&self, off: usize, len: usize) -> &'a mut [T] { + unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) } + } } /// Parallel equivalent of `data.chunks_mut(chunk).enumerate().for_each(...)`, running @@ -365,15 +335,9 @@ where }); } -/// Parallel `data.iter_mut().enumerate().for_each(|(i, x)| f(i, x))`: run `f` for every -/// element across the pool, sized with [`recommended_chunk_size`]. Hands the closure each -/// element's **global** index, so call sites stop reconstructing `chunk_index * chunk + k` -/// (and the chunk-size dance) by hand — the one home for the "write `out[i]` from a -/// function of `i`" fan-out. -/// -/// `#[inline]` so this thin wrapper folds into the caller — it is invoked once per parallel -/// region (e.g. every `fold_multilinear`, thousands of times per proof), and inlining -/// recovers exactly the codegen of the hand-written `par_chunks_mut` + index loop it replaces. +/// Parallel `data.iter_mut().enumerate().for_each(...)`, sized with +/// [`recommended_chunk_size`]. Hands the closure each element's **global** index. +/// `#[inline]` to fold into the per-region caller (recovers the hand-written codegen). #[inline] pub fn par_for_each_mut(data: &mut [T], f: F) where diff --git a/crates/backend/poly/src/eq_mle.rs b/crates/backend/poly/src/eq_mle.rs index 3c3f661fd..16c59f015 100644 --- a/crates/backend/poly/src/eq_mle.rs +++ b/crates/backend/poly/src/eq_mle.rs @@ -7,19 +7,9 @@ use system_info::NUM_THREADS; const LOG_NUM_THREADS: usize = log2_ceil_usize(NUM_THREADS); const LOG_BATCHED_TILE_SIZE: usize = 14; -/// Oversubscription factor (log2) for the parallel fan-out: the prover's eq_mle -/// kernels split the output into `NUM_THREADS << PARALLEL_LOG_OVERSUB` chunks rather -/// than one per worker. Emitting more chunks than workers lets the pool's atomic -/// task counter rebalance across heterogeneous cores (e.g. Apple P/E cores) instead -/// of being bound by the slowest single chunk. `0` reproduces the one-chunk-per- -/// worker behavior. -/// -/// Default is `2` (4x): a deliberately conservative, machine-agnostic value. On the -/// M-series Mac the benefit saturates by 2-3x and a higher factor (4-5x) is slightly -/// faster, but those finer chunks risk regressing on many-core homogeneous CPUs -/// (more contention on the shared counter, worse cache reuse), so we take the low -/// end that captures most of the gain everywhere rather than the per-machine optimum. -/// Exposed as a runtime knob so the benchmark can re-sweep on each target machine. +/// log2 oversubscription for the eq_mle fan-out: emit `NUM_THREADS << this` chunks so the +/// pool's task counter rebalances across heterogeneous cores (e.g. P/E). `0` = one chunk +/// per worker. Default `2` (4x) is conservative; a runtime knob so the benchmark can sweep. pub static PARALLEL_LOG_OVERSUB: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(2); /// `(log2(n_chunks), n_chunks)` for the parallel fan-out, honoring [`PARALLEL_LOG_OVERSUB`]. diff --git a/crates/backend/poly/src/evals.rs b/crates/backend/poly/src/evals.rs index 9eaa065b0..c1168470f 100644 --- a/crates/backend/poly/src/evals.rs +++ b/crates/backend/poly/src/evals.rs @@ -324,7 +324,6 @@ where }); interpolate_res(&partials, lead, mul_res_point) } else { - // For smaller subproblems, execute sequentially. let (f0, f1) = evals.split_at(evals.len() / 2); let f0_eval = eval_multilinear_generic::<_, _, _, _, _, _, false>( f0, diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index 2935f585f..8a47e716d 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -443,15 +443,10 @@ fn handle_parallel_batch( // Raw base pointer + length per disjoint segment, so the pool can run each segment // on its own slice without moving `&mut` references through the `Fn` task closure. - struct SegPtr(*mut Option); - // SAFETY: the segments are non-overlapping `chunks_mut` of `right`; each task `i` - // reconstructs and touches only segment `i`. - unsafe impl Send for SegPtr {} - unsafe impl Sync for SegPtr {} - - let seg_info: Vec<(SegPtr, usize)> = segment_slices + // SAFETY: segments are non-overlapping `chunks_mut` of `right`; task `i` touches only `i`. + let seg_info: Vec<(parallel::SendPtr>, usize)> = segment_slices .iter_mut() - .map(|s| (SegPtr(s.as_mut_ptr()), s.len())) + .map(|s| (parallel::SendPtr(s.as_mut_ptr()), s.len())) .collect(); // Release the `&mut` borrows so only the raw pointers alias the segments. drop(segment_slices); diff --git a/crates/whir/src/dft.rs b/crates/whir/src/dft.rs index d0e6cde4b..9b0d8329e 100644 --- a/crates/whir/src/dft.rs +++ b/crates/whir/src/dft.rs @@ -37,26 +37,6 @@ use crate::{Matrix, RowMajorMatrix, RowMajorMatrixViewMut}; /// The number of layers to compute in each parallelization. const LAYERS_PER_GROUP: usize = 3; -/// Raw mutable base pointer shareable across pool tasks. The multi-layer butterfly -/// kernels flatten their (block, inner-group) iteration space into one parallel loop and -/// reconstruct each group's disjoint `width`-row sub-slices from this base — so coarse -/// layers (few blocks) still parallelize over their inner groups instead of going serial. -#[derive(Clone, Copy)] -struct ButterflyPtr(*mut F); -// SAFETY: each task touches only the disjoint rows computed from its group index. -unsafe impl Send for ButterflyPtr {} -unsafe impl Sync for ButterflyPtr {} - -impl ButterflyPtr { - /// Reconstruct the `width`-long row starting at element offset `off`. - /// SAFETY: `off`/`width` must stay in-bounds and the row must be disjoint from every - /// other row any concurrent task reconstructs. - #[inline] - unsafe fn row<'a>(self, off: usize, width: usize) -> &'a mut [F] { - unsafe { std::slice::from_raw_parts_mut(self.0.add(off), width) } - } -} - #[derive(Default, Debug)] pub(crate) struct EvalsDft { twiddles: RwLock>>, @@ -223,13 +203,13 @@ fn dft_layer_par>(vec: &mut [F], twiddles: &[B], width // Flatten (block, group) into one parallel loop over `n_blocks * ts` groups so coarse // layers (few blocks) still parallelize; guided scheduling keeps a worker's batch of // consecutive groups within the same block, preserving the per-block cache locality. - let base = ButterflyPtr(vec.as_mut_ptr()); + let base = parallel::SendPtr(vec.as_mut_ptr()); parallel::for_each_index(n_blocks * ts, |g| { let block_base = (g / ts) * block_size; let ind = g % ts; // SAFETY: distinct `g` map to disjoint (hi, lo) `width`-rows. - let hi = unsafe { base.row(block_base + ind * width, width) }; - let lo = unsafe { base.row(block_base + (ts + ind) * width, width) }; + let hi = unsafe { base.slice(block_base + ind * width, width) }; + let lo = unsafe { base.slice(block_base + (ts + ind) * width, width) }; twiddles[ind].apply_to_rows(hi, lo); }); } @@ -267,16 +247,16 @@ fn dft_layer_par_double, M: MultiLayerButterfly> let ts = twiddles_small.len(); let block_size = 4 * ts * width; // == twiddles_large.len() * 2 * width let n_blocks = mat.values.len() / block_size; - let base = ButterflyPtr(mat.values.as_mut_ptr()); + let base = parallel::SendPtr(mat.values.as_mut_ptr()); parallel::for_each_index(n_blocks * ts, |g| { let block_base = (g / ts) * block_size; let ind = g % ts; let row = |k: usize| block_base + (k * ts + ind) * width; // SAFETY: distinct `g` map to disjoint sets of 4 `width`-rows. - let hi_hi = unsafe { base.row(row(0), width) }; - let hi_lo = unsafe { base.row(row(1), width) }; - let lo_hi = unsafe { base.row(row(2), width) }; - let lo_lo = unsafe { base.row(row(3), width) }; + let hi_hi = unsafe { base.slice(row(0), width) }; + let hi_lo = unsafe { base.slice(row(1), width) }; + let lo_hi = unsafe { base.slice(row(2), width) }; + let lo_lo = unsafe { base.slice(row(3), width) }; multi_butterfly.apply_2_layers(((hi_hi, hi_lo), (lo_hi, lo_lo)), ind, twiddles_small, twiddles_large); }); } @@ -321,20 +301,20 @@ fn dft_layer_par_triple, M: MultiLayerButterfly> let ts = twiddles_small.len(); let block_size = 8 * ts * width; // == twiddles_large.len() * 2 * width let n_blocks = mat.values.len() / block_size; - let base = ButterflyPtr(mat.values.as_mut_ptr()); + let base = parallel::SendPtr(mat.values.as_mut_ptr()); parallel::for_each_index(n_blocks * ts, |g| { let block_base = (g / ts) * block_size; let ind = g % ts; let row = |k: usize| block_base + (k * ts + ind) * width; // SAFETY: distinct `g` map to disjoint sets of 8 `width`-rows. - let hi_hi_hi = unsafe { base.row(row(0), width) }; - let hi_hi_lo = unsafe { base.row(row(1), width) }; - let hi_lo_hi = unsafe { base.row(row(2), width) }; - let hi_lo_lo = unsafe { base.row(row(3), width) }; - let lo_hi_hi = unsafe { base.row(row(4), width) }; - let lo_hi_lo = unsafe { base.row(row(5), width) }; - let lo_lo_hi = unsafe { base.row(row(6), width) }; - let lo_lo_lo = unsafe { base.row(row(7), width) }; + let hi_hi_hi = unsafe { base.slice(row(0), width) }; + let hi_hi_lo = unsafe { base.slice(row(1), width) }; + let hi_lo_hi = unsafe { base.slice(row(2), width) }; + let hi_lo_lo = unsafe { base.slice(row(3), width) }; + let lo_hi_hi = unsafe { base.slice(row(4), width) }; + let lo_hi_lo = unsafe { base.slice(row(5), width) }; + let lo_lo_hi = unsafe { base.slice(row(6), width) }; + let lo_lo_lo = unsafe { base.slice(row(7), width) }; multi_butterfly.apply_3_layers( ( ((hi_hi_hi, hi_hi_lo), (hi_lo_hi, hi_lo_lo)), From 72d81988f7cdca7ea4ee8d6a365b8f17f10ee20e Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 30 May 2026 16:22:22 +0400 Subject: [PATCH 35/65] perf: faster `prepare_evals_for_fft_packed_extension` --- crates/whir/src/utils.rs | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/crates/whir/src/utils.rs b/crates/whir/src/utils.rs index 64f78994e..06288dc98 100644 --- a/crates/whir/src/utils.rs +++ b/crates/whir/src/utils.rs @@ -172,22 +172,36 @@ fn prepare_evals_for_fft_packed_extension>>( let full_len = evals.len() << (log_inv_rate + log_packing); let block_size = full_len / n_blocks; let log_block_size = log2_strict_usize(block_size); - let n_blocks_mask = n_blocks - 1; let packing_mask = (1 << log_packing) - 1; let mut out: Vec = unsafe { uninitialized_vec(full_len) }; - parallel::par_for_each_mut(&mut out, |i, slot| { - let block_index = i & n_blocks_mask; - let offset_in_block = i >> folding_factor; - let src_index = ((block_index << log_block_size) + offset_in_block) >> log_inv_rate; - let packed_src_index = src_index >> log_packing; - let offset_in_packing = src_index & packing_mask; - let packed = unsafe { evals.get_unchecked(packed_src_index) }; - let unpacked: &[PFPacking] = packed.as_basis_coefficients_slice(); - *slot = EF::from_basis_coefficients_fn(|j| unsafe { - let u: &PFPacking = unpacked.get_unchecked(j); - *u.as_slice().get_unchecked(offset_in_packing) - }); + if block_size == 0 || n_blocks == 0 { + return out; + } + + let rows_per_band = ((system_info::L1_CACHE_SIZE / 2) / (n_blocks * size_of::())).clamp(1, block_size); + let band_len = rows_per_band * n_blocks; + + parallel::par_chunks_mut(&mut out, band_len, |band_idx, band| { + let row0 = band_idx * rows_per_band; + let n_rows = band.len() / n_blocks; + for col in 0..n_blocks { + let col_base = col << log_block_size; + for r in 0..n_rows { + let src_index = (col_base + row0 + r) >> log_inv_rate; + let packed_src_index = src_index >> log_packing; + let offset_in_packing = src_index & packing_mask; + let packed = unsafe { evals.get_unchecked(packed_src_index) }; + let unpacked: &[PFPacking] = packed.as_basis_coefficients_slice(); + let val = EF::from_basis_coefficients_fn(|j| unsafe { + let u: &PFPacking = unpacked.get_unchecked(j); + *u.as_slice().get_unchecked(offset_in_packing) + }); + unsafe { + *band.get_unchecked_mut(r * n_blocks + col) = val; + } + } + } }); out } From ad8425425519d13dce9158003770c7ddc9979f70 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 30 May 2026 16:30:42 +0400 Subject: [PATCH 36/65] forbid nested parallelism --- crates/backend/parallel/src/lib.rs | 21 ++-- crates/backend/poly/src/mle/mle_single_ref.rs | 10 +- crates/backend/poly/src/utils.rs | 116 ++++++++---------- crates/rec_aggregation/src/bytecode_claims.rs | 20 +-- .../src/quotient_gkr/sumcheck_utils.rs | 4 +- 5 files changed, 75 insertions(+), 96 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index e47ec1350..0fca9a4b9 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -16,11 +16,11 @@ //! the dispatcher spins on. The per-worker `parked` flag is ordered SeqCst against //! `generation` so no wake-up is lost and the unpark syscall is skipped while workers spin. //! -//! ## Nesting falls back to sequential +//! ## Nesting is forbidden //! //! A flat pool can't dispatch from inside a task (would deadlock the dispatch lock), so a -//! `for_each_index` issued from within a pool task runs sequentially inline. The outer -//! level has usually already saturated all cores. +//! dispatch issued from within a pool task panics. The outer level has already saturated all +//! cores, so nested parallelism would buy nothing anyway. //! //! ## Constraint //! @@ -60,8 +60,8 @@ thread_local! { /// Stable id of this thread within the pool. Set once per background worker; /// stays `0` on the dispatching thread (worker 0) and on any non-worker thread. static WORKER_ID: Cell = const { Cell::new(0) }; - /// True while this thread is executing a pool task. A `for_each_index` issued in - /// that state is a nested dispatch and runs sequentially (see module docs). + /// True while this thread is executing a pool task. A dispatch issued in that state is + /// nested parallelism, which is forbidden and panics (see module docs). static IN_TASK: Cell = const { Cell::new(false) }; } @@ -199,7 +199,7 @@ fn drain(pool: &Pool) { // SAFETY: `job.f` points at a `&dyn Fn` borrow held live by the blocked dispatcher. let f = unsafe { job.f.as_ref() }; let n = job.n_tasks; - // Mark this thread as in-task so a nested `for_each_index` runs sequentially + // Mark this thread as in-task so a nested dispatch panics (see `for_each_chunk`) // rather than deadlocking on the dispatch lock. let prev = IN_TASK.replace(true); loop { @@ -228,9 +228,12 @@ fn drain(pool: &Pool) { /// This is the primitive everything else is built on. Range-based (rather than per-index) /// so a reduction can look up its per-worker accumulator once per claimed batch. pub fn for_each_chunk(n_tasks: usize, f: F) { - // Trivial sizes, single-core builds, and nested dispatches (called from within a - // pool task) all run sequentially — the last avoids deadlocking on the lock. - if NUM_THREADS <= 1 || n_tasks <= 1 || IN_TASK.get() { + // Nesting is forbidden (would deadlock the dispatch lock): panic rather than silently + // running sequentially, so an accidental nested dispatch is caught instead of going slow. + assert!(!IN_TASK.get(), "nested parallel dispatch from within a pool task"); + + // Trivial sizes and single-core builds run sequentially inline. + if NUM_THREADS <= 1 || n_tasks <= 1 { if n_tasks > 0 { f(0, n_tasks); } diff --git a/crates/backend/poly/src/mle/mle_single_ref.rs b/crates/backend/poly/src/mle/mle_single_ref.rs index 61d607d76..269884fdf 100644 --- a/crates/backend/poly/src/mle/mle_single_ref.rs +++ b/crates/backend/poly/src/mle/mle_single_ref.rs @@ -119,13 +119,15 @@ impl<'a, EF: ExtensionField>> MleRef<'a, EF> { pub fn fold(&self, alpha: EF) -> MleOwned { match self { - Self::Base(pols) => MleOwned::Extension(fold_multilinear(pols, alpha, &|a, b| b * a)), - Self::Extension(pols) => MleOwned::Extension(fold_multilinear(pols, alpha, &|a, b| b * a)), + Self::Base(pols) => MleOwned::Extension(fold_multilinear(pols, alpha, &|a, b| b * a, false)), + Self::Extension(pols) => MleOwned::Extension(fold_multilinear(pols, alpha, &|a, b| b * a, false)), Self::BasePacked(pols) => { let alpha_packed = EFPacking::::from(alpha); - MleOwned::ExtensionPacked(fold_multilinear(pols, alpha_packed, &|a, b| b * a)) + MleOwned::ExtensionPacked(fold_multilinear(pols, alpha_packed, &|a, b| b * a, false)) + } + Self::ExtensionPacked(pols) => { + MleOwned::ExtensionPacked(fold_multilinear(pols, alpha, &|a, b| a * b, false)) } - Self::ExtensionPacked(pols) => MleOwned::ExtensionPacked(fold_multilinear(pols, alpha, &|a, b| a * b)), } } } diff --git a/crates/backend/poly/src/utils.rs b/crates/backend/poly/src/utils.rs index 4f45f2826..cc729bc74 100644 --- a/crates/backend/poly/src/utils.rs +++ b/crates/backend/poly/src/utils.rs @@ -67,33 +67,24 @@ pub const fn must_unpack_multilinears(n_vars: usize) -> bool { n_vars <= 1 + packing_log_width::() } -pub fn batch_fold_multilinears< - EF: PrimeCharacteristicRing + Copy + Send + Sync, - IF: Copy + Sub + Send + Sync, - OF: Copy + Add + Send + Sync, - F: Fn(IF, EF) -> OF + Sync + Send, ->( - polys: &[&[IF]], - alpha: EF, - mul_if_of: F, -) -> Vec> { - let total_size: usize = polys.iter().map(|p| p.len()).sum(); - if total_size < PARALLEL_THRESHOLD { - polys - .iter() - .map(|poly| fold_multilinear(poly, alpha, &mul_if_of)) - .collect() +/// Fill `len` output slots with `compute(i)`, parallelizing via the pool when the work is +/// large enough. `seq` forces the sequential path: the batched wrappers below dispatch one +/// pool task per poly, so their inner fold must not nest a parallel dispatch (which would +/// panic in [`parallel`]). +#[inline] +fn fold_fill OF + Sync>(len: usize, seq: bool, compute: C) -> Vec { + let mut res = unsafe { uninitialized_vec(len) }; + if seq || len < PARALLEL_THRESHOLD { + for (i, r) in res.iter_mut().enumerate() { + *r = compute(i); + } } else { - // One task per poly (inner fold runs sequentially via the pool's nesting fallback). - let mut out: Vec> = (0..polys.len()).map(|_| Vec::new()).collect(); - parallel::par_chunks_mut(&mut out, 1, |i, slot| { - slot[0] = fold_multilinear(polys[i], alpha, &mul_if_of); - }); - out + parallel::par_for_each_mut(&mut res, |i, r| *r = compute(i)); } + res } -pub fn fold_multilinear_lsb< +fn fold_multilinear_lsb< EF: PrimeCharacteristicRing + Copy + Send + Sync, IF: Copy + Sub + Send + Sync, OF: Copy + Add + Send + Sync, @@ -102,22 +93,14 @@ pub fn fold_multilinear_lsb< m: &[IF], alpha: EF, mul_if_of: &Mul, + seq: bool, ) -> Vec { - let new_size = m.len() / 2; - let mut res = unsafe { uninitialized_vec(new_size) }; - let compute = |(c, r_v): (&[IF], &mut OF)| { - *r_v = mul_if_of(c[1] - c[0], alpha) + c[0]; - }; - if new_size < PARALLEL_THRESHOLD { - m.chunks_exact(2).zip(res.iter_mut()).for_each(compute); - } else { - parallel::par_for_each_mut(&mut res, |j, r_v| { - compute((&m[2 * j..2 * j + 2], r_v)); - }); - } - res + fold_fill(m.len() / 2, seq, |j| { + mul_if_of(m[2 * j + 1] - m[2 * j], alpha) + m[2 * j] + }) } +/// Fold `m` at variable `bit`. `seq` forces sequential execution (see [`fold_fill`]). pub fn fold_multilinear_at_bit< EF: PrimeCharacteristicRing + Copy + Send + Sync, IF: Copy + Sub + Send + Sync, @@ -128,38 +111,24 @@ pub fn fold_multilinear_at_bit< alpha: EF, bit: usize, mul_if_of: &Mul, + seq: bool, ) -> Vec { - let new_size = m.len() / 2; assert!(m.len() >= 2 * (1 << bit), "bit out of range for slice length"); - if bit == 0 { - return fold_multilinear_lsb(m, alpha, mul_if_of); + return fold_multilinear_lsb(m, alpha, mul_if_of, seq); } - let stride = 1usize << bit; let lo_mask = stride - 1; - let mut res = unsafe { uninitialized_vec(new_size) }; - - let compute = |new_j: usize| { + fold_fill(m.len() / 2, seq, |new_j| { let i_hi = new_j >> bit; let i_lo = new_j & lo_mask; let i0 = (i_hi << (bit + 1)) | i_lo; let i1 = i0 | stride; mul_if_of(m[i1] - m[i0], alpha) + m[i0] - }; - - if new_size < PARALLEL_THRESHOLD { - for (new_j, res_v) in res.iter_mut().enumerate() { - *res_v = compute(new_j); - } - } else { - parallel::par_for_each_mut(&mut res, |new_j, res_v| { - *res_v = compute(new_j); - }); - } - res + }) } +/// Fold `m` at its top variable. `seq` forces sequential execution (see [`fold_fill`]). pub fn fold_multilinear< EF: PrimeCharacteristicRing + Copy + Send + Sync, IF: Copy + Sub + Send + Sync, @@ -169,20 +138,35 @@ pub fn fold_multilinear< m: &[IF], alpha: EF, mul_if_of: &F, + seq: bool, ) -> Vec { let new_size = m.len() / 2; - let mut res = unsafe { uninitialized_vec(new_size) }; + fold_fill(new_size, seq, |i| mul_if_of(m[i + new_size] - m[i], alpha) + m[i]) +} - if new_size < PARALLEL_THRESHOLD { - for i in 0..new_size { - res[i] = mul_if_of(m[i + new_size] - m[i], alpha) + m[i]; - } +pub fn batch_fold_multilinears< + EF: PrimeCharacteristicRing + Copy + Send + Sync, + IF: Copy + Sub + Send + Sync, + OF: Copy + Add + Send + Sync, + F: Fn(IF, EF) -> OF + Sync + Send, +>( + polys: &[&[IF]], + alpha: EF, + mul_if_of: F, +) -> Vec> { + let total_size: usize = polys.iter().map(|p| p.len()).sum(); + if total_size < PARALLEL_THRESHOLD { + polys + .iter() + .map(|poly| fold_multilinear(poly, alpha, &mul_if_of, true)) + .collect() } else { - parallel::par_for_each_mut(&mut res, |i, res_v| { - *res_v = mul_if_of(m[i + new_size] - m[i], alpha) + m[i]; + let mut out: Vec> = (0..polys.len()).map(|_| Vec::new()).collect(); + parallel::par_chunks_mut(&mut out, 1, |i, slot| { + slot[0] = fold_multilinear(polys[i], alpha, &mul_if_of, true); }); + out } - res } pub fn batch_fold_multilinears_at_bit< @@ -196,17 +180,17 @@ pub fn batch_fold_multilinears_at_bit< bit: usize, mul_if_of: F, ) -> Vec> { + // See `batch_fold_multilinears`: one task per poly, inner fold forced sequential. let total_size: usize = polys.iter().map(|p| p.len()).sum(); if total_size < PARALLEL_THRESHOLD { polys .iter() - .map(|poly| fold_multilinear_at_bit(poly, alpha, bit, &mul_if_of)) + .map(|poly| fold_multilinear_at_bit(poly, alpha, bit, &mul_if_of, true)) .collect() } else { - // One task per poly (inner fold runs sequentially via the pool's nesting fallback). let mut out: Vec> = (0..polys.len()).map(|_| Vec::new()).collect(); parallel::par_chunks_mut(&mut out, 1, |i, slot| { - slot[0] = fold_multilinear_at_bit(polys[i], alpha, bit, &mul_if_of); + slot[0] = fold_multilinear_at_bit(polys[i], alpha, bit, &mul_if_of, true); }); out } diff --git a/crates/rec_aggregation/src/bytecode_claims.rs b/crates/rec_aggregation/src/bytecode_claims.rs index d20ade0ee..081347b8a 100644 --- a/crates/rec_aggregation/src/bytecode_claims.rs +++ b/crates/rec_aggregation/src/bytecode_claims.rs @@ -64,21 +64,11 @@ pub(crate) fn reduce_bytecode_claims(verified: &[InnerVerified]) -> ReducedBytec let alpha: EF = reduction_prover.sample(); let alpha_powers: Vec = alpha.powers().take(n_claims).collect(); - let weights_packed = parallel::map_reduce( - n_claims, - Vec::>::new, - |i| eval_eq_packed_scaled(&claims[i].point.0, alpha_powers[i]), - |mut acc, eq_i| { - if acc.is_empty() { - eq_i - } else { - for (w, e) in acc.iter_mut().zip(&eq_i) { - *w += *e; - } - acc - } - }, - ); + let n_vars = claims[0].point.0.len(); + let mut weights_packed = EFPacking::::zero_vec(1 << (n_vars - packing_log_width::())); + for (claim, &alpha_pow) in claims.iter().zip(&alpha_powers) { + compute_eval_eq_packed::(&claim.point.0, &mut weights_packed, alpha_pow); + } let claimed_sum: EF = dot_product(claims.iter().map(|c| c.value), alpha_powers.iter().copied()); diff --git a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs index 7c85d0e2f..49e0cc0cf 100644 --- a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs +++ b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs @@ -255,8 +255,8 @@ pub(super) fn run_phase1_sumcheck<'a, EF: ExtensionField>>( if let Some(prev_r) = pending_r { let prev_bit = layer_chunk_log - 1 - w; let mul = |x: EFPacking, a: EF| x * a; - nums = Cow::Owned(fold_multilinear_at_bit(nums.as_ref(), prev_r, prev_bit, &mul)); - dens = Cow::Owned(fold_multilinear_at_bit(dens.as_ref(), prev_r, prev_bit, &mul)); + nums = Cow::Owned(fold_multilinear_at_bit(nums.as_ref(), prev_r, prev_bit, &mul, false)); + dens = Cow::Owned(fold_multilinear_at_bit(dens.as_ref(), prev_r, prev_bit, &mul, false)); } let nums_nat = unpack_and_unreverse_active::(nums.as_ref(), layer_chunk_log); From 34bf1945423d96b5943665121fd6f306f3983dbc Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 30 May 2026 20:30:06 +0200 Subject: [PATCH 37/65] improve parallele lib.rs --- crates/backend/parallel/src/lib.rs | 122 +++++++++++++++++------------ 1 file changed, 71 insertions(+), 51 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 0fca9a4b9..f2af04e5e 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -18,9 +18,9 @@ //! //! ## Nesting is forbidden //! -//! A flat pool can't dispatch from inside a task (would deadlock the dispatch lock), so a -//! dispatch issued from within a pool task panics. The outer level has already saturated all -//! cores, so nested parallelism would buy nothing anyway. +//! A flat pool can't dispatch from inside a task (it would deadlock on the dispatch lock); +//! an `IN_TASK` guard catches that and panics instead. The outer level has already saturated +//! all cores, so nested parallelism would buy nothing anyway. //! //! ## Constraint //! @@ -83,6 +83,16 @@ struct Job { n_tasks: usize, } +/// Per-worker state touched by the park/unpark protocol (indexed by worker id; slot 0, +/// the dispatcher, is unused — it never parks). +#[derive(Debug)] +struct Worker { + /// "Currently parked" flag, ordered SeqCst against `Pool::generation`. + parked: AtomicBool, + /// Thread handle for `unpark`, published once by the worker on start-up. + handle: OnceLock, +} + struct Pool { /// Current job. Written by the dispatcher before bumping `generation`, read by /// workers after they observe the bump; `generation` supplies the happens-before. @@ -93,11 +103,8 @@ struct Pool { counter: AtomicUsize, /// Background workers still draining the current dispatch; dispatcher spins to 0. working: AtomicUsize, - shutdown: AtomicBool, - /// Per-worker "currently parked" flags (indexed by worker id; slot 0 unused). - parked: Vec, - /// Per-worker thread handles for `unpark` (indexed by worker id; slot 0 unused). - handles: Vec>, + /// Per-worker park flag + unpark handle (indexed by worker id; slot 0 unused). + workers: Vec, /// Serializes dispatchers: only one thread may drive the pool at a time. dispatch: Mutex<()>, } @@ -131,9 +138,12 @@ fn pool() -> &'static Pool { generation: AtomicUsize::new(0), counter: AtomicUsize::new(0), working: AtomicUsize::new(0), - shutdown: AtomicBool::new(false), - parked: (0..n).map(|_| AtomicBool::new(false)).collect(), - handles: (0..n).map(|_| OnceLock::new()).collect(), + workers: (0..n) + .map(|_| Worker { + parked: AtomicBool::new(false), + handle: OnceLock::new(), + }) + .collect(), dispatch: Mutex::new(()), })); for id in 1..n { @@ -148,49 +158,53 @@ fn pool() -> &'static Pool { fn worker_main(pool: &'static Pool, id: usize) { WORKER_ID.with(|c| c.set(id)); - let _ = pool.handles[id].set(std::thread::current()); + let _ = pool.workers[id].handle.set(std::thread::current()); + // The pool is leaked and lives for the whole process: workers never shut down, they + // die with the process. Each iteration services exactly one dispatch. let mut last_gen = 0usize; loop { - let mut spins = 0u32; - let g = loop { - let g = pool.generation.load(Ordering::Acquire); - if g != last_gen { - break g; - } - if pool.shutdown.load(Ordering::Acquire) { - return; - } - if spins < SPIN_LIMIT { - spins += 1; - std::hint::spin_loop(); - } else { - // About to park. Publish it, then re-check `generation`: by SeqCst - // total order with the dispatcher's `generation` bump and `parked` - // load, at least one side sees the other, so no wake-up is lost. - pool.parked[id].store(true, Ordering::SeqCst); - if pool.generation.load(Ordering::SeqCst) != last_gen { - pool.parked[id].store(false, Ordering::SeqCst); - } else if pool.shutdown.load(Ordering::SeqCst) { - pool.parked[id].store(false, Ordering::SeqCst); - return; - } else { - std::thread::park(); - pool.parked[id].store(false, Ordering::SeqCst); - } - spins = 0; - } - }; - last_gen = g; + last_gen = wait_for_dispatch(pool, id, last_gen); drain(pool); pool.working.fetch_sub(1, Ordering::Release); } } +/// Block until the dispatcher publishes a new job, returning its generation. Spins up to +/// [`SPIN_LIMIT`] times (so back-to-back dispatches pay no syscall), then parks. +/// +/// The park is the delicate part: we publish `parked = true` and only then re-check +/// `generation`, both under `SeqCst`. That single total order is also observed by the +/// dispatcher's `generation` bump and subsequent `parked` load, so for every dispatch at +/// least one side sees the other — a wake-up can never be lost. +fn wait_for_dispatch(pool: &Pool, id: usize, last_gen: usize) -> usize { + let mut spins = 0u32; + loop { + let g = pool.generation.load(Ordering::Acquire); + if g != last_gen { + return g; + } + if spins < SPIN_LIMIT { + spins += 1; + std::hint::spin_loop(); + continue; + } + // Spun out. Announce the intent to park, then re-check under SeqCst: park only if + // nothing changed in the meantime; otherwise loop back and re-observe at the top. + pool.workers[id].parked.store(true, Ordering::SeqCst); + if pool.generation.load(Ordering::SeqCst) == last_gen { + std::thread::park(); + } + pool.workers[id].parked.store(false, Ordering::SeqCst); + spins = 0; + } +} + /// Claim and run task indices until the counter is exhausted, using **guided -/// self-scheduling**: each claim grabs `remaining / (NUM_THREADS * 2)`, capped at -/// [`MAX_CLAIM_BATCH`]. Large early batches keep counter contention low; the proportional -/// shrink keeps the tail finely divided for load balance. +/// self-scheduling**: each claim grabs `remaining / (NUM_THREADS * 2)`, clamped to +/// `1..=`[`MAX_CLAIM_BATCH`] (the floor of 1 guarantees progress on tiny tails). Large +/// early batches keep counter contention low; the proportional shrink keeps the tail +/// finely divided for load balance. fn drain(pool: &Pool) { // SAFETY: the dispatcher published `Some(job)` before the generation bump this // worker just observed, and overwrites it only on the next dispatch (gated on @@ -244,9 +258,12 @@ pub fn for_each_chunk(n_tasks: usize, f: F) { let _guard = pool.dispatch.lock().unwrap(); let n = NUM_THREADS; - // SAFETY: erase the borrow's lifetime so the closure can live in the `'static` - // `Job`. The dispatcher blocks on `working` below before returning, so `f` - // outlives every worker dereference of this pointer. + // SAFETY: erase the borrow's lifetime so the closure can live in the `'static` `Job`. + // The dispatcher blocks on `working` below before returning, so `f` outlives every + // worker dereference of this pointer. `transmute` is required here — NOT a `&dyn -> *const` + // cast: coercing to a bare `*const dyn` defaults the trait object to `'static`, which would + // force `F: 'static` (E0310) and break every non-'static caller. The transmute reinterprets + // the same (data, vtable) fat pointer without imposing that bound. let f_ref: &(dyn Fn(usize, usize) + Sync) = &f; let f_erased: NonNull = unsafe { std::mem::transmute(NonNull::from(f_ref)) }; @@ -260,9 +277,9 @@ pub fn for_each_chunk(n_tasks: usize, f: F) { pool.generation.fetch_add(1, Ordering::SeqCst); // Wake only workers that actually parked; hot (spinning) ones see the bump for free. - for id in 1..n { - if pool.parked[id].load(Ordering::SeqCst) - && let Some(t) = pool.handles[id].get() + for worker in &pool.workers[1..n] { + if worker.parked.load(Ordering::SeqCst) + && let Some(t) = worker.handle.get() { t.unpark(); } @@ -278,6 +295,9 @@ pub fn for_each_chunk(n_tasks: usize, f: F) { /// Run `f(i)` for every `i` in `0..n_tasks`, in parallel across the pool. Blocks until /// all tasks complete; the dispatching thread participates as worker 0. +/// `#[inline]` so the trivial range→index adapter folds into the monomorphized +/// [`for_each_chunk`] (mirrors [`par_for_each_mut`]). +#[inline] pub fn for_each_index(n_tasks: usize, f: F) { for_each_chunk(n_tasks, |start, end| { for i in start..end { From 63cfa7b90722dd7f37722961584ab3bd97a2d51c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 31 May 2026 03:13:31 +0400 Subject: [PATCH 38/65] wip --- Cargo.lock | 59 ++----- Cargo.toml | 9 +- crates/backend/zk-alloc/Cargo.toml | 14 ++ crates/backend/zk-alloc/src/lib.rs | 198 ++++++++++++++++++++++++ crates/backend/zk-alloc/src/syscall.rs | 163 +++++++++++++++++++ crates/rec_aggregation/Cargo.toml | 3 + crates/rec_aggregation/src/benchmark.rs | 10 ++ src/lib.rs | 45 +++--- src/main.rs | 17 +- tests/test_aggregation.rs | 5 +- tests/test_zk_alloc.rs | 27 ++++ 11 files changed, 463 insertions(+), 87 deletions(-) create mode 100644 crates/backend/zk-alloc/Cargo.toml create mode 100644 crates/backend/zk-alloc/src/lib.rs create mode 100644 crates/backend/zk-alloc/src/syscall.rs create mode 100644 tests/test_zk_alloc.rs diff --git a/Cargo.lock b/Cargo.lock index 0bb8c2bce..cc84bd751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,16 +138,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "cc" -version = "1.2.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" -dependencies = [ - "find-msvc-tools", - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -269,12 +259,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "cty" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" - [[package]] name = "digest" version = "0.10.7" @@ -320,12 +304,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - [[package]] name = "foldhash" version = "0.1.5" @@ -491,8 +469,6 @@ dependencies = [ "clap", "lean_vm", "libc", - "libmimalloc-sys", - "mimalloc", "rand", "rec_aggregation", "serde_json", @@ -500,6 +476,7 @@ dependencies = [ "system-info", "utils", "xmss", + "zk-alloc", ] [[package]] @@ -564,16 +541,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libmimalloc-sys" -version = "0.1.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" -dependencies = [ - "cc", - "cty", -] - [[package]] name = "lock_api" version = "0.4.14" @@ -613,15 +580,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "mimalloc" -version = "0.1.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" -dependencies = [ - "libmimalloc-sys", -] - [[package]] name = "mt-air" version = "0.1.0" @@ -953,6 +911,7 @@ dependencies = [ "tracing", "utils", "xmss", + "zk-alloc", ] [[package]] @@ -1066,12 +1025,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - [[package]] name = "smallvec" version = "1.15.1" @@ -1481,6 +1434,14 @@ dependencies = [ "utils", ] +[[package]] +name = "zk-alloc" +version = "0.1.0" +dependencies = [ + "libc", + "system-info", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index ce778c312..7270ec157 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/backend/sumcheck", "crates/backend/system-info", "crates/backend/parallel", + "crates/backend/zk-alloc", ] [workspace.lints] @@ -63,6 +64,7 @@ rec_aggregation = { path = "crates/rec_aggregation" } backend = { path = "crates/backend" } system-info = { path = "crates/backend/system-info" } parallel = { path = "crates/backend/parallel" } +zk-alloc = { path = "crates/backend/zk-alloc" } # External sha3 = "0.11.0" @@ -82,14 +84,13 @@ include_dir = "0.7" [features] prox-gaps-conjecture = ["rec_aggregation/prox-gaps-conjecture"] -# Build with the plain system allocator instead of mimalloc (for comparison/debugging). -standard-alloc = [] +# Build with the plain system allocator instead of zk-alloc (for comparison/debugging). +standard-alloc = ["rec_aggregation/standard-alloc"] [dependencies] clap.workspace = true rec_aggregation.workspace = true -mimalloc = "0.1" -libmimalloc-sys = { version = "0.1", features = ["extended"] } +zk-alloc.workspace = true libc = "0.2" rand.workspace = true sub_protocols.workspace = true diff --git a/crates/backend/zk-alloc/Cargo.toml b/crates/backend/zk-alloc/Cargo.toml new file mode 100644 index 000000000..0c4ab6a5f --- /dev/null +++ b/crates/backend/zk-alloc/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zk-alloc" +version.workspace = true +edition.workspace = true +description = "Bump+reset arena allocator for ZK proving workloads" + +[dependencies] +system-info.workspace = true + +[target.'cfg(not(all(target_os = "linux", target_arch = "x86_64")))'.dependencies] +libc = "0.2" + +[lints] +workspace = true diff --git a/crates/backend/zk-alloc/src/lib.rs b/crates/backend/zk-alloc/src/lib.rs new file mode 100644 index 000000000..6c5f5a10a --- /dev/null +++ b/crates/backend/zk-alloc/src/lib.rs @@ -0,0 +1,198 @@ +//! Bump-pointer arena allocator. +//! +//! One mmap region split into per-thread slabs. Allocation = increment a thread-local +//! pointer; free = no-op. `begin_phase()` resets the arena: each thread's next +//! allocation starts over at the beginning of its slab, overwriting the previous +//! phase's data. Allocations that don't fit (too large, or beyond `MAX_THREADS`) fall +//! back to the system allocator. +//! +//! ```ignore +//! init(); // once, at process start +//! loop { +//! begin_phase(); // arena ON; slabs reset lazily +//! let res = heavy_work(); // fast increments +//! end_phase(); // arena OFF; new allocations go to System +//! let copy = res.clone(); // detach from arena before next phase resets it +//! } +//! ``` + +use std::alloc::{GlobalAlloc, Layout}; +use std::cell::Cell; +use std::sync::Once; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +use system_info::NUM_THREADS; + +mod syscall; + +const SLAB_SIZE: usize = 8 << 30; // 8GB +const SLACK: usize = 4; // SLACK absorbs the main thread and any non-rayon helpers. +const MAX_THREADS: usize = NUM_THREADS + SLACK; +const REGION_SIZE: usize = SLAB_SIZE * MAX_THREADS; + +#[derive(Debug)] +pub struct ZkAllocator; + +/// Incremented by `begin_phase()`. Every thread caches the last value it saw in +/// `ARENA_GEN`; when they differ, the thread resets its allocation cursor to the start +/// of its slab on the next allocation. This is how a single store on the main thread +/// "resets" every other thread's slab without any cross-thread synchronization. +static GENERATION: AtomicUsize = AtomicUsize::new(0); + +/// Master switch for the arena. `true` (set by `begin_phase`) routes allocations +/// through the arena; `false` (set by `end_phase`) routes them to the system allocator. +static ARENA_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// Base address of the mmap'd region, or `0` before `ensure_region` runs. Read on +/// every `dealloc` to test whether a pointer belongs to us. +static REGION_BASE: AtomicUsize = AtomicUsize::new(0); + +/// Synchronizes the one-time mmap so concurrent first-allocators don't race. +static REGION_INIT: Once = Once::new(); + +/// Monotonic counter handed out to threads to pick their slab. `fetch_add`'d once per +/// thread on its first arena allocation. Threads that get `idx >= MAX_THREADS` mark +/// themselves `ARENA_NO_SLAB` and permanently fall through to the system allocator. +static THREAD_IDX: AtomicUsize = AtomicUsize::new(0); + +thread_local! { + /// Where this thread's next allocation lands. Advanced past each allocation. + static ARENA_PTR: Cell = const { Cell::new(0) }; + /// One past the last byte of this thread's slab. An alloc fits iff + /// `aligned + size <= ARENA_END`. + static ARENA_END: Cell = const { Cell::new(0) }; + /// Base address of this thread's slab (`0` = not yet claimed). On reset, + /// `ARENA_PTR` is set back to this value. + static ARENA_BASE: Cell = const { Cell::new(0) }; + /// Last `GENERATION` value this thread observed. When the global moves past + /// this, the next allocation resets `ARENA_PTR` to `ARENA_BASE` and updates + /// this field. + static ARENA_GEN: Cell = const { Cell::new(0) }; + /// `true` if this thread was created after `MAX_THREADS` was already exhausted. + /// Such threads skip arena logic entirely and always go to the system allocator. + static ARENA_NO_SLAB: Cell = const { Cell::new(false) }; +} + +/// Returns the base address of the mmap'd region, mapping it on the first call. +fn ensure_region() -> usize { + REGION_INIT.call_once(|| { + // SAFETY: mmap_anonymous returns a page-aligned pointer or null. MAP_NORESERVE + // means no physical memory is committed until pages are touched. + let ptr = unsafe { syscall::mmap_anonymous(REGION_SIZE) }; + if ptr.is_null() { + std::process::abort(); + } + unsafe { syscall::madvise(ptr, REGION_SIZE, syscall::MADV_NOHUGEPAGE) }; + REGION_BASE.store(ptr as usize, Ordering::Release); + }); + REGION_BASE.load(Ordering::Acquire) +} + +/// Call once at process start, before any `begin_phase()`. +pub fn init() { + let actual_num_threads = std::thread::available_parallelism().unwrap().get(); + assert_eq!( + actual_num_threads, NUM_THREADS, + "built for {NUM_THREADS} threads but this machine reports {actual_num_threads} -> please rebuild`" + ); +} + +/// Activates the arena and resets every thread's slab. All allocations until the next +/// `end_phase()` go to the arena; the previous phase's data is overwritten in place. +pub fn begin_phase() { + let prev_active = ARENA_ACTIVE.swap(true, Ordering::Release); + assert!( + !prev_active, + "begin_phase() called while another phase is already active — phases must not nest" + ); + GENERATION.fetch_add(1, Ordering::Release); +} + +/// Deactivates the arena. New allocations go to the system allocator; existing arena +/// pointers stay valid until the next `begin_phase()` resets the slabs. +/// +/// Unlike the rayon-based build (which needed `flush_rayon` to drain crossbeam's +/// arena-allocated injector blocks), the in-house `parallel` pool allocates its state +/// once at startup and nothing per-dispatch, so no flush is required here. +pub fn end_phase() { + ARENA_ACTIVE.store(false, Ordering::Release); +} + +#[cold] +#[inline(never)] +unsafe fn arena_alloc_cold(size: usize, align: usize) -> *mut u8 { + let generation = GENERATION.load(Ordering::Relaxed); + if !ARENA_NO_SLAB.get() && ARENA_GEN.get() != generation { + let mut base = ARENA_BASE.get(); + if base == 0 { + let region = ensure_region(); + let idx = THREAD_IDX.fetch_add(1, Ordering::Relaxed); + if idx >= MAX_THREADS { + ARENA_NO_SLAB.set(true); + return unsafe { std::alloc::System.alloc(Layout::from_size_align_unchecked(size, align)) }; + } + base = region + idx * SLAB_SIZE; + ARENA_BASE.set(base); + ARENA_END.set(base + SLAB_SIZE); + } + ARENA_PTR.set(base); + ARENA_GEN.set(generation); + let aligned = base.next_multiple_of(align); + let new_ptr = aligned + size; + if new_ptr <= ARENA_END.get() { + ARENA_PTR.set(new_ptr); + return aligned as *mut u8; + } + } + unsafe { std::alloc::System.alloc(Layout::from_size_align_unchecked(size, align)) } +} + +// SAFETY: All pointers returned are either from our mmap'd region (valid, aligned, +// non-overlapping per thread) or from System. The arena is thread-local so no data +// races. Relaxed ordering on ARENA_ACTIVE/GENERATION is sound: worst case a thread +// sees a stale value and does one extra system-alloc before picking up the new +// generation on the next call. +unsafe impl GlobalAlloc for ZkAllocator { + #[inline(always)] + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + if ARENA_ACTIVE.load(Ordering::Relaxed) { + let generation = GENERATION.load(Ordering::Relaxed); + if ARENA_GEN.get() == generation { + let align = layout.align(); + let aligned = (ARENA_PTR.get() + align - 1) & !(align - 1); + let new_ptr = aligned + layout.size(); + if new_ptr <= ARENA_END.get() { + ARENA_PTR.set(new_ptr); + return aligned as *mut u8; + } + } + return unsafe { arena_alloc_cold(layout.size(), layout.align()) }; + } + unsafe { std::alloc::System.alloc(layout) } + } + + #[inline(always)] + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + let addr = ptr as usize; + let base = REGION_BASE.load(Ordering::Relaxed); + if base != 0 && addr >= base && addr < base + REGION_SIZE { + return; // arena-owned pointer — free is a no-op + } + unsafe { std::alloc::System.dealloc(ptr, layout) }; + } + + #[inline(always)] + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + if new_size <= layout.size() { + return ptr; + } + // SAFETY: new_size > layout.size() > 0, align unchanged from valid layout. + let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) }; + let new_ptr = unsafe { self.alloc(new_layout) }; + if !new_ptr.is_null() { + unsafe { std::ptr::copy(ptr, new_ptr, layout.size()) }; + unsafe { self.dealloc(ptr, layout) }; + } + new_ptr + } +} diff --git a/crates/backend/zk-alloc/src/syscall.rs b/crates/backend/zk-alloc/src/syscall.rs new file mode 100644 index 000000000..13d71531d --- /dev/null +++ b/crates/backend/zk-alloc/src/syscall.rs @@ -0,0 +1,163 @@ +// Raw syscalls instead of libc wrappers to avoid reentrancy: libc's mmap/madvise +// may internally call malloc, which would deadlock when called from inside +// #[global_allocator]. + +#[cfg(all(target_os = "linux", target_arch = "x86_64"))] +mod imp { + use std::ptr; + + const SYS_MMAP: usize = 9; + const SYS_MADVISE: usize = 28; + + const PROT_READ: usize = 1; + const PROT_WRITE: usize = 2; + const MAP_PRIVATE: usize = 0x02; + const MAP_ANONYMOUS: usize = 0x20; + const MAP_NORESERVE: usize = 0x4000; + + pub const MADV_NOHUGEPAGE: usize = 15; + + #[inline] + unsafe fn syscall6(nr: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) -> isize { + let ret: isize; + unsafe { + std::arch::asm!( + "syscall", + inlateout("rax") nr as isize => ret, + in("rdi") a1, + in("rsi") a2, + in("rdx") a3, + in("r10") a4, + in("r8") a5, + in("r9") a6, + lateout("rcx") _, + lateout("r11") _, + options(nostack), + ); + } + ret + } + + #[inline] + unsafe fn syscall3(nr: usize, a1: usize, a2: usize, a3: usize) -> isize { + let ret: isize; + unsafe { + std::arch::asm!( + "syscall", + inlateout("rax") nr as isize => ret, + in("rdi") a1, + in("rsi") a2, + in("rdx") a3, + lateout("rcx") _, + lateout("r11") _, + lateout("r10") _, + options(nostack), + ); + } + ret + } + + #[inline] + pub unsafe fn mmap_anonymous(size: usize) -> *mut u8 { + let flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE; + let ret = unsafe { syscall6(SYS_MMAP, 0, size, PROT_READ | PROT_WRITE, flags, usize::MAX, 0) }; + if ret < 0 { ptr::null_mut() } else { ret as *mut u8 } + } + + #[inline] + pub unsafe fn madvise(ptr: *mut u8, size: usize, advice: usize) { + unsafe { syscall3(SYS_MADVISE, ptr as usize, size, advice) }; + } +} + +#[cfg(all(target_os = "linux", target_arch = "aarch64"))] +mod imp { + use std::ptr; + + const SYS_MMAP: usize = 222; + const SYS_MADVISE: usize = 233; + + const PROT_READ: usize = 1; + const PROT_WRITE: usize = 2; + const MAP_PRIVATE: usize = 0x02; + const MAP_ANONYMOUS: usize = 0x20; + const MAP_NORESERVE: usize = 0x4000; + + pub const MADV_NOHUGEPAGE: usize = 15; + + #[inline] + unsafe fn syscall6(nr: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) -> isize { + let ret: isize; + unsafe { + std::arch::asm!( + "svc 0", + in("x8") nr, + inlateout("x0") a1 as isize => ret, + in("x1") a2, + in("x2") a3, + in("x3") a4, + in("x4") a5, + in("x5") a6, + options(nostack), + ); + } + ret + } + + #[inline] + unsafe fn syscall3(nr: usize, a1: usize, a2: usize, a3: usize) -> isize { + let ret: isize; + unsafe { + std::arch::asm!( + "svc 0", + in("x8") nr, + inlateout("x0") a1 as isize => ret, + in("x1") a2, + in("x2") a3, + options(nostack), + ); + } + ret + } + + #[inline] + pub unsafe fn mmap_anonymous(size: usize) -> *mut u8 { + let flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE; + let ret = unsafe { syscall6(SYS_MMAP, 0, size, PROT_READ | PROT_WRITE, flags, usize::MAX, 0) }; + if ret < 0 { ptr::null_mut() } else { ret as *mut u8 } + } + + #[inline] + pub unsafe fn madvise(ptr: *mut u8, size: usize, advice: usize) { + unsafe { syscall3(SYS_MADVISE, ptr as usize, size, advice) }; + } +} + +#[cfg(not(all(target_os = "linux", any(target_arch = "x86_64", target_arch = "aarch64"))))] +mod imp { + use std::ptr; + + pub const MADV_NOHUGEPAGE: usize = 15; + + #[inline] + pub unsafe fn mmap_anonymous(size: usize) -> *mut u8 { + // MAP_NORESERVE is Linux-only. macOS lazily backs anonymous mappings + // with physical memory by default, so the large virtual reservation + // is fine without NORESERVE. + let prot = libc::PROT_READ | libc::PROT_WRITE; + let flags = libc::MAP_PRIVATE | libc::MAP_ANON; + let ret = unsafe { libc::mmap(ptr::null_mut(), size, prot, flags, -1, 0) }; + if ret == libc::MAP_FAILED { + ptr::null_mut() + } else { + ret.cast::() + } + } + + #[inline] + pub unsafe fn madvise(_ptr: *mut u8, _size: usize, _advice: usize) { + // The advice values we pass are Linux-specific. + } +} + +pub use imp::{MADV_NOHUGEPAGE, madvise, mmap_anonymous}; diff --git a/crates/rec_aggregation/Cargo.toml b/crates/rec_aggregation/Cargo.toml index a28af7b9b..7cadde763 100644 --- a/crates/rec_aggregation/Cargo.toml +++ b/crates/rec_aggregation/Cargo.toml @@ -8,11 +8,14 @@ workspace = true [features] prox-gaps-conjecture = ["lean_prover/prox-gaps-conjecture"] +# Skip the zk-alloc bump-arena phase ceremony (use the plain system allocator). +standard-alloc = [] [dependencies] utils.workspace = true xmss.workspace = true rand.workspace = true +zk-alloc.workspace = true tracing.workspace = true include_dir.workspace = true diff --git a/crates/rec_aggregation/src/benchmark.rs b/crates/rec_aggregation/src/benchmark.rs index c80174d0a..c9f188fdd 100644 --- a/crates/rec_aggregation/src/benchmark.rs +++ b/crates/rec_aggregation/src/benchmark.rs @@ -397,6 +397,9 @@ fn build_aggregation( let mut last_result: Option = None; let own_display_index = display_index + count_nodes(topology) - 1; for _ in 0..repeat { + #[cfg(not(feature = "standard-alloc"))] + zk_alloc::begin_phase(); + let time = Instant::now(); let result = aggregate_single_msg_signatures( &children, @@ -408,6 +411,13 @@ fn build_aggregation( .unwrap(); let elapsed = time.elapsed(); + // Clone the outputs out of the arena before the next phase resets its slabs. + #[cfg(not(feature = "standard-alloc"))] + let result = { + zk_alloc::end_phase(); + result.clone() + }; + times.push(elapsed.as_secs_f64()); last_result = Some(result); diff --git a/src/lib.rs b/src/lib.rs index 48a4a0be0..3d516e48d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,31 +11,20 @@ pub use xmss::{MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, xmss pub type F = KoalaBear; -/// Tune the allocator and VM memory policy for the prover's churn of huge buffers. +/// Tune the VM memory policy for the prover's churn of huge buffers. /// -/// Two things, both before any heavy proving allocation (idempotent): -/// -/// 1. **Disable mimalloc purging** so freed large blocks are *retained* rather than returned -/// to the OS and re-faulted on the next allocation — what made the old bump arena fast -/// (page reuse); mimalloc-with-retention matches and beats it. -/// -/// 2. **Disable Transparent Huge Pages for this process.** On Zen4 (and likely other x86 with -/// physically-indexed L2/L3), when the kernel promotes the allocator's large arenas to -/// 2 MB huge pages, the prover's strided multilinear/NTT array access collapses into a few -/// cache sets — measured **+217% cache-misses, IPC 0.85 → 0.51, +50% wall time** on -/// `fancy-aggregation`. It's intermittent (only fires when 2 MB-contiguous memory is free -/// for THP promotion), which is what made it so hard to pin down. `prctl(PR_SET_THP_DISABLE)` -/// is process-local and overrides even a system-wide `THP=always`. No-op off Linux (macOS -/// has no THP — Apple silicon was never affected). Applies under any allocator. +/// **Disable Transparent Huge Pages for this process.** On Zen4 (and likely other x86 with +/// physically-indexed L2/L3), when the kernel promotes the allocator's large arenas to +/// 2 MB huge pages, the prover's strided multilinear/NTT array access collapses into a few +/// cache sets — measured **+217% cache-misses, IPC 0.85 → 0.51, +50% wall time** on +/// `fancy-aggregation`. It's intermittent (only fires when 2 MB-contiguous memory is free +/// for THP promotion), which is what made it so hard to pin down. `prctl(PR_SET_THP_DISABLE)` +/// is process-local and overrides even a system-wide `THP=always`. No-op off Linux (macOS +/// has no THP — Apple silicon was never affected). Applies under any allocator. +// Not `const`: the body is non-empty (and non-const) on Linux; it only looks empty elsewhere. +#[allow(clippy::missing_const_for_fn)] pub fn tune_allocator() { - // mimalloc v3 option index `mi_option_purge_delay` = 15; value -1 = never purge - // (equivalent to `MIMALLOC_PURGE_DELAY=-1`). No-op under the `standard-alloc` (plain - // system allocator) build. - #[cfg(not(feature = "standard-alloc"))] - unsafe { - libmimalloc_sys::mi_option_set(15, -1); - } - // Keep allocator arenas on 4 KB pages (see point 2 above). + // Keep the arena's slabs on 4 KB pages. #[cfg(target_os = "linux")] unsafe { libc::prctl(libc::PR_SET_THP_DISABLE, 1, 0, 0, 0); @@ -54,3 +43,13 @@ pub fn setup_prover() { pub fn setup_verifier() { rec_aggregation::init_aggregation_bytecode(); } + +/// Bump-arena allocator. +/// +/// To enable, set it as the `#[global_allocator]` in your binary and call [`init_allocator`] +/// once at startup. Then bracket each proving call with [`begin_phase`] / [`end_phase`] and +/// **clone the outputs after [`end_phase`]** so the cloned copy lands in the system allocator +/// before the next [`begin_phase`] resets the arena slabs. +/// +/// See `tests/test_zk_alloc.rs` for a runnable end-to-end example. +pub use zk_alloc::{ZkAllocator, begin_phase, end_phase, init as init_allocator}; diff --git a/src/main.rs b/src/main.rs index 4328b882b..d15b6f073 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,14 @@ use clap::Parser; use rec_aggregation::benchmark::{AggregationTopology, biggest_leaf, run_aggregation_benchmark}; -// Allocator: mimalloc — a robust production allocator, tuned to retain freed memory (see -// `lean_multisig::tune_allocator`). Replaces the former `zk-alloc` bump arena, which was -// fast but fragile: any allocation outliving a phase, or a pointer retained across -// `begin_phase`'s slab reset (e.g. from a background thread, tracing, or a stray clone), -// silently corrupted memory. mimalloc-with-retention is both **faster** here and stable. +// Allocator: zk-alloc — a bump+reset arena. Allocation is a pointer bump and free is a no-op; +// `begin_phase`/`end_phase` (see `benchmark.rs`) reset every thread's slab between proofs so the +// prover's huge per-phase buffer churn reuses pages instead of re-faulting. Outputs are cloned +// out after `end_phase` so they detach to the system allocator before the next reset. // The `standard-alloc` feature selects the plain system allocator for comparison. #[cfg(not(feature = "standard-alloc"))] #[global_allocator] -static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; +static ALLOC: zk_alloc::ZkAllocator = zk_alloc::ZkAllocator; #[derive(Parser)] enum Cli { @@ -73,9 +72,11 @@ fn run_with_warmup(topology: &AggregationTopology, tracing: bool, json: bool, re #[allow(clippy::too_many_lines)] fn main() { - // Retain freed memory (no purging) so the prover's huge buffer churn reuses pages - // instead of re-faulting — the property that made the old arena fast. Before any work. + // Disable THP for the prover's strided arrays (see `tune_allocator`), before any work. lean_multisig::tune_allocator(); + // Validate the build-time thread count before the arena maps its slabs. + #[cfg(not(feature = "standard-alloc"))] + lean_multisig::init_allocator(); let cli = Cli::parse(); diff --git a/tests/test_aggregation.rs b/tests/test_aggregation.rs index 14e06d5fb..aa9fb78ff 100644 --- a/tests/test_aggregation.rs +++ b/tests/test_aggregation.rs @@ -1,9 +1,8 @@ use lean_multisig::{aggregate_single_msg_signatures, setup_prover, verify_single_message_aggregate}; use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; -// End-to-end prove+verify under the default (mimalloc) allocator. Repeated to catch any -// cross-run state corruption. (Replaces the former `test_zk_alloc` arena regression test; -// the bump arena and its phase ceremony were removed in favor of a robust allocator.) +// End-to-end prove+verify under the system allocator (no arena phases). Repeated to catch +// any cross-run state corruption. `test_zk_alloc.rs` is the matching arena-allocator run. #[test] fn test_aggregation_prove_verify() { setup_prover(); diff --git a/tests/test_zk_alloc.rs b/tests/test_zk_alloc.rs new file mode 100644 index 000000000..4596bde61 --- /dev/null +++ b/tests/test_zk_alloc.rs @@ -0,0 +1,27 @@ +use lean_multisig::{ + ZkAllocator, aggregate_single_msg_signatures, begin_phase, end_phase, setup_prover, verify_single_message_aggregate, +}; +use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; + +#[global_allocator] +static ALLOC: ZkAllocator = ZkAllocator; + +#[test] +#[allow(clippy::redundant_clone)] +fn test_aggregation_with_zk_alloc() { + setup_prover(); + + let log_inv_rate = 2; + let message = message_for_benchmark(); + let slot: u32 = BENCHMARK_SLOT; + let signatures = get_benchmark_signatures(); + let raw_xmss = signatures[0..6].to_vec(); + + begin_phase(); + let aggregated = aggregate_single_msg_signatures(&[], raw_xmss, message, slot, log_inv_rate).unwrap(); + end_phase(); + // IMPORTANT: clone to move the data out of the arena memory + let aggregated = aggregated.clone(); + + verify_single_message_aggregate(&aggregated).unwrap(); +} From 2467601504bc51fbb0c7b3a0853ca08e0e715f39 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 31 May 2026 03:10:03 +0200 Subject: [PATCH 39/65] fix: forbidden nested parallel dispatch in XMSS benchmark cache generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom thread pool panics on nested dispatch. The benchmark builds its signer cache by parallelizing over the 10k signers, and each signer's `xmss_key_gen` parallelizes over its Merkle leaves/nodes — a nested dispatch that aborts the run whenever the on-disk cache is absent (fresh checkout / CI). Misattributed to the zk-alloc arena; the arena is fine (the multi-phase verifying test passes). Fix is confined to the xmss crate (allocator and thread-pool crates untouched): - `xmss_key_gen` gains a `sequential` flag; a small `fill` helper runs the inner leaf/node loops inline when set, dispatching to the pool otherwise. - `compute_signer` (the per-signer cache builder body) passes `sequential = true`, so the outer 10k-signer loop stays parallel while the inner key-gen does not nest. - standalone callers pass `false`. Also: `main` now calls `setup_prover()` before the first `begin_phase()` so the pool, bytecode, and twiddles are allocated in the system allocator and survive the per-proof slab resets; `test_zk_alloc` runs several armed phases and verifies each. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/xmss/src/signers_cache.rs | 4 +++- crates/xmss/src/xmss.rs | 19 +++++++++++++++++-- crates/xmss/tests/xmss_tests.rs | 4 ++-- src/main.rs | 6 ++++++ tests/test_multisignatures.rs | 4 ++-- tests/test_zk_alloc.rs | 24 ++++++++++++++++-------- 6 files changed, 46 insertions(+), 15 deletions(-) diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 27c73dd15..9afd176bc 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -65,7 +65,9 @@ fn compute_signer(index: usize) -> (XmssPublicKey, XmssSignature) { let mut rng = StdRng::seed_from_u64(index as u64); let key_start = BENCHMARK_SLOT; let key_end = BENCHMARK_SLOT + 1; - let (sk, pk) = xmss_key_gen(rng.random(), key_start, key_end).unwrap(); + // `sequential = true`: this runs inside the parallel per-signer cache builder, so the + // inner key-gen loops must not dispatch again (nested pool dispatch is forbidden). + let (sk, pk) = xmss_key_gen(rng.random(), key_start, key_end, true).unwrap(); let sig = xmss_sign(&mut rng, &sk, &message_for_benchmark(), BENCHMARK_SLOT).unwrap(); (pk, sig) } diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index e56771cc8..5eaf14280 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -74,10 +74,25 @@ pub enum XmssKeyGenError { InvalidRange, } +/// Fill `data` with `f`, in parallel — unless `sequential`, in which case run it inline. +/// The sequential path exists for callers already running inside a pool task (e.g. the +/// benchmark cache builder, which parallelizes over signers): the custom pool forbids nested +/// dispatch, and the outer level has already saturated the cores. +fn fill(sequential: bool, data: &mut [T], f: impl Fn(usize, &mut T) + Sync) { + if sequential { + data.iter_mut().enumerate().for_each(|(i, out)| f(i, out)); + } else { + parallel::par_for_each_mut(data, f); + } +} + +/// `sequential` runs the internal leaf/node loops inline instead of dispatching to the pool; +/// pass `true` when calling from within an outer parallel loop (see [`fill`]), `false` otherwise. pub fn xmss_key_gen( seed: [u8; 32], slot_start: u32, slot_end: u32, + sequential: bool, ) -> Result<(XmssSecretKey, XmssPublicKey), XmssKeyGenError> { if slot_start > slot_end || slot_end as u64 >= (1 << LOG_LIFETIME) { return Err(XmssKeyGenError::InvalidRange); @@ -86,7 +101,7 @@ pub fn xmss_key_gen( // Level 0: WOTS leaf hashes for slots in [slot_start, slot_end] let n_leaves = (slot_end - slot_start + 1) as usize; let mut leaves: Vec = unsafe { uninitialized_vec(n_leaves) }; - parallel::par_for_each_mut(&mut leaves, |i, out| { + fill(sequential, &mut leaves, |i, out| { let slot = slot_start + i as u32; let wots = gen_wots_secret_key(&seed, slot, public_param); *out = wots.public_key().hash(public_param, slot); @@ -104,7 +119,7 @@ pub fn xmss_key_gen( let prev = &merkle_tree[level - 1]; let n_nodes = (top - base + 1) as usize; let mut nodes: Vec = unsafe { uninitialized_vec(n_nodes) }; - parallel::par_for_each_mut(&mut nodes, |k, out| { + fill(sequential, &mut nodes, |k, out| { let i = base + k as u64; let left_idx = 2 * i; let right_idx = 2 * i + 1; diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 7abf02312..6d2721df0 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -12,7 +12,7 @@ fn test_xmss_serialize_deserialize() { let slot_end = 115; let slot = 110; - let (sk, pk) = xmss_key_gen(keygen_seed, slot_start, slot_end).unwrap(); + let (sk, pk) = xmss_key_gen(keygen_seed, slot_start, slot_end, false).unwrap(); let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, &message, slot).unwrap(); let pk_bytes = postcard::to_allocvec(&pk).unwrap(); @@ -32,7 +32,7 @@ fn keygen_sign_verify() { let message: [F; MESSAGE_LEN_FE] = std::array::from_fn(|i| F::from_usize(i * 3 + 7)); for slot in [0, 1234, u32::MAX] { - let (sk, pk) = xmss_key_gen(keygen_seed, slot.saturating_sub(1), slot.saturating_add(2)).unwrap(); + let (sk, pk) = xmss_key_gen(keygen_seed, slot.saturating_sub(1), slot.saturating_add(2), false).unwrap(); let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, &message, slot).unwrap(); xmss_verify(&pk, &message, &sig, slot).unwrap(); } diff --git a/src/main.rs b/src/main.rs index d15b6f073..485e00986 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,12 @@ enum Cli { } fn run_with_warmup(topology: &AggregationTopology, tracing: bool, json: bool, repeat: usize) { + // Build all persistent state — thread pool, compiled bytecode, DFT twiddles — before the + // first `begin_phase()`, so it lands in the system allocator and survives the per-proof + // slab resets. Without this it would initialize lazily inside the phased warm-up, land in + // arena memory, and be corrupted by the next phase's reset. (The arena requires all + // cross-phase state to predate the first phase; the tests already do this.) + lean_multisig::setup_prover(); let warmup = biggest_leaf(topology).unwrap(); eprint!("warming up... "); let _ = run_aggregation_benchmark(&warmup, false, true, 1); diff --git a/tests/test_multisignatures.rs b/tests/test_multisignatures.rs index c5ba89d4c..a7cd12ef9 100644 --- a/tests/test_multisignatures.rs +++ b/tests/test_multisignatures.rs @@ -23,7 +23,7 @@ fn test_xmss_signature() { let mut rng: StdRng = StdRng::seed_from_u64(0); let msg = rng.random(); - let (secret_key, pub_key) = xmss_key_gen(rng.random(), start_slot, end_slot).unwrap(); + let (secret_key, pub_key) = xmss_key_gen(rng.random(), start_slot, end_slot, false).unwrap(); let signature = xmss_sign(&mut rng, &secret_key, &msg, slot).unwrap(); xmss_verify(&pub_key, &msg, &signature, slot).unwrap(); } @@ -91,7 +91,7 @@ fn test_multi_message_aggregation() { let raws_b: Vec<_> = (0..2) .map(|_| { - let (sk, pk) = xmss_key_gen(rng_b.random(), slot_b, slot_b).unwrap(); + let (sk, pk) = xmss_key_gen(rng_b.random(), slot_b, slot_b, false).unwrap(); let sig = xmss_sign(&mut rng_b, &sk, &message_b, slot_b).unwrap(); (pk, sig) }) diff --git a/tests/test_zk_alloc.rs b/tests/test_zk_alloc.rs index 4596bde61..cd64c62b9 100644 --- a/tests/test_zk_alloc.rs +++ b/tests/test_zk_alloc.rs @@ -6,22 +6,30 @@ use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_ #[global_allocator] static ALLOC: ZkAllocator = ZkAllocator; +// Exercise the full multi-phase arena lifecycle and verify every proof. Each `begin_phase` +// resets the slabs, overwriting the previous phase's arena memory — so if any persistent +// state (thread pool, compiled bytecode, twiddles, static caches) had leaked into the arena +// instead of the system allocator, a later phase's proof would be built on corrupted data and +// fail to verify. `setup_prover()` (called before the first phase) settles that state in the +// system allocator up front; this test guards that contract across several phases. #[test] #[allow(clippy::redundant_clone)] fn test_aggregation_with_zk_alloc() { setup_prover(); let log_inv_rate = 2; - let message = message_for_benchmark(); let slot: u32 = BENCHMARK_SLOT; let signatures = get_benchmark_signatures(); let raw_xmss = signatures[0..6].to_vec(); - begin_phase(); - let aggregated = aggregate_single_msg_signatures(&[], raw_xmss, message, slot, log_inv_rate).unwrap(); - end_phase(); - // IMPORTANT: clone to move the data out of the arena memory - let aggregated = aggregated.clone(); - - verify_single_message_aggregate(&aggregated).unwrap(); + for _ in 0..3 { + begin_phase(); + let aggregated = + aggregate_single_msg_signatures(&[], raw_xmss.clone(), message_for_benchmark(), slot, log_inv_rate) + .unwrap(); + end_phase(); + // IMPORTANT: clone to move the data out of the arena memory before the next reset. + let aggregated = aggregated.clone(); + verify_single_message_aggregate(&aggregated).unwrap(); + } } From a152f02c5c755b2e9e602b492aac8bdc84c9e249 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 31 May 2026 03:10:03 +0200 Subject: [PATCH 40/65] fix: forbidden nested parallel dispatch in XMSS benchmark cache generation --- crates/xmss/src/signers_cache.rs | 2 +- crates/xmss/src/xmss.rs | 13 +++++++++++-- crates/xmss/tests/xmss_tests.rs | 4 ++-- tests/test_multisignatures.rs | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 27c73dd15..80c978c84 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -65,7 +65,7 @@ fn compute_signer(index: usize) -> (XmssPublicKey, XmssSignature) { let mut rng = StdRng::seed_from_u64(index as u64); let key_start = BENCHMARK_SLOT; let key_end = BENCHMARK_SLOT + 1; - let (sk, pk) = xmss_key_gen(rng.random(), key_start, key_end).unwrap(); + let (sk, pk) = xmss_key_gen(rng.random(), key_start, key_end, true).unwrap(); let sig = xmss_sign(&mut rng, &sk, &message_for_benchmark(), BENCHMARK_SLOT).unwrap(); (pk, sig) } diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index e56771cc8..9f77837af 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -74,10 +74,19 @@ pub enum XmssKeyGenError { InvalidRange, } +fn fill(sequential: bool, data: &mut [T], f: impl Fn(usize, &mut T) + Sync) { + if sequential { + data.iter_mut().enumerate().for_each(|(i, out)| f(i, out)); + } else { + parallel::par_for_each_mut(data, f); + } +} + pub fn xmss_key_gen( seed: [u8; 32], slot_start: u32, slot_end: u32, + sequential: bool, ) -> Result<(XmssSecretKey, XmssPublicKey), XmssKeyGenError> { if slot_start > slot_end || slot_end as u64 >= (1 << LOG_LIFETIME) { return Err(XmssKeyGenError::InvalidRange); @@ -86,7 +95,7 @@ pub fn xmss_key_gen( // Level 0: WOTS leaf hashes for slots in [slot_start, slot_end] let n_leaves = (slot_end - slot_start + 1) as usize; let mut leaves: Vec = unsafe { uninitialized_vec(n_leaves) }; - parallel::par_for_each_mut(&mut leaves, |i, out| { + fill(sequential, &mut leaves, |i, out| { let slot = slot_start + i as u32; let wots = gen_wots_secret_key(&seed, slot, public_param); *out = wots.public_key().hash(public_param, slot); @@ -104,7 +113,7 @@ pub fn xmss_key_gen( let prev = &merkle_tree[level - 1]; let n_nodes = (top - base + 1) as usize; let mut nodes: Vec = unsafe { uninitialized_vec(n_nodes) }; - parallel::par_for_each_mut(&mut nodes, |k, out| { + fill(sequential, &mut nodes, |k, out| { let i = base + k as u64; let left_idx = 2 * i; let right_idx = 2 * i + 1; diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 7abf02312..6d2721df0 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -12,7 +12,7 @@ fn test_xmss_serialize_deserialize() { let slot_end = 115; let slot = 110; - let (sk, pk) = xmss_key_gen(keygen_seed, slot_start, slot_end).unwrap(); + let (sk, pk) = xmss_key_gen(keygen_seed, slot_start, slot_end, false).unwrap(); let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, &message, slot).unwrap(); let pk_bytes = postcard::to_allocvec(&pk).unwrap(); @@ -32,7 +32,7 @@ fn keygen_sign_verify() { let message: [F; MESSAGE_LEN_FE] = std::array::from_fn(|i| F::from_usize(i * 3 + 7)); for slot in [0, 1234, u32::MAX] { - let (sk, pk) = xmss_key_gen(keygen_seed, slot.saturating_sub(1), slot.saturating_add(2)).unwrap(); + let (sk, pk) = xmss_key_gen(keygen_seed, slot.saturating_sub(1), slot.saturating_add(2), false).unwrap(); let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, &message, slot).unwrap(); xmss_verify(&pk, &message, &sig, slot).unwrap(); } diff --git a/tests/test_multisignatures.rs b/tests/test_multisignatures.rs index c5ba89d4c..a7cd12ef9 100644 --- a/tests/test_multisignatures.rs +++ b/tests/test_multisignatures.rs @@ -23,7 +23,7 @@ fn test_xmss_signature() { let mut rng: StdRng = StdRng::seed_from_u64(0); let msg = rng.random(); - let (secret_key, pub_key) = xmss_key_gen(rng.random(), start_slot, end_slot).unwrap(); + let (secret_key, pub_key) = xmss_key_gen(rng.random(), start_slot, end_slot, false).unwrap(); let signature = xmss_sign(&mut rng, &secret_key, &msg, slot).unwrap(); xmss_verify(&pub_key, &msg, &signature, slot).unwrap(); } @@ -91,7 +91,7 @@ fn test_multi_message_aggregation() { let raws_b: Vec<_> = (0..2) .map(|_| { - let (sk, pk) = xmss_key_gen(rng_b.random(), slot_b, slot_b).unwrap(); + let (sk, pk) = xmss_key_gen(rng_b.random(), slot_b, slot_b, false).unwrap(); let sig = xmss_sign(&mut rng_b, &sk, &message_b, slot_b).unwrap(); (pk, sig) }) From c2b77b01aee86513ef4b844bc6debd9d70b128b4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 2 Jun 2026 17:39:01 +0400 Subject: [PATCH 41/65] replace mimalloc by smalloc --- Cargo.lock | 93 +++++++++++++++++++++------------------ Cargo.toml | 5 +-- src/lib.rs | 32 -------------- src/main.rs | 12 +---- tests/test_aggregation.rs | 2 +- 5 files changed, 54 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bb8c2bce..79d40edcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,16 +138,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "cc" -version = "1.2.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" -dependencies = [ - "find-msvc-tools", - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -269,12 +259,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "cty" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" - [[package]] name = "digest" version = "0.10.7" @@ -321,10 +305,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "foldhash" @@ -491,11 +485,10 @@ dependencies = [ "clap", "lean_vm", "libc", - "libmimalloc-sys", - "mimalloc", "rand", "rec_aggregation", "serde_json", + "smmalloc", "sub_protocols", "system-info", "utils", @@ -565,14 +558,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libmimalloc-sys" -version = "0.1.49" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" -dependencies = [ - "cc", - "cty", -] +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -598,6 +587,15 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "mach-sys" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48460c2e82a3a0de197152fdf8d2c2d5e43adc501501553e439bf2156e6f87c7" +dependencies = [ + "fastrand", +] + [[package]] name = "matchers" version = "0.2.0" @@ -613,15 +611,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "mimalloc" -version = "0.1.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" -dependencies = [ - "libmimalloc-sys", -] - [[package]] name = "mt-air" version = "0.1.0" @@ -981,6 +970,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1066,18 +1068,23 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smmalloc" +version = "7.6.10+c55d2f585a638d795a1d2927baa982712eed93ab" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027cb6587cf11dbf2da1d6d9b25b32b1cbf7648235b85de0e8221249ab6b379a" +dependencies = [ + "mach-sys", + "rustix", + "windows-sys", +] + [[package]] name = "spin" version = "0.9.8" diff --git a/Cargo.toml b/Cargo.toml index ce778c312..c1eefe00e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,14 +82,13 @@ include_dir = "0.7" [features] prox-gaps-conjecture = ["rec_aggregation/prox-gaps-conjecture"] -# Build with the plain system allocator instead of mimalloc (for comparison/debugging). +# Build with the plain system allocator instead of smalloc (for comparison/debugging). standard-alloc = [] [dependencies] clap.workspace = true rec_aggregation.workspace = true -mimalloc = "0.1" -libmimalloc-sys = { version = "0.1", features = ["extended"] } +smalloc = { package = "smmalloc", version = "7.6.10" } libc = "0.2" rand.workspace = true sub_protocols.workspace = true diff --git a/src/lib.rs b/src/lib.rs index 48a4a0be0..1e299b38d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,40 +11,8 @@ pub use xmss::{MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, xmss pub type F = KoalaBear; -/// Tune the allocator and VM memory policy for the prover's churn of huge buffers. -/// -/// Two things, both before any heavy proving allocation (idempotent): -/// -/// 1. **Disable mimalloc purging** so freed large blocks are *retained* rather than returned -/// to the OS and re-faulted on the next allocation — what made the old bump arena fast -/// (page reuse); mimalloc-with-retention matches and beats it. -/// -/// 2. **Disable Transparent Huge Pages for this process.** On Zen4 (and likely other x86 with -/// physically-indexed L2/L3), when the kernel promotes the allocator's large arenas to -/// 2 MB huge pages, the prover's strided multilinear/NTT array access collapses into a few -/// cache sets — measured **+217% cache-misses, IPC 0.85 → 0.51, +50% wall time** on -/// `fancy-aggregation`. It's intermittent (only fires when 2 MB-contiguous memory is free -/// for THP promotion), which is what made it so hard to pin down. `prctl(PR_SET_THP_DISABLE)` -/// is process-local and overrides even a system-wide `THP=always`. No-op off Linux (macOS -/// has no THP — Apple silicon was never affected). Applies under any allocator. -pub fn tune_allocator() { - // mimalloc v3 option index `mi_option_purge_delay` = 15; value -1 = never purge - // (equivalent to `MIMALLOC_PURGE_DELAY=-1`). No-op under the `standard-alloc` (plain - // system allocator) build. - #[cfg(not(feature = "standard-alloc"))] - unsafe { - libmimalloc_sys::mi_option_set(15, -1); - } - // Keep allocator arenas on 4 KB pages (see point 2 above). - #[cfg(target_os = "linux")] - unsafe { - libc::prctl(libc::PR_SET_THP_DISABLE, 1, 0, 0, 0); - } -} - /// Call once before proving. Compiles the aggregation program and precomputes DFT twiddles. pub fn setup_prover() { - tune_allocator(); parallel::init(); // construct the thread pool up front (was done by `zk_alloc::begin_phase`) rec_aggregation::init_aggregation_bytecode(); precompute_dft_twiddles::(1 << 24); diff --git a/src/main.rs b/src/main.rs index 4328b882b..fc22b54a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,9 @@ use clap::Parser; use rec_aggregation::benchmark::{AggregationTopology, biggest_leaf, run_aggregation_benchmark}; -// Allocator: mimalloc — a robust production allocator, tuned to retain freed memory (see -// `lean_multisig::tune_allocator`). Replaces the former `zk-alloc` bump arena, which was -// fast but fragile: any allocation outliving a phase, or a pointer retained across -// `begin_phase`'s slab reset (e.g. from a background thread, tracing, or a stray clone), -// silently corrupted memory. mimalloc-with-retention is both **faster** here and stable. -// The `standard-alloc` feature selects the plain system allocator for comparison. #[cfg(not(feature = "standard-alloc"))] #[global_allocator] -static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; +static ALLOC: smalloc::Smalloc = smalloc::Smalloc::new(); #[derive(Parser)] enum Cli { @@ -73,10 +67,6 @@ fn run_with_warmup(topology: &AggregationTopology, tracing: bool, json: bool, re #[allow(clippy::too_many_lines)] fn main() { - // Retain freed memory (no purging) so the prover's huge buffer churn reuses pages - // instead of re-faulting — the property that made the old arena fast. Before any work. - lean_multisig::tune_allocator(); - let cli = Cli::parse(); match cli { diff --git a/tests/test_aggregation.rs b/tests/test_aggregation.rs index 14e06d5fb..c9b6398b3 100644 --- a/tests/test_aggregation.rs +++ b/tests/test_aggregation.rs @@ -1,7 +1,7 @@ use lean_multisig::{aggregate_single_msg_signatures, setup_prover, verify_single_message_aggregate}; use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; -// End-to-end prove+verify under the default (mimalloc) allocator. Repeated to catch any +// End-to-end prove+verify under the default (smalloc) allocator. Repeated to catch any // cross-run state corruption. (Replaces the former `test_zk_alloc` arena regression test; // the bump arena and its phase ceremony were removed in favor of a robust allocator.) #[test] From 87f4205a781ed4691f16a32d6473772c391cea71 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 2 Jun 2026 22:30:18 +0200 Subject: [PATCH 42/65] cleaning + handle panics gracefully in parallel crate --- crates/backend/parallel/src/lib.rs | 361 ++++++++++++++--------------- crates/backend/poly/src/eq_mle.rs | 1 + crates/whir/src/dft.rs | 1 + src/lib.rs | 2 +- 4 files changed, 171 insertions(+), 194 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index f2af04e5e..e3adeefce 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -1,32 +1,24 @@ -//! Minimal fixed-size thread pool for flat data-parallel kernels: "split a range into -//! pieces, run a closure on each." No nested work-stealing, no per-dispatch allocation — -//! owning the runtime lets us attach per-worker scratch buffers and drop rayon. +//! Minimal fixed-size thread pool for flat data-parallel kernels ("split a range, run a +//! closure on each piece"). No work-stealing, no per-dispatch allocation; owning the runtime +//! lets us pin per-worker scratch and drop rayon. //! -//! ## Model -//! -//! `NUM_THREADS - 1` background workers (ids `1..NUM_THREADS`); the dispatching thread is -//! worker `0` and runs its share inline (no oversubscription). Tasks are claimed from a -//! shared atomic counter for dynamic load balancing. -//! -//! ## Lock-free dispatch -//! -//! A mutex+condvar wake-up costs ~2x per dispatch. Instead, dispatch bumps a `generation` -//! counter that idle workers **spin** on (back-to-back dispatches pay no syscall), parking -//! only after `SPIN_LIMIT` unrewarded spins. Completion is a lock-free `working` countdown -//! the dispatcher spins on. The per-worker `parked` flag is ordered SeqCst against -//! `generation` so no wake-up is lost and the unpark syscall is skipped while workers spin. -//! -//! ## Nesting is forbidden -//! -//! A flat pool can't dispatch from inside a task (it would deadlock on the dispatch lock); -//! an `IN_TASK` guard catches that and panics instead. The outer level has already saturated -//! all cores, so nested parallelism would buy nothing anyway. -//! -//! ## Constraint -//! -//! One dispatcher at a time: concurrent (non-nested) dispatches are serialized by a mutex. - +//! - **Model.** `NUM_THREADS-1` background workers (ids `1..NUM_THREADS`); the dispatcher is +//! worker 0 and runs its share inline. Workers claim task ranges from a shared atomic +//! counter (guided self-scheduling) for dynamic load balance. +//! - **Lock-free dispatch.** Dispatch bumps a `generation` counter idle workers spin on +//! (back-to-back dispatches pay no syscall), parking only after `SPIN_LIMIT` idle spins. +//! Completion is a `working` countdown the dispatcher spins on. The per-worker `parked` flag +//! is SeqCst-ordered against `generation`, so for every dispatch at least one side sees the +//! other — a wakeup can't be lost — and unpark is skipped while a worker spins. +//! - **No nesting.** Dispatching from inside a task would deadlock the dispatch lock; an +//! `IN_TASK` guard panics instead (the outer level already saturates every core). +//! - **Panics.** A task panic is caught on its worker and re-raised on the dispatcher once the +//! dispatch quiesces (rayon's propagate-to-caller behavior); the pool stays usable. +//! - **One dispatcher at a time**, serialized by the `dispatch` mutex. + +use std::any::Any; use std::cell::{Cell, UnsafeCell}; +use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; use std::ptr::NonNull; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Mutex, Once, OnceLock}; @@ -34,21 +26,21 @@ use std::thread::Thread; use system_info::NUM_THREADS; -/// Idle spins before a worker parks. Tuned so back-to-back dispatches stay hot but -/// sequential gaps let workers sleep, freeing cores for the active thread. +/// Idle spins before a worker parks: long enough that back-to-back dispatches stay hot, short +/// enough that sequential gaps free the core for the active thread. const SPIN_LIMIT: u32 = 1 << 12; -/// Cap on a single guided-self-scheduling claim (see [`drain`]). Bounds worst-case load -/// imbalance while keeping million-task kernels to a few thousand claims. +/// Max tasks claimed in one guided-self-scheduling step: bounds load imbalance while keeping +/// million-task kernels to a few thousand claims. const MAX_CLAIM_BATCH: usize = 1 << 12; -/// Total worker count (including the dispatching thread). Equal to build-time `NUM_THREADS`. +/// Worker count including the dispatcher (= build-time `NUM_THREADS`). #[must_use] pub const fn num_threads() -> usize { NUM_THREADS } -/// Chunk size for a flat fan-out: a few chunks per worker so the atomic counter can +/// Chunk size for a flat fan-out: a few chunks per worker — fine enough for the counter to /// rebalance heterogeneous cores, coarse enough to amortize dispatch. #[must_use] #[inline] @@ -57,68 +49,65 @@ pub fn recommended_chunk_size(n_items: usize) -> usize { } thread_local! { - /// Stable id of this thread within the pool. Set once per background worker; - /// stays `0` on the dispatching thread (worker 0) and on any non-worker thread. + /// Stable pool id of this thread; `0` on the dispatcher and off-pool threads. static WORKER_ID: Cell = const { Cell::new(0) }; - /// True while this thread is executing a pool task. A dispatch issued in that state is - /// nested parallelism, which is forbidden and panics (see module docs). + /// Set while running a task; a dispatch in this state is forbidden nesting (panics). static IN_TASK: Cell = const { Cell::new(false) }; } -/// Stable id of the calling worker, in `0..NUM_THREADS` (`0` off-pool). The hook for -/// per-worker scratch buffers. +/// Calling worker's id in `0..NUM_THREADS` (`0` off-pool). The hook for per-worker scratch. #[must_use] pub fn current_worker_id() -> usize { WORKER_ID.with(Cell::get) } -/// Type-erased unit of work: a `&(dyn Fn(usize, usize) + Sync)` whose lifetime is erased -/// to `'static`. Only dereferenced inside a dispatch window during which the dispatcher -/// blocks, so the source borrow outlives every call. +/// Type-erased work unit. The `&dyn Fn` lifetime is erased to `'static`; it is dereferenced +/// only inside a dispatch window during which the dispatcher blocks, so the borrow outlives +/// every call. Range-based (`f(start, end)`) so a reduction looks up its per-worker +/// accumulator once per claimed batch, not per element. struct Job { - /// Range-based work: `f(start, end)` processes the half-open task range `start..end`. - /// Building the primitive on ranges (rather than single indices) lets reductions look - /// up their per-worker accumulator once per claimed batch instead of once per element. f: NonNull, n_tasks: usize, } -/// Per-worker state touched by the park/unpark protocol (indexed by worker id; slot 0, -/// the dispatcher, is unused — it never parks). +/// Park/unpark state, indexed by worker id (slot 0, the dispatcher, never parks). #[derive(Debug)] struct Worker { - /// "Currently parked" flag, ordered SeqCst against `Pool::generation`. + /// "Currently parked", SeqCst-ordered against `Pool::generation`. parked: AtomicBool, - /// Thread handle for `unpark`, published once by the worker on start-up. + /// Handle for `unpark`, published once at worker start-up. handle: OnceLock, } struct Pool { - /// Current job. Written by the dispatcher before bumping `generation`, read by - /// workers after they observe the bump; `generation` supplies the happens-before. + /// Current job: written by the dispatcher before the `generation` bump, read by workers + /// after observing it (the bump supplies the happens-before). job: UnsafeCell>, - /// Bumped once per dispatch. Idle workers watch it (spin, then park). + /// Bumped once per dispatch; idle workers watch it (spin, then park). generation: AtomicUsize, - /// Next task index to claim. Reset to 0 before each dispatch. + /// Next task index to claim; reset to 0 per dispatch. counter: AtomicUsize, - /// Background workers still draining the current dispatch; dispatcher spins to 0. + /// Background workers still draining; the dispatcher spins this to 0. working: AtomicUsize, - /// Per-worker park flag + unpark handle (indexed by worker id; slot 0 unused). + /// Park flag + unpark handle per worker (slot 0 unused). workers: Vec, - /// Serializes dispatchers: only one thread may drive the pool at a time. + /// Serializes dispatchers: one driver at a time. dispatch: Mutex<()>, + /// First task-panic payload of the current dispatch, re-raised by the dispatcher. Caught + /// here so it can't unwind across `worker_main` (which would skip the `working` decrement + /// and deadlock the completion spin). + panic: Mutex>>, } -// SAFETY: `job` is mutated only by the unique dispatcher while workers are parked or -// before they observe the generation bump, and read only after; the `generation` -// release/acquire (and SeqCst park protocol) order these phases. The erased `Job` -// pointer is never used outside a dispatch window during which its borrow is live. +// SAFETY: `job` is written only by the sole dispatcher (while workers are parked or before +// they observe the generation bump) and read only after; the generation release/acquire and +// SeqCst park protocol order the phases. The erased `Job` pointer is used only within a +// dispatch window where its borrow is live. unsafe impl Sync for Pool {} unsafe impl Send for Pool {} -/// Idempotent warm-up: spawn workers and run one dispatch so the pool, parkers, and the -/// mutex's lazily-allocated `pthread_mutex_t` (macOS) exist before timed proving work. -/// Without it the pool initializes lazily on the first parallel call. +/// Idempotent warm-up: spawn workers and run one empty dispatch so the pool and the (macOS) +/// lazily-allocated mutex exist before timed work. Otherwise the pool inits on first use. pub fn init() { static INIT: Once = Once::new(); INIT.call_once(|| { @@ -145,6 +134,7 @@ fn pool() -> &'static Pool { }) .collect(), dispatch: Mutex::new(()), + panic: Mutex::new(None), })); for id in 1..n { std::thread::Builder::new() @@ -159,9 +149,7 @@ fn pool() -> &'static Pool { fn worker_main(pool: &'static Pool, id: usize) { WORKER_ID.with(|c| c.set(id)); let _ = pool.workers[id].handle.set(std::thread::current()); - - // The pool is leaked and lives for the whole process: workers never shut down, they - // die with the process. Each iteration services exactly one dispatch. + // Leaked, lives for the whole process; workers never shut down. One iteration per dispatch. let mut last_gen = 0usize; loop { last_gen = wait_for_dispatch(pool, id, last_gen); @@ -170,13 +158,10 @@ fn worker_main(pool: &'static Pool, id: usize) { } } -/// Block until the dispatcher publishes a new job, returning its generation. Spins up to -/// [`SPIN_LIMIT`] times (so back-to-back dispatches pay no syscall), then parks. -/// -/// The park is the delicate part: we publish `parked = true` and only then re-check -/// `generation`, both under `SeqCst`. That single total order is also observed by the -/// dispatcher's `generation` bump and subsequent `parked` load, so for every dispatch at -/// least one side sees the other — a wake-up can never be lost. +/// Block until a new job is published, returning its generation. Spins up to [`SPIN_LIMIT`], +/// then parks. The park is delicate: publish `parked = true`, then re-check `generation`, both +/// SeqCst — the same total order the dispatcher's bump and `parked` load observe, so a wakeup +/// can never be lost. fn wait_for_dispatch(pool: &Pool, id: usize, last_gen: usize) -> usize { let mut spins = 0u32; loop { @@ -189,8 +174,7 @@ fn wait_for_dispatch(pool: &Pool, id: usize, last_gen: usize) -> usize { std::hint::spin_loop(); continue; } - // Spun out. Announce the intent to park, then re-check under SeqCst: park only if - // nothing changed in the meantime; otherwise loop back and re-observe at the top. + // Announce intent to park, then re-check: park only if nothing changed, else re-loop. pool.workers[id].parked.store(true, Ordering::SeqCst); if pool.generation.load(Ordering::SeqCst) == last_gen { std::thread::park(); @@ -200,53 +184,48 @@ fn wait_for_dispatch(pool: &Pool, id: usize, last_gen: usize) -> usize { } } -/// Claim and run task indices until the counter is exhausted, using **guided -/// self-scheduling**: each claim grabs `remaining / (NUM_THREADS * 2)`, clamped to -/// `1..=`[`MAX_CLAIM_BATCH`] (the floor of 1 guarantees progress on tiny tails). Large -/// early batches keep counter contention low; the proportional shrink keeps the tail -/// finely divided for load balance. +/// Claim and run task ranges until the counter is exhausted (guided self-scheduling: each claim +/// takes `remaining / (NUM_THREADS*2)`, clamped to `1..=`[`MAX_CLAIM_BATCH`]). Big early claims +/// cut counter contention; the proportional shrink keeps the tail balanced. fn drain(pool: &Pool) { - // SAFETY: the dispatcher published `Some(job)` before the generation bump this - // worker just observed, and overwrites it only on the next dispatch (gated on - // `working == 0`); nobody writes during drain. + // SAFETY: the dispatcher published `Some(job)` before the bump this worker observed and + // overwrites it only on the next dispatch (gated on `working == 0`); no writer during drain. let job = unsafe { (*pool.job.get()).as_ref().expect("drain without a published job") }; - // SAFETY: `job.f` points at a `&dyn Fn` borrow held live by the blocked dispatcher. + // SAFETY: `job.f` borrows a `&dyn Fn` the blocked dispatcher keeps live. let f = unsafe { job.f.as_ref() }; let n = job.n_tasks; - // Mark this thread as in-task so a nested dispatch panics (see `for_each_chunk`) - // rather than deadlocking on the dispatch lock. - let prev = IN_TASK.replace(true); - loop { - // Stale counter read only affects batch granularity, not correctness: `fetch_add` - // tiles `0..n` into non-overlapping claims, and out-of-range tails are clamped. - let observed = pool.counter.load(Ordering::Relaxed); - if observed >= n { - break; + let prev = IN_TASK.replace(true); // catch nested dispatch (see `for_each_chunk`) + // Catch a task panic so it can't unwind across `worker_main` (skipping the `working` + // decrement → deadlock) or poison the dispatch lock; `for_each_chunk` re-raises it. + let result = catch_unwind(AssertUnwindSafe(|| { + loop { + // Stale read only affects granularity: `fetch_add` tiles `0..n` into disjoint claims. + let observed = pool.counter.load(Ordering::Relaxed); + if observed >= n { + break; + } + let batch = ((n - observed) / (NUM_THREADS * 2)).clamp(1, MAX_CLAIM_BATCH); + let start = pool.counter.fetch_add(batch, Ordering::Relaxed); + if start >= n { + break; + } + f(start, (start + batch).min(n)); } - let batch = ((n - observed) / (NUM_THREADS * 2)).clamp(1, MAX_CLAIM_BATCH); - let start = pool.counter.fetch_add(batch, Ordering::Relaxed); - if start >= n { - break; - } - let end = (start + batch).min(n); - f(start, end); - } + })); IN_TASK.set(prev); + if let Err(payload) = result { + pool.panic.lock().unwrap().get_or_insert(payload); // keep the first + } } -/// Core dispatch: run `f(start, end)` over disjoint contiguous sub-ranges that together -/// tile `0..n_tasks`, in parallel across the pool. The ranges are produced by guided -/// self-scheduling (see [`drain`]); a worker may receive several. Blocks until all -/// complete; the dispatching thread participates as worker 0. -/// -/// This is the primitive everything else is built on. Range-based (rather than per-index) -/// so a reduction can look up its per-worker accumulator once per claimed batch. +/// Run `f(start, end)` over disjoint ranges tiling `0..n_tasks`, in parallel; a worker may get +/// several (guided self-scheduling, see [`drain`]). Blocks until done, the dispatcher acting as +/// worker 0. The base primitive — range-based so reductions amortize per-worker lookups. pub fn for_each_chunk(n_tasks: usize, f: F) { - // Nesting is forbidden (would deadlock the dispatch lock): panic rather than silently - // running sequentially, so an accidental nested dispatch is caught instead of going slow. + // Nesting would deadlock the dispatch lock — panic so it's caught, not silently serial. assert!(!IN_TASK.get(), "nested parallel dispatch from within a pool task"); - // Trivial sizes and single-core builds run sequentially inline. + // Trivial sizes / single-core builds run inline. if NUM_THREADS <= 1 || n_tasks <= 1 { if n_tasks > 0 { f(0, n_tasks); @@ -256,28 +235,22 @@ pub fn for_each_chunk(n_tasks: usize, f: F) { let pool = pool(); let _guard = pool.dispatch.lock().unwrap(); - let n = NUM_THREADS; - - // SAFETY: erase the borrow's lifetime so the closure can live in the `'static` `Job`. - // The dispatcher blocks on `working` below before returning, so `f` outlives every - // worker dereference of this pointer. `transmute` is required here — NOT a `&dyn -> *const` - // cast: coercing to a bare `*const dyn` defaults the trait object to `'static`, which would - // force `F: 'static` (E0310) and break every non-'static caller. The transmute reinterprets - // the same (data, vtable) fat pointer without imposing that bound. + + // SAFETY: erase the borrow to `'static` so it fits the `Job`. The dispatcher blocks on + // `working` before returning, so `f` outlives every deref. `transmute` (not a `*const dyn` + // cast) is required: a bare cast would default the trait object to `'static` and force + // `F: 'static` (E0310); the transmute reinterprets the same fat pointer without that bound. let f_ref: &(dyn Fn(usize, usize) + Sync) = &f; let f_erased: NonNull = unsafe { std::mem::transmute(NonNull::from(f_ref)) }; - // SAFETY: all workers finished the previous dispatch (we waited for `working == 0`) - // and none observes this one until the generation bump, so we are the sole writer. + // SAFETY: sole writer — prior dispatch fully drained (`working == 0`), next not yet observed. unsafe { *pool.job.get() = Some(Job { f: f_erased, n_tasks }) }; pool.counter.store(0, Ordering::Relaxed); - pool.working.store(n - 1, Ordering::Release); - - // Publish the dispatch. SeqCst so the parked-flag protocol can't lose a wake-up. - pool.generation.fetch_add(1, Ordering::SeqCst); + pool.working.store(NUM_THREADS - 1, Ordering::Release); + pool.generation.fetch_add(1, Ordering::SeqCst); // publish; SeqCst guards the park protocol - // Wake only workers that actually parked; hot (spinning) ones see the bump for free. - for worker in &pool.workers[1..n] { + // Wake only parked workers; spinning ones see the bump for free. + for worker in &pool.workers[1..] { if worker.parked.load(Ordering::SeqCst) && let Some(t) = worker.handle.get() { @@ -286,29 +259,28 @@ pub fn for_each_chunk(n_tasks: usize, f: F) { } drain(pool); // dispatcher runs as worker 0 - - // Lock-free completion: wait for every background worker to finish draining. while pool.working.load(Ordering::Acquire) != 0 { - std::hint::spin_loop(); + std::hint::spin_loop(); // lock-free completion wait + } + + // Re-raise the first task panic (if any) after dropping `_guard`, so the lock releases + // cleanly (no poison) and the pool stays usable. + let panicked = pool.panic.lock().unwrap().take(); + drop(_guard); + if let Some(payload) = panicked { + resume_unwind(payload); } } -/// Run `f(i)` for every `i` in `0..n_tasks`, in parallel across the pool. Blocks until -/// all tasks complete; the dispatching thread participates as worker 0. -/// `#[inline]` so the trivial range→index adapter folds into the monomorphized -/// [`for_each_chunk`] (mirrors [`par_for_each_mut`]). +/// `f(i)` for every `i` in `0..n_tasks`, in parallel. `#[inline]` folds the range→index adapter +/// into the monomorphized [`for_each_chunk`]. #[inline] pub fn for_each_index(n_tasks: usize, f: F) { - for_each_chunk(n_tasks, |start, end| { - for i in start..end { - f(i); - } - }); + for_each_chunk(n_tasks, |start, end| (start..end).for_each(&f)); } -/// A raw base pointer shareable across pool workers. Sound only because callers partition -/// the allocation by task index, so each worker touches a disjoint region. Reuse this for -/// the "share a `*mut` across a dispatch" pattern instead of redefining it per crate. +/// A base `*mut` shareable across workers. Sound only because callers partition the allocation +/// by task index (disjoint regions). Reuse this instead of redefining the pattern per crate. #[derive(Debug)] pub struct SendPtr(pub *mut T); // SAFETY: accesses are partitioned by task index (see callers). @@ -316,51 +288,41 @@ unsafe impl Send for SendPtr {} unsafe impl Sync for SendPtr {} impl SendPtr { - /// Offset the base pointer by `n` elements. - /// + /// Offset the base by `n` elements. /// # Safety - /// `n` must keep the result within the original allocation, and any write through it - /// must target a slot no other concurrent task touches. + /// `n` stays in the allocation; any write targets a slot no concurrent task touches. #[inline] pub unsafe fn add(&self, n: usize) -> *mut T { unsafe { self.0.add(n) } } - /// Reconstruct the `len`-long slice starting at element offset `off`. - /// + /// Reconstruct the `len`-element slice at element offset `off`. /// # Safety - /// `off`/`len` must stay in-bounds and the slice must be disjoint from every other - /// slice any concurrent task reconstructs. + /// `off`/`len` in-bounds and disjoint from every other concurrent task's slice. #[inline] pub unsafe fn slice<'a>(&self, off: usize, len: usize) -> &'a mut [T] { unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) } } } -/// Parallel equivalent of `data.chunks_mut(chunk).enumerate().for_each(...)`, running -/// `f(chunk_index, chunk)` on each in parallel. The final chunk may be shorter. +/// Parallel `data.chunks_mut(chunk).enumerate().for_each(f)`; the final chunk may be shorter. pub fn par_chunks_mut(data: &mut [T], chunk: usize, f: F) where F: Fn(usize, &mut [T]) + Sync, { assert!(chunk > 0, "chunk size must be non-zero"); let len = data.len(); - let n_chunks = len.div_ceil(chunk); let base = SendPtr(data.as_mut_ptr()); - - for_each_index(n_chunks, |i| { + for_each_index(len.div_ceil(chunk), |i| { let start = i * chunk; - let this_len = chunk.min(len - start); - // SAFETY: distinct `i` produce non-overlapping in-bounds ranges, and `data` - // stays borrowed for the whole call. - let slice = unsafe { std::slice::from_raw_parts_mut(base.add(start), this_len) }; + // SAFETY: distinct `i` give disjoint in-bounds ranges; `data` stays borrowed. + let slice = unsafe { base.slice(start, chunk.min(len - start)) }; f(i, slice); }); } -/// Parallel `data.iter_mut().enumerate().for_each(...)`, sized with -/// [`recommended_chunk_size`]. Hands the closure each element's **global** index. -/// `#[inline]` to fold into the per-region caller (recovers the hand-written codegen). +/// Parallel `data.iter_mut().enumerate().for_each(f)`, chunked by [`recommended_chunk_size`]. +/// Hands the closure each element's **global** index. `#[inline]` to recover hand-written codegen. #[inline] pub fn par_for_each_mut(data: &mut [T], f: F) where @@ -368,36 +330,31 @@ where { let chunk = recommended_chunk_size(data.len()); par_chunks_mut(data, chunk, |ci, sub| { - let base = ci * chunk; for (k, slot) in sub.iter_mut().enumerate() { - f(base + k, slot); + f(ci * chunk + k, slot); } }); } -/// Give each worker exclusive, persistent access to its own `Option` slot while it -/// drains `0..n_tasks`: `run(slot, start, end)` is called once per claimed batch, always -/// with the same slot for a given worker, so state accumulates across the batches it -/// claims. Returns the per-worker slots (one per worker that ran, rest `None`) for the -/// caller to combine. This is the single home of the cross-worker slot `unsafe`. +/// Give each worker its own persistent `Option` slot while it drains `0..n_tasks`: +/// `run(slot, start, end)` fires once per claimed batch with that worker's slot, so state +/// accumulates across its batches. Returns the slots (rest `None`) for the caller to combine. +/// The sole home of the cross-worker slot `unsafe`. fn drain_into_slots(n_tasks: usize, run: impl Fn(&mut Option, usize, usize) + Sync) -> Vec> { let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); let ptr = SendPtr(slots.as_mut_ptr()); for_each_chunk(n_tasks, |start, end| { - // SAFETY: `current_worker_id()` is unique per live worker and < NUM_THREADS, so - // workers touch disjoint slots; `slots` outlives the dispatch. + // SAFETY: `current_worker_id() < NUM_THREADS` is unique per live worker → disjoint + // slots; `slots` outlives the dispatch. let slot = unsafe { &mut *ptr.add(current_worker_id()) }; run(slot, start, end); }); slots } -/// Parallel map-reduce over `0..n_tasks`, equivalent to -/// `(0..n_tasks).into_par_iter().map(map).reduce(identity, reduce)`. -/// -/// Each worker folds the task indices it claims into one local accumulator (one per -/// worker, not per task); the per-worker partials are combined on the dispatcher. -/// `reduce` must be associative. +/// Parallel map-reduce over `0..n_tasks` = `(0..n).map(map).reduce(identity, reduce)`. Each +/// worker folds its claimed indices into one local accumulator; the partials combine on the +/// dispatcher. `reduce` must be associative. pub fn map_reduce(n_tasks: usize, identity: ID, map: M, reduce: R) -> T where T: Send, @@ -409,24 +366,17 @@ where return (0..n_tasks).fold(identity(), |acc, i| reduce(acc, map(i))); } let slots = drain_into_slots(n_tasks, |slot, start, end| { - // Fold the batch into the worker's accumulator, taking/replacing the shared slot - // just once so per-element writes stay off the cross-worker pointer. - let acc = (start..end).fold(slot.take(), |acc, i| { - Some(match acc { - Some(a) => reduce(a, map(i)), - None => map(i), - }) + // Take/replace the shared slot once per batch so per-element writes stay local. + *slot = (start..end).fold(slot.take(), |acc, i| { + Some(acc.map_or_else(|| map(i), |a| reduce(a, map(i)))) }); - *slot = acc; }); slots.into_iter().flatten().fold(identity(), &reduce) } -/// Parallel reduce where each worker keeps reusable **scratch** alongside its -/// accumulator, so the per-task body can avoid allocating. Each worker creates -/// `(scratch, acc)` once on its first task and reuses the scratch across all the -/// tasks it claims; the per-worker `acc`s are then combined. `combine` must be -/// associative. +/// Parallel reduce where each worker keeps reusable scratch beside its accumulator (so the +/// per-task body needn't allocate). `(scratch, acc)` are created once per worker and threaded +/// through its batches; the `acc`s combine on the dispatcher. `combine` must be associative. pub fn map_reduce_with_state(n_tasks: usize, init_state: IS, init_acc: IA, fold: F, combine: C) -> A where S: Send, @@ -437,15 +387,13 @@ where C: Fn(A, A) -> A, { if NUM_THREADS <= 1 || n_tasks <= 1 { - let mut state = init_state(); - let mut acc = init_acc(); + let (mut state, mut acc) = (init_state(), init_acc()); for i in 0..n_tasks { fold(&mut state, &mut acc, i); } return acc; } let slots = drain_into_slots(n_tasks, |slot, start, end| { - // Scratch and accumulator are created once and threaded through every batch. let (state, acc) = slot.get_or_insert_with(|| (init_state(), init_acc())); for i in start..end { fold(state, acc, i); @@ -457,3 +405,30 @@ where .map(|(_, acc)| acc) .fold(init_acc(), &combine) } + +#[cfg(test)] +mod tests { + use super::*; + + /// A task panic must surface on the dispatcher (never hang the completion spin), and the + /// pool must stay usable for later dispatches. + #[test] + fn task_panic_propagates_and_pool_survives() { + init(); + // Silence the default hook so the intentional panic doesn't spam test output. + let prev_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(|_| {})); + let caught = catch_unwind(|| for_each_index(1 << 10, |i| assert_ne!(i, 513, "intentional task panic"))); + std::panic::set_hook(prev_hook); + assert!( + caught.is_err(), + "task panic should propagate to the dispatcher, not hang" + ); + + // Pool still works after a caught task panic. + assert_eq!(map_reduce(1000, || 0usize, |i| i, |a, b| a + b), (0..1000).sum()); + let mut data = vec![0usize; 1000]; + par_for_each_mut(&mut data, |i, slot| *slot = i); + assert!(data.iter().enumerate().all(|(i, &v)| i == v)); + } +} diff --git a/crates/backend/poly/src/eq_mle.rs b/crates/backend/poly/src/eq_mle.rs index 16c59f015..3f4f6e67b 100644 --- a/crates/backend/poly/src/eq_mle.rs +++ b/crates/backend/poly/src/eq_mle.rs @@ -30,6 +30,7 @@ where A: Sync, G: Fn(&mut [T], &A) + Sync, { + debug_assert_eq!(out.len(), chunk * buf.len()); parallel::par_chunks_mut(out, chunk, |i, c| g(c, &buf[i])); } diff --git a/crates/whir/src/dft.rs b/crates/whir/src/dft.rs index d4bb8c73c..91296c312 100644 --- a/crates/whir/src/dft.rs +++ b/crates/whir/src/dft.rs @@ -199,6 +199,7 @@ fn dft_layer>(vec: &mut [F], twiddles: &[B], width: us fn dft_layer_par>(vec: &mut [F], twiddles: &[B], width: usize) { let ts = twiddles.len(); let block_size = 2 * ts * width; + debug_assert!(vec.len().is_multiple_of(block_size),); let n_blocks = vec.len() / block_size; // Flatten (block, group) into one parallel loop over `n_blocks * ts` groups so coarse // layers (few blocks) still parallelize; guided scheduling keeps a worker's batch of diff --git a/src/lib.rs b/src/lib.rs index ddd2bc0d9..c805f00aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ pub fn tune_allocator() { /// Call once before proving. Compiles the aggregation program and precomputes DFT twiddles. pub fn setup_prover() { - parallel::init(); // construct the thread pool up front (was done by `zk_alloc::begin_phase`) + parallel::init(); rec_aggregation::init_aggregation_bytecode(); precompute_dft_twiddles::(1 << 24); } From ef6b00f6f1bf6e3adbdff7624656b42ad491b999 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 2 Jun 2026 22:43:21 +0200 Subject: [PATCH 43/65] wip --- crates/backend/parallel/src/lib.rs | 25 +++++++++++-------------- src/main.rs | 12 ------------ tests/test_aggregation.rs | 20 -------------------- tests/test_zk_alloc.rs | 24 ++++++++---------------- 4 files changed, 19 insertions(+), 62 deletions(-) delete mode 100644 tests/test_aggregation.rs diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index e3adeefce..af05d0be4 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -353,8 +353,9 @@ fn drain_into_slots(n_tasks: usize, run: impl Fn(&mut Option, usize, } /// Parallel map-reduce over `0..n_tasks` = `(0..n).map(map).reduce(identity, reduce)`. Each -/// worker folds its claimed indices into one local accumulator; the partials combine on the -/// dispatcher. `reduce` must be associative. +/// worker folds its claimed indices into one local partial; the partials combine on the +/// dispatcher. `reduce` must be associative with `identity()` a neutral element (rayon's +/// `reduce` contract). pub fn map_reduce(n_tasks: usize, identity: ID, map: M, reduce: R) -> T where T: Send, @@ -362,21 +363,22 @@ where M: Fn(usize) -> T + Sync, R: Fn(T, T) -> T + Sync, { - if NUM_THREADS <= 1 || n_tasks <= 1 { - return (0..n_tasks).fold(identity(), |acc, i| reduce(acc, map(i))); - } let slots = drain_into_slots(n_tasks, |slot, start, end| { - // Take/replace the shared slot once per batch so per-element writes stay local. + // Fold the batch into the worker's partial, seeded by the first `map` so `identity` + // stays off the per-element path; take/replace the shared slot just once. *slot = (start..end).fold(slot.take(), |acc, i| { Some(acc.map_or_else(|| map(i), |a| reduce(a, map(i)))) }); }); + // `identity()` seeds the combine as a no-op left-identity; the empty and single-thread + // (`for_each_chunk` runs inline) cases then fall out without a special path. slots.into_iter().flatten().fold(identity(), &reduce) } /// Parallel reduce where each worker keeps reusable scratch beside its accumulator (so the /// per-task body needn't allocate). `(scratch, acc)` are created once per worker and threaded -/// through its batches; the `acc`s combine on the dispatcher. `combine` must be associative. +/// through its batches; the `acc`s combine on the dispatcher. `combine` must be associative +/// with `init_acc()` a neutral element. pub fn map_reduce_with_state(n_tasks: usize, init_state: IS, init_acc: IA, fold: F, combine: C) -> A where S: Send, @@ -386,19 +388,14 @@ where F: Fn(&mut S, &mut A, usize) + Sync, C: Fn(A, A) -> A, { - if NUM_THREADS <= 1 || n_tasks <= 1 { - let (mut state, mut acc) = (init_state(), init_acc()); - for i in 0..n_tasks { - fold(&mut state, &mut acc, i); - } - return acc; - } let slots = drain_into_slots(n_tasks, |slot, start, end| { let (state, acc) = slot.get_or_insert_with(|| (init_state(), init_acc())); for i in start..end { fold(state, acc, i); } }); + // `init_acc()` seeds the combine as a neutral element; the empty and single-thread cases + // (`for_each_chunk` runs inline) then fall out without a special path. slots .into_iter() .flatten() diff --git a/src/main.rs b/src/main.rs index 485e00986..b13e6cd97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,6 @@ use clap::Parser; use rec_aggregation::benchmark::{AggregationTopology, biggest_leaf, run_aggregation_benchmark}; -// Allocator: zk-alloc — a bump+reset arena. Allocation is a pointer bump and free is a no-op; -// `begin_phase`/`end_phase` (see `benchmark.rs`) reset every thread's slab between proofs so the -// prover's huge per-phase buffer churn reuses pages instead of re-faulting. Outputs are cloned -// out after `end_phase` so they detach to the system allocator before the next reset. -// The `standard-alloc` feature selects the plain system allocator for comparison. #[cfg(not(feature = "standard-alloc"))] #[global_allocator] static ALLOC: zk_alloc::ZkAllocator = zk_alloc::ZkAllocator; @@ -57,11 +52,6 @@ enum Cli { } fn run_with_warmup(topology: &AggregationTopology, tracing: bool, json: bool, repeat: usize) { - // Build all persistent state — thread pool, compiled bytecode, DFT twiddles — before the - // first `begin_phase()`, so it lands in the system allocator and survives the per-proof - // slab resets. Without this it would initialize lazily inside the phased warm-up, land in - // arena memory, and be corrupted by the next phase's reset. (The arena requires all - // cross-phase state to predate the first phase; the tests already do this.) lean_multisig::setup_prover(); let warmup = biggest_leaf(topology).unwrap(); eprint!("warming up... "); @@ -78,9 +68,7 @@ fn run_with_warmup(topology: &AggregationTopology, tracing: bool, json: bool, re #[allow(clippy::too_many_lines)] fn main() { - // Disable THP for the prover's strided arrays (see `tune_allocator`), before any work. lean_multisig::tune_allocator(); - // Validate the build-time thread count before the arena maps its slabs. #[cfg(not(feature = "standard-alloc"))] lean_multisig::init_allocator(); diff --git a/tests/test_aggregation.rs b/tests/test_aggregation.rs deleted file mode 100644 index aa9fb78ff..000000000 --- a/tests/test_aggregation.rs +++ /dev/null @@ -1,20 +0,0 @@ -use lean_multisig::{aggregate_single_msg_signatures, setup_prover, verify_single_message_aggregate}; -use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}; - -// End-to-end prove+verify under the system allocator (no arena phases). Repeated to catch -// any cross-run state corruption. `test_zk_alloc.rs` is the matching arena-allocator run. -#[test] -fn test_aggregation_prove_verify() { - setup_prover(); - - let log_inv_rate = 2; - let message = message_for_benchmark(); - let slot: u32 = BENCHMARK_SLOT; - let signatures = get_benchmark_signatures(); - let raw_xmss = signatures[0..6].to_vec(); - - for _ in 0..2 { - let aggregated = aggregate_single_msg_signatures(&[], raw_xmss.clone(), message, slot, log_inv_rate).unwrap(); - verify_single_message_aggregate(&aggregated).unwrap(); - } -} diff --git a/tests/test_zk_alloc.rs b/tests/test_zk_alloc.rs index cd64c62b9..4596bde61 100644 --- a/tests/test_zk_alloc.rs +++ b/tests/test_zk_alloc.rs @@ -6,30 +6,22 @@ use xmss::signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_ #[global_allocator] static ALLOC: ZkAllocator = ZkAllocator; -// Exercise the full multi-phase arena lifecycle and verify every proof. Each `begin_phase` -// resets the slabs, overwriting the previous phase's arena memory — so if any persistent -// state (thread pool, compiled bytecode, twiddles, static caches) had leaked into the arena -// instead of the system allocator, a later phase's proof would be built on corrupted data and -// fail to verify. `setup_prover()` (called before the first phase) settles that state in the -// system allocator up front; this test guards that contract across several phases. #[test] #[allow(clippy::redundant_clone)] fn test_aggregation_with_zk_alloc() { setup_prover(); let log_inv_rate = 2; + let message = message_for_benchmark(); let slot: u32 = BENCHMARK_SLOT; let signatures = get_benchmark_signatures(); let raw_xmss = signatures[0..6].to_vec(); - for _ in 0..3 { - begin_phase(); - let aggregated = - aggregate_single_msg_signatures(&[], raw_xmss.clone(), message_for_benchmark(), slot, log_inv_rate) - .unwrap(); - end_phase(); - // IMPORTANT: clone to move the data out of the arena memory before the next reset. - let aggregated = aggregated.clone(); - verify_single_message_aggregate(&aggregated).unwrap(); - } + begin_phase(); + let aggregated = aggregate_single_msg_signatures(&[], raw_xmss, message, slot, log_inv_rate).unwrap(); + end_phase(); + // IMPORTANT: clone to move the data out of the arena memory + let aggregated = aggregated.clone(); + + verify_single_message_aggregate(&aggregated).unwrap(); } From 4a24bc77d2cdd8bff91d834d88f63eda3121f307 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 2 Jun 2026 23:14:05 +0200 Subject: [PATCH 44/65] fix perf --- crates/backend/parallel/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index af05d0be4..cd6a24104 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -276,7 +276,11 @@ pub fn for_each_chunk(n_tasks: usize, f: F) { /// into the monomorphized [`for_each_chunk`]. #[inline] pub fn for_each_index(n_tasks: usize, f: F) { - for_each_chunk(n_tasks, |start, end| (start..end).for_each(&f)); + for_each_chunk(n_tasks, |start, end| { + for i in start..end { + f(i); + } + }); } /// A base `*mut` shareable across workers. Sound only because callers partition the allocation From 7b7e1ba7dd332311d241a90db02ed72e66c40ff4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 3 Jun 2026 10:53:56 +0200 Subject: [PATCH 45/65] clean --- .gitignore | 2 - Cargo.lock | 92 +++++++++++++++---------------- crates/backend/poly/src/eq_mle.rs | 86 ++--------------------------- src/lib.rs | 4 +- src/main.rs | 1 - 5 files changed, 50 insertions(+), 135 deletions(-) diff --git a/.gitignore b/.gitignore index 2c2fca099..55d8a4c3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ /target -/target-* -/.lm-* .vscode /docs/benchmark_graphs/.venv minimal_zkVM.synctex.gz diff --git a/Cargo.lock b/Cargo.lock index 6a680a83e..cc84bd751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,9 +87,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backend" @@ -110,9 +110,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.12.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -157,9 +157,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "hybrid-array", ] @@ -271,20 +271,20 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer 0.12.0", "const-oid", - "crypto-common 0.2.2", + "crypto-common 0.2.1", ] [[package]] name = "either" -version = "1.16.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embedded-io" @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heapless" @@ -380,9 +380,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ "typenum", ] @@ -414,12 +414,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.17.1", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -552,15 +552,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.31" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lz4_flex" -version = "0.13.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" dependencies = [ "twox-hash", ] @@ -576,9 +576,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mt-air" @@ -877,9 +877,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", "getrandom", @@ -888,9 +888,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "rec_aggregation" @@ -948,9 +948,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.28" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" @@ -984,9 +984,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -1012,7 +1012,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ - "digest 0.11.3", + "digest 0.11.2", "keccak", ] @@ -1193,9 +1193,9 @@ checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] name = "typenum" -version = "1.20.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -1246,11 +1246,11 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen 0.57.1", + "wit-bindgen", ] [[package]] @@ -1259,7 +1259,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -1342,12 +1342,6 @@ dependencies = [ "wit-bindgen-rust-macro", ] -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/crates/backend/poly/src/eq_mle.rs b/crates/backend/poly/src/eq_mle.rs index 3f4f6e67b..ca54a2e4d 100644 --- a/crates/backend/poly/src/eq_mle.rs +++ b/crates/backend/poly/src/eq_mle.rs @@ -9,13 +9,13 @@ const LOG_BATCHED_TILE_SIZE: usize = 14; /// log2 oversubscription for the eq_mle fan-out: emit `NUM_THREADS << this` chunks so the /// pool's task counter rebalances across heterogeneous cores (e.g. P/E). `0` = one chunk -/// per worker. Default `2` (4x) is conservative; a runtime knob so the benchmark can sweep. -pub static PARALLEL_LOG_OVERSUB: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(2); +/// per worker; `2` (4x) is a conservative default that balances well without over-fragmenting. +const PARALLEL_LOG_OVERSUB: usize = 2; -/// `(log2(n_chunks), n_chunks)` for the parallel fan-out, honoring [`PARALLEL_LOG_OVERSUB`]. +/// `(log2(n_chunks), n_chunks)` for the parallel fan-out. #[inline] fn parallel_split() -> (usize, usize) { - let log_chunks = LOG_NUM_THREADS + PARALLEL_LOG_OVERSUB.load(std::sync::atomic::Ordering::Relaxed); + let log_chunks = LOG_NUM_THREADS + PARALLEL_LOG_OVERSUB; (log_chunks, 1 << log_chunks) } @@ -1306,84 +1306,6 @@ mod tests { } } - #[test] - #[ignore = "benchmark; run explicitly with --ignored --nocapture"] - fn bench_pool_oversub() { - use std::sync::atomic::Ordering; - use std::time::Instant; - - // Sweep oversubscription log-factors. 0 == one chunk per worker. - const FACTORS: [usize; 6] = [0, 1, 2, 3, 4, 5]; - - let mut rng = StdRng::seed_from_u64(0); - - // Time `f` over `iters` runs after `warmup` discarded runs; report best. - fn timed(warmup: usize, iters: usize, mut f: impl FnMut()) -> std::time::Duration { - for _ in 0..warmup { - f(); - } - let mut best = std::time::Duration::MAX; - for _ in 0..iters { - let t = Instant::now(); - f(); - best = best.min(t.elapsed()); - } - best - } - - // Time `run` at every oversub factor; print ms (best-of-N) per factor, with - // the per-row best marked. Lets us pick a factor robust across machines. - fn sweep(label: &str, n_vars: usize, warmup: usize, iters: usize, mut run: impl FnMut()) { - let restore = PARALLEL_LOG_OVERSUB.load(Ordering::Relaxed); - let times: Vec = FACTORS - .iter() - .map(|&f| { - PARALLEL_LOG_OVERSUB.store(f, Ordering::Relaxed); - timed(warmup, iters, &mut run).as_secs_f64() * 1e3 - }) - .collect(); - PARALLEL_LOG_OVERSUB.store(restore, Ordering::Relaxed); - let best = times.iter().copied().fold(f64::MAX, f64::min); - print!(" {label:>14} n={n_vars:>2} |"); - for &t in × { - let mark = if (t - best).abs() < 1e-9 { '*' } else { ' ' }; - print!(" {t:>6.2}{mark}"); - } - println!(); - } - - print!("\n oversub factor:"); - for f in FACTORS { - print!(" {f:>2}x "); - } - println!(" (ms, best-of-N, * = row best)"); - for n_vars in [18usize, 20, 22, 23, 24] { - let eval_ef: Vec = (0..n_vars).map(|_| rng.random()).collect(); - let eval_f: Vec = (0..n_vars).map(|_| rng.random()).collect(); - let scalar: EF = rng.random(); - let (warmup, iters) = if n_vars >= 23 { (1, 3) } else { (2, 10) }; - - // Correctness: the factor must not change the result. - PARALLEL_LOG_OVERSUB.store(0, Ordering::Relaxed); - let mut ref_out = EF::zero_vec(1 << n_vars); - compute_eval_eq::(&eval_ef, &mut ref_out, scalar); - for f in FACTORS { - PARALLEL_LOG_OVERSUB.store(f, Ordering::Relaxed); - let mut out = EF::zero_vec(1 << n_vars); - compute_eval_eq::(&eval_ef, &mut out, scalar); - assert_eq!(ref_out, out, "oversub {f} changed output (ext) at n={n_vars}"); - } - - let mut out = EF::zero_vec(1 << n_vars); - sweep("eval_eq (ext)", n_vars, warmup, iters, || { - compute_eval_eq::(&eval_ef, &mut out, scalar); - }); - sweep("eval_eq_base", n_vars, warmup, iters, || { - compute_eval_eq_base::(&eval_f, &mut out, scalar); - }); - } - } - #[test] fn test_compute_eval_eq_packed_dual() { let packing_width = ::Packing::WIDTH; diff --git a/src/lib.rs b/src/lib.rs index c805f00aa..278dd5e56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,8 +31,10 @@ pub fn tune_allocator() { } } -/// Call once before proving. Compiles the aggregation program and precomputes DFT twiddles. +/// Call once before proving. Tunes the process memory policy (see [`tune_allocator`]), +/// compiles the aggregation program, and precomputes DFT twiddles. pub fn setup_prover() { + tune_allocator(); parallel::init(); rec_aggregation::init_aggregation_bytecode(); precompute_dft_twiddles::(1 << 24); diff --git a/src/main.rs b/src/main.rs index b13e6cd97..b8c3d1bb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,7 +68,6 @@ fn run_with_warmup(topology: &AggregationTopology, tracing: bool, json: bool, re #[allow(clippy::too_many_lines)] fn main() { - lean_multisig::tune_allocator(); #[cfg(not(feature = "standard-alloc"))] lean_multisig::init_allocator(); From 583eead1e7d4363760d08ab93e55d7eb149a1302 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 3 Jun 2026 11:05:07 +0200 Subject: [PATCH 46/65] wip --- crates/backend/parallel/src/lib.rs | 9 +++++++++ crates/backend/zk-alloc/src/lib.rs | 10 ---------- src/lib.rs | 23 ++++++----------------- src/main.rs | 3 --- 4 files changed, 15 insertions(+), 30 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index cd6a24104..08c6c1d37 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -108,9 +108,18 @@ unsafe impl Send for Pool {} /// Idempotent warm-up: spawn workers and run one empty dispatch so the pool and the (macOS) /// lazily-allocated mutex exist before timed work. Otherwise the pool inits on first use. +/// +/// Also fail-fast if the machine's core count differs from the build-time [`NUM_THREADS`] +/// (which sizes the whole pool): a mismatch silently over/under-subscribes every kernel, so +/// surface it as "rebuild" rather than a quiet perf cliff. pub fn init() { static INIT: Once = Once::new(); INIT.call_once(|| { + let actual = std::thread::available_parallelism().unwrap().get(); + assert_eq!( + actual, NUM_THREADS, + "parallel pool built for {NUM_THREADS} threads but this machine reports {actual} -> please rebuild" + ); let _ = pool(); if NUM_THREADS > 1 { for_each_index(NUM_THREADS, |_| {}); diff --git a/crates/backend/zk-alloc/src/lib.rs b/crates/backend/zk-alloc/src/lib.rs index 6c5f5a10a..f0433d3a4 100644 --- a/crates/backend/zk-alloc/src/lib.rs +++ b/crates/backend/zk-alloc/src/lib.rs @@ -7,7 +7,6 @@ //! back to the system allocator. //! //! ```ignore -//! init(); // once, at process start //! loop { //! begin_phase(); // arena ON; slabs reset lazily //! let res = heavy_work(); // fast increments @@ -88,15 +87,6 @@ fn ensure_region() -> usize { REGION_BASE.load(Ordering::Acquire) } -/// Call once at process start, before any `begin_phase()`. -pub fn init() { - let actual_num_threads = std::thread::available_parallelism().unwrap().get(); - assert_eq!( - actual_num_threads, NUM_THREADS, - "built for {NUM_THREADS} threads but this machine reports {actual_num_threads} -> please rebuild`" - ); -} - /// Activates the arena and resets every thread's slab. All allocations until the next /// `end_phase()` go to the arena; the previous phase's data is overwritten in place. pub fn begin_phase() { diff --git a/src/lib.rs b/src/lib.rs index 278dd5e56..78046ed7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,22 +11,11 @@ pub use xmss::{MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, xmss pub type F = KoalaBear; -/// Tune the VM memory policy for the prover's churn of huge buffers. -/// -/// **Disable Transparent Huge Pages for this process.** On Zen4 (and likely other x86 with -/// physically-indexed L2/L3), when the kernel promotes the allocator's large arenas to -/// 2 MB huge pages, the prover's strided multilinear/NTT array access collapses into a few -/// cache sets — measured **+217% cache-misses, IPC 0.85 → 0.51, +50% wall time** on -/// `fancy-aggregation`. It's intermittent (only fires when 2 MB-contiguous memory is free -/// for THP promotion), which is what made it so hard to pin down. `prctl(PR_SET_THP_DISABLE)` -/// is process-local and overrides even a system-wide `THP=always`. No-op off Linux (macOS -/// has no THP — Apple silicon was never affected). Applies under any allocator. -// Not `const`: the body is non-empty (and non-const) on Linux; it only looks empty elsewhere. #[allow(clippy::missing_const_for_fn)] pub fn tune_allocator() { - // Keep the arena's slabs on 4 KB pages. #[cfg(target_os = "linux")] unsafe { + // Disable Transparent Huge Pages libc::prctl(libc::PR_SET_THP_DISABLE, 1, 0, 0, 0); } } @@ -47,10 +36,10 @@ pub fn setup_verifier() { /// Bump-arena allocator. /// -/// To enable, set it as the `#[global_allocator]` in your binary and call [`init_allocator`] -/// once at startup. Then bracket each proving call with [`begin_phase`] / [`end_phase`] and -/// **clone the outputs after [`end_phase`]** so the cloned copy lands in the system allocator -/// before the next [`begin_phase`] resets the arena slabs. +/// To enable, set it as the `#[global_allocator]` in your binary. Then bracket each proving +/// call with [`begin_phase`] / [`end_phase`] and **clone the outputs after [`end_phase`]** so +/// the cloned copy lands in the system allocator before the next [`begin_phase`] resets the +/// arena slabs. /// /// See `tests/test_zk_alloc.rs` for a runnable end-to-end example. -pub use zk_alloc::{ZkAllocator, begin_phase, end_phase, init as init_allocator}; +pub use zk_alloc::{ZkAllocator, begin_phase, end_phase}; diff --git a/src/main.rs b/src/main.rs index b8c3d1bb3..c77e12ea5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,9 +68,6 @@ fn run_with_warmup(topology: &AggregationTopology, tracing: bool, json: bool, re #[allow(clippy::too_many_lines)] fn main() { - #[cfg(not(feature = "standard-alloc"))] - lean_multisig::init_allocator(); - let cli = Cli::parse(); match cli { From f69feb02cf7cd2bbb86e7f75034dcd644fcb3e24 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 3 Jun 2026 11:16:36 +0200 Subject: [PATCH 47/65] remove "Disable Transparent Huge Pages" --- src/lib.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 78046ed7e..eea73127b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,19 +11,9 @@ pub use xmss::{MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, xmss pub type F = KoalaBear; -#[allow(clippy::missing_const_for_fn)] -pub fn tune_allocator() { - #[cfg(target_os = "linux")] - unsafe { - // Disable Transparent Huge Pages - libc::prctl(libc::PR_SET_THP_DISABLE, 1, 0, 0, 0); - } -} - /// Call once before proving. Tunes the process memory policy (see [`tune_allocator`]), /// compiles the aggregation program, and precomputes DFT twiddles. pub fn setup_prover() { - tune_allocator(); parallel::init(); rec_aggregation::init_aggregation_bytecode(); precompute_dft_twiddles::(1 << 24); From d603440ec443b5441670903b7f9d368bc66e3644 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 3 Jun 2026 11:18:36 +0200 Subject: [PATCH 48/65] wip --- crates/xmss/src/signers_cache.rs | 2 -- crates/xmss/src/xmss.rs | 6 ------ 2 files changed, 8 deletions(-) diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 9afd176bc..80c978c84 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -65,8 +65,6 @@ fn compute_signer(index: usize) -> (XmssPublicKey, XmssSignature) { let mut rng = StdRng::seed_from_u64(index as u64); let key_start = BENCHMARK_SLOT; let key_end = BENCHMARK_SLOT + 1; - // `sequential = true`: this runs inside the parallel per-signer cache builder, so the - // inner key-gen loops must not dispatch again (nested pool dispatch is forbidden). let (sk, pk) = xmss_key_gen(rng.random(), key_start, key_end, true).unwrap(); let sig = xmss_sign(&mut rng, &sk, &message_for_benchmark(), BENCHMARK_SLOT).unwrap(); (pk, sig) diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 5eaf14280..9f77837af 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -74,10 +74,6 @@ pub enum XmssKeyGenError { InvalidRange, } -/// Fill `data` with `f`, in parallel — unless `sequential`, in which case run it inline. -/// The sequential path exists for callers already running inside a pool task (e.g. the -/// benchmark cache builder, which parallelizes over signers): the custom pool forbids nested -/// dispatch, and the outer level has already saturated the cores. fn fill(sequential: bool, data: &mut [T], f: impl Fn(usize, &mut T) + Sync) { if sequential { data.iter_mut().enumerate().for_each(|(i, out)| f(i, out)); @@ -86,8 +82,6 @@ fn fill(sequential: bool, data: &mut [T], f: impl Fn(usize, &mut T) + S } } -/// `sequential` runs the internal leaf/node loops inline instead of dispatching to the pool; -/// pass `true` when calling from within an outer parallel loop (see [`fill`]), `false` otherwise. pub fn xmss_key_gen( seed: [u8; 32], slot_start: u32, From be7ca9577d130999fe938e03db26d0207a099f2c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 3 Jun 2026 11:24:40 +0200 Subject: [PATCH 49/65] par_map_collect --- crates/backend/parallel/src/lib.rs | 46 ++++++++++++------------------ crates/xmss/src/signers_cache.rs | 6 ++-- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 08c6c1d37..9a243b9a3 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -349,6 +349,23 @@ where }); } +/// Parallel `(0..n_tasks).map(f).collect::>()`: runs `f(i)` across the pool and gathers +/// the results in index order, writing each straight into the output (no `Option` slots, one +/// allocation). Folds away the common "fill a `Vec>` in parallel, then unwrap" dance. +pub fn par_map_collect T + Sync>(n_tasks: usize, f: F) -> Vec { + let mut out: Vec = Vec::with_capacity(n_tasks); + let base = SendPtr(out.as_mut_ptr()); + for_each_index(n_tasks, |i| { + // SAFETY: distinct `i` write disjoint, in-bounds slots (each exactly once) and the + // dispatch blocks until all writes finish. A panic in `f` leaks the slots written so + // far, which is fine: a pool task panic is fatal (see the module's "Panics" note). + unsafe { base.add(i).write(f(i)) }; + }); + // SAFETY: every slot in `0..n_tasks` was initialized exactly once above. + unsafe { out.set_len(n_tasks) }; + out +} + /// Give each worker its own persistent `Option` slot while it drains `0..n_tasks`: /// `run(slot, start, end)` fires once per claimed batch with that worker's slot, so state /// accumulates across its batches. Returns the slots (rest `None`) for the caller to combine. @@ -414,31 +431,4 @@ where .flatten() .map(|(_, acc)| acc) .fold(init_acc(), &combine) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// A task panic must surface on the dispatcher (never hang the completion spin), and the - /// pool must stay usable for later dispatches. - #[test] - fn task_panic_propagates_and_pool_survives() { - init(); - // Silence the default hook so the intentional panic doesn't spam test output. - let prev_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(|_| {})); - let caught = catch_unwind(|| for_each_index(1 << 10, |i| assert_ne!(i, 513, "intentional task panic"))); - std::panic::set_hook(prev_hook); - assert!( - caught.is_err(), - "task panic should propagate to the dispatcher, not hang" - ); - - // Pool still works after a caught task panic. - assert_eq!(map_reduce(1000, || 0usize, |i| i, |a, b| a + b), (0..1000).sum()); - let mut data = vec![0usize; 1000]; - par_for_each_mut(&mut data, |i, slot| *slot = i); - assert!(data.iter().enumerate().all(|(i, &v)| i == v)); - } -} +} \ No newline at end of file diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 80c978c84..b843b9c28 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -90,17 +90,15 @@ fn gen_benchmark_signers_cache() -> Vec<(XmssPublicKey, XmssSignature)> { let completed = AtomicUsize::new(1); let time = Instant::now(); let n_rest = NUM_BENCHMARK_SIGNERS - 1; - let mut rest_opt: Vec> = (0..n_rest).map(|_| None).collect(); - parallel::par_for_each_mut(&mut rest_opt, |i, out| { + let rest = parallel::par_map_collect(n_rest, |i| { let signer = compute_signer(1 + i); let done = completed.fetch_add(1, Ordering::Relaxed) + 1; print!( "\rPrecomputing benchmark signatures (cached after first run): {:.0}%", 100.0 * done as f64 / NUM_BENCHMARK_SIGNERS as f64 ); - *out = Some(signer); + signer }); - let rest: Vec<_> = rest_opt.into_iter().map(Option::unwrap).collect(); println!( "\rGenerating signatures for benchmark (one-time operation): 100% - done ({:.2}s)", From 19210e2c4423ac13689727a9dfef4007f0659fe7 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 3 Jun 2026 11:42:56 +0200 Subject: [PATCH 50/65] wip --- crates/backend/parallel/src/lib.rs | 2 +- crates/backend/poly/src/evals.rs | 7 +++---- crates/backend/poly/src/utils.rs | 14 ++++---------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 9a243b9a3..d75624914 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -431,4 +431,4 @@ where .flatten() .map(|(_, acc)| acc) .fold(init_acc(), &combine) -} \ No newline at end of file +} diff --git a/crates/backend/poly/src/evals.rs b/crates/backend/poly/src/evals.rs index c1168470f..b11f2ff81 100644 --- a/crates/backend/poly/src/evals.rs +++ b/crates/backend/poly/src/evals.rs @@ -312,15 +312,14 @@ where let (lead, sub_point) = point.split_at(n_split); let n_chunks = 1 << n_split; let chunk = evals.len() >> n_split; - let mut partials = vec![Res::ZERO; n_chunks]; - parallel::par_chunks_mut(&mut partials, 1, |j, slot| { - slot[0] = eval_multilinear_generic::<_, _, _, _, _, _, false>( + let partials = parallel::par_map_collect(n_chunks, |j| { + eval_multilinear_generic::<_, _, _, _, _, _, false>( &evals[j * chunk..][..chunk], sub_point, mul_coeffs_point, add_res_coeffs, mul_res_point, - ); + ) }); interpolate_res(&partials, lead, mul_res_point) } else { diff --git a/crates/backend/poly/src/utils.rs b/crates/backend/poly/src/utils.rs index cc729bc74..3f531a19c 100644 --- a/crates/backend/poly/src/utils.rs +++ b/crates/backend/poly/src/utils.rs @@ -161,11 +161,7 @@ pub fn batch_fold_multilinears< .map(|poly| fold_multilinear(poly, alpha, &mul_if_of, true)) .collect() } else { - let mut out: Vec> = (0..polys.len()).map(|_| Vec::new()).collect(); - parallel::par_chunks_mut(&mut out, 1, |i, slot| { - slot[0] = fold_multilinear(polys[i], alpha, &mul_if_of, true); - }); - out + parallel::par_map_collect(polys.len(), |i| fold_multilinear(polys[i], alpha, &mul_if_of, true)) } } @@ -188,11 +184,9 @@ pub fn batch_fold_multilinears_at_bit< .map(|poly| fold_multilinear_at_bit(poly, alpha, bit, &mul_if_of, true)) .collect() } else { - let mut out: Vec> = (0..polys.len()).map(|_| Vec::new()).collect(); - parallel::par_chunks_mut(&mut out, 1, |i, slot| { - slot[0] = fold_multilinear_at_bit(polys[i], alpha, bit, &mul_if_of, true); - }); - out + parallel::par_map_collect(polys.len(), |i| { + fold_multilinear_at_bit(polys[i], alpha, bit, &mul_if_of, true) + }) } } From 899187ffd3ae14ed75c768ddfd601de4ffa69e74 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 13:50:24 +0200 Subject: [PATCH 51/65] wip --- crates/sub_protocols/src/logup.rs | 2 -- crates/sub_protocols/src/quotient_gkr/layers.rs | 2 -- .../src/quotient_gkr/sumcheck_utils.rs | 2 -- crates/utils/src/misc.rs | 5 ----- crates/whir/src/dft.rs | 14 -------------- crates/whir/src/merkle.rs | 1 - crates/whir/src/open.rs | 2 -- src/lib.rs | 3 +-- 8 files changed, 1 insertion(+), 30 deletions(-) diff --git a/crates/sub_protocols/src/logup.rs b/crates/sub_protocols/src/logup.rs index 85578a1d7..d4db50ce7 100644 --- a/crates/sub_protocols/src/logup.rs +++ b/crates/sub_protocols/src/logup.rs @@ -534,8 +534,6 @@ where par_fill(dst, build); } -/// Fill `dst` in parallel through the in-house pool, computing each slot from its -/// global index. Replaces the rayon `par_iter_mut().enumerate()` constant/index fills. #[inline] fn par_fill T + Sync>(dst: &mut [T], build: Build) { parallel::par_for_each_mut(dst, |i, slot| *slot = build(i)); diff --git a/crates/sub_protocols/src/quotient_gkr/layers.rs b/crates/sub_protocols/src/quotient_gkr/layers.rs index b6c4502e1..4dfaf82e4 100644 --- a/crates/sub_protocols/src/quotient_gkr/layers.rs +++ b/crates/sub_protocols/src/quotient_gkr/layers.rs @@ -137,7 +137,6 @@ fn sum_quotients_2_by_2>>(nums: &[EF], dens: &[EF]) -> let d0 = dens[2 * i]; let d1 = dens[2 * i + 1]; *num = d1 * n0 + d0 * n1; - // SAFETY: each `i` writes a distinct slot in `new_dens`, a separate buffer. unsafe { *dp.add(i) = d0 * d1 }; }); } @@ -179,7 +178,6 @@ where let i0 = (i_hi << (bit + 1)) | i_lo; let i1 = i0 | stride; *num_out = dens[i1] * nums[i0] + dens[i0] * nums[i1]; - // SAFETY: each `new_j` writes a distinct slot in `new_dens`, a separate buffer. unsafe { *dp.add(new_j) = dens[i0] * dens[i1] }; }); } diff --git a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs index 0f17c4035..1bcb50e37 100644 --- a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs +++ b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs @@ -481,8 +481,6 @@ where |c| { let n_c = &nums[c * in_packed..][..in_packed]; let d_c = &dens[c * in_packed..][..in_packed]; - // SAFETY: chunk `c` owns the disjoint `out_packed`-sized regions of the two - // output buffers at `c * out_packed`; no other task touches them. let nn_c = unsafe { std::slice::from_raw_parts_mut(nn.add(c * out_packed), out_packed) }; let nd_c = unsafe { std::slice::from_raw_parts_mut(nd.add(c * out_packed), out_packed) }; let eq_o: EF = eq_outer.get(c).copied().unwrap_or(EF::ONE); diff --git a/crates/utils/src/misc.rs b/crates/utils/src/misc.rs index 09bad5162..a707dd0af 100644 --- a/crates/utils/src/misc.rs +++ b/crates/utils/src/misc.rs @@ -7,9 +7,6 @@ pub fn from_end(slice: &[A], n: usize) -> &[A] { &slice[slice.len() - n..] } -/// Run `g(i, row)` in parallel over `i in 0..len`, where `row` is `[&mut A; N]` holding -/// the `i`-th element of each of the `N` equal-length vectors (a transposed row). -/// Dispatched through the in-house [`parallel`] pool. pub fn transposed_par_for_each_mut(array: &mut [Vec; N], g: G) where G: Fn(usize, [&mut A; N]) + Sync, @@ -19,8 +16,6 @@ where let data_ptrs: [AtomicPtr; N] = array.each_mut().map(|v| AtomicPtr::new(v.as_mut_ptr())); parallel::for_each_index(len, |i| { - // SAFETY: distinct `i` access disjoint row `i` of each of the `N` vectors, and the - // arrays outlive the dispatch (the dispatcher blocks until all tasks complete). let row: [&mut A; N] = unsafe { std::array::from_fn(|j| &mut *data_ptrs[j].load(Ordering::Relaxed).add(i)) }; g(i, row); }); diff --git a/crates/whir/src/dft.rs b/crates/whir/src/dft.rs index 91296c312..c523cf379 100644 --- a/crates/whir/src/dft.rs +++ b/crates/whir/src/dft.rs @@ -201,14 +201,10 @@ fn dft_layer_par>(vec: &mut [F], twiddles: &[B], width let block_size = 2 * ts * width; debug_assert!(vec.len().is_multiple_of(block_size),); let n_blocks = vec.len() / block_size; - // Flatten (block, group) into one parallel loop over `n_blocks * ts` groups so coarse - // layers (few blocks) still parallelize; guided scheduling keeps a worker's batch of - // consecutive groups within the same block, preserving the per-block cache locality. let base = parallel::SendPtr(vec.as_mut_ptr()); parallel::for_each_index(n_blocks * ts, |g| { let block_base = (g / ts) * block_size; let ind = g % ts; - // SAFETY: distinct `g` map to disjoint (hi, lo) `width`-rows. let hi = unsafe { base.slice(block_base + ind * width, width) }; let lo = unsafe { base.slice(block_base + (ts + ind) * width, width) }; twiddles[ind].apply_to_rows(hi, lo); @@ -241,10 +237,6 @@ fn dft_layer_par_double, M: MultiLayerButterfly> assert_eq!(twiddles_large.len(), twiddles_small.len() * 2); - // Flatten (block, inner-group) into one parallel loop. A block is `4·ts` rows of - // `width`; group `ind` touches the 4 rows at sub-block offsets `k·ts + ind` (k=0..3). - // Coarse layers (few blocks) thus still parallelize over their `ts` inner groups, and - // guided scheduling keeps a worker's consecutive groups within one block (cache-local). let ts = twiddles_small.len(); let block_size = 4 * ts * width; // == twiddles_large.len() * 2 * width let n_blocks = mat.values.len() / block_size; @@ -253,7 +245,6 @@ fn dft_layer_par_double, M: MultiLayerButterfly> let block_base = (g / ts) * block_size; let ind = g % ts; let row = |k: usize| block_base + (k * ts + ind) * width; - // SAFETY: distinct `g` map to disjoint sets of 4 `width`-rows. let hi_hi = unsafe { base.slice(row(0), width) }; let hi_lo = unsafe { base.slice(row(1), width) }; let lo_hi = unsafe { base.slice(row(2), width) }; @@ -295,10 +286,6 @@ fn dft_layer_par_triple, M: MultiLayerButterfly> // let inner_chunk_size = // (workload_size::().next_power_of_two() / 8).min(eighth_outer_block_size); - // Flatten (block, inner-group) into one parallel loop. A block is `8·ts` rows of - // `width`; group `ind` touches the 8 rows at sub-block offsets `k·ts + ind` (k=0..7). - // Coarse layers still parallelize over their `ts` inner groups; guided scheduling keeps - // a worker's consecutive groups within one block (cache-local). let ts = twiddles_small.len(); let block_size = 8 * ts * width; // == twiddles_large.len() * 2 * width let n_blocks = mat.values.len() / block_size; @@ -307,7 +294,6 @@ fn dft_layer_par_triple, M: MultiLayerButterfly> let block_base = (g / ts) * block_size; let ind = g % ts; let row = |k: usize| block_base + (k * ts + ind) * width; - // SAFETY: distinct `g` map to disjoint sets of 8 `width`-rows. let hi_hi_hi = unsafe { base.slice(row(0), width) }; let hi_hi_lo = unsafe { base.slice(row(1), width) }; let hi_lo_hi = unsafe { base.slice(row(2), width) }; diff --git a/crates/whir/src/merkle.rs b/crates/whir/src/merkle.rs index e6c6f5a79..44e973d26 100644 --- a/crates/whir/src/merkle.rs +++ b/crates/whir/src/merkle.rs @@ -193,7 +193,6 @@ where let mut digests = unsafe { uninitialized_vec(height) }; - // `height` is a multiple of `width`, so every chunk is exactly `width` long. parallel::par_chunks_mut(&mut digests, width, |i, digests_chunk| { let first_row = i * width; let rtl_iter = matrix.vertically_packed_row_rtl::

(first_row, effective_base_width, n_pad); diff --git a/crates/whir/src/open.rs b/crates/whir/src/open.rs index 6b737b9c4..6fa6fbed7 100644 --- a/crates/whir/src/open.rs +++ b/crates/whir/src/open.rs @@ -593,8 +593,6 @@ where for (e, &scalar) in smt.values.iter().zip(&next_gamma_powers) { combined_sum += e.value * scalar; } - // Few sparse statements (the outer chunks) but each inner accumulation can be - // large, so parallelize the inner loop per statement (the outer runs serial). for (out_buff, &(origin_index, _)) in chunks_mut.iter_mut().zip(&indexed_smt_values) { let out = &mut out_buff[..1 << shift]; let scalar = next_gamma_powers[origin_index]; diff --git a/src/lib.rs b/src/lib.rs index eea73127b..451fe4ea3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,7 @@ pub use xmss::{MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, xmss pub type F = KoalaBear; -/// Call once before proving. Tunes the process memory policy (see [`tune_allocator`]), -/// compiles the aggregation program, and precomputes DFT twiddles. +/// Call once before proving. pub fn setup_prover() { parallel::init(); rec_aggregation::init_aggregation_bytecode(); From da59c3677a32f6edea9730cb66f0fa31216d68ea Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 14:08:23 +0200 Subject: [PATCH 52/65] wip --- crates/backend/parallel/src/lib.rs | 68 ++++++++++--------- crates/backend/poly/src/utils.rs | 2 +- crates/sub_protocols/src/logup.rs | 20 ++---- .../src/quotient_gkr/sumcheck_utils.rs | 2 +- 4 files changed, 42 insertions(+), 50 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index d75624914..eb7ed9ff9 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -1,19 +1,18 @@ -//! Minimal fixed-size thread pool for flat data-parallel kernels ("split a range, run a -//! closure on each piece"). No work-stealing, no per-dispatch allocation; owning the runtime -//! lets us pin per-worker scratch and drop rayon. +//! Minimal fixed-size thread pool for flat data-parallel kernels ("split a range, run a closure +//! on each piece"). No work-stealing, no per-dispatch allocation; owning the runtime lets us pin +//! per-worker scratch and drop rayon. //! //! - **Model.** `NUM_THREADS-1` background workers (ids `1..NUM_THREADS`); the dispatcher is -//! worker 0 and runs its share inline. Workers claim task ranges from a shared atomic -//! counter (guided self-scheduling) for dynamic load balance. -//! - **Lock-free dispatch.** Dispatch bumps a `generation` counter idle workers spin on -//! (back-to-back dispatches pay no syscall), parking only after `SPIN_LIMIT` idle spins. -//! Completion is a `working` countdown the dispatcher spins on. The per-worker `parked` flag -//! is SeqCst-ordered against `generation`, so for every dispatch at least one side sees the -//! other — a wakeup can't be lost — and unpark is skipped while a worker spins. -//! - **No nesting.** Dispatching from inside a task would deadlock the dispatch lock; an -//! `IN_TASK` guard panics instead (the outer level already saturates every core). +//! worker 0 and runs its share inline. Workers claim ranges from a shared atomic counter +//! (guided self-scheduling) for load balance. +//! - **Lock-free dispatch.** Dispatch bumps a `generation` counter idle workers spin on, parking +//! after `SPIN_LIMIT` spins; completion is a `working` countdown the dispatcher spins on. +//! `parked` is SeqCst-ordered against `generation`, so each dispatch one side sees the other +//! (no lost wakeup) and unpark is skipped while a worker spins. +//! - **No nesting.** A dispatch from within a task would deadlock the dispatch lock; an `IN_TASK` +//! guard panics instead. //! - **Panics.** A task panic is caught on its worker and re-raised on the dispatcher once the -//! dispatch quiesces (rayon's propagate-to-caller behavior); the pool stays usable. +//! dispatch quiesces; the pool stays usable. //! - **One dispatcher at a time**, serialized by the `dispatch` mutex. use std::any::Any; @@ -26,8 +25,8 @@ use std::thread::Thread; use system_info::NUM_THREADS; -/// Idle spins before a worker parks: long enough that back-to-back dispatches stay hot, short -/// enough that sequential gaps free the core for the active thread. +/// Idle spins before a worker parks: long enough to stay hot across back-to-back dispatches, +/// short enough to yield the core during sequential gaps. const SPIN_LIMIT: u32 = 1 << 12; /// Max tasks claimed in one guided-self-scheduling step: bounds load imbalance while keeping @@ -55,7 +54,7 @@ thread_local! { static IN_TASK: Cell = const { Cell::new(false) }; } -/// Calling worker's id in `0..NUM_THREADS` (`0` off-pool). The hook for per-worker scratch. +/// Calling worker's id in `0..NUM_THREADS` (`0` off-pool). #[must_use] pub fn current_worker_id() -> usize { WORKER_ID.with(Cell::get) @@ -107,11 +106,10 @@ unsafe impl Sync for Pool {} unsafe impl Send for Pool {} /// Idempotent warm-up: spawn workers and run one empty dispatch so the pool and the (macOS) -/// lazily-allocated mutex exist before timed work. Otherwise the pool inits on first use. +/// lazily-allocated mutex exist before timed work; otherwise the pool inits on first use. /// -/// Also fail-fast if the machine's core count differs from the build-time [`NUM_THREADS`] -/// (which sizes the whole pool): a mismatch silently over/under-subscribes every kernel, so -/// surface it as "rebuild" rather than a quiet perf cliff. +/// Also fail-fast if the machine's core count differs from the build-time [`NUM_THREADS`] (which +/// sizes the pool): a mismatch silently over/under-subscribes every kernel. pub fn init() { static INIT: Once = Once::new(); INIT.call_once(|| { @@ -167,10 +165,9 @@ fn worker_main(pool: &'static Pool, id: usize) { } } -/// Block until a new job is published, returning its generation. Spins up to [`SPIN_LIMIT`], -/// then parks. The park is delicate: publish `parked = true`, then re-check `generation`, both -/// SeqCst — the same total order the dispatcher's bump and `parked` load observe, so a wakeup -/// can never be lost. +/// Block until a new job is published, returning its generation. Spins up to [`SPIN_LIMIT`], then +/// parks: publish `parked = true`, re-check `generation`, both SeqCst — the same total order the +/// dispatcher's bump and `parked` load observe, so a wakeup can't be lost. fn wait_for_dispatch(pool: &Pool, id: usize, last_gen: usize) -> usize { let mut spins = 0u32; loop { @@ -293,7 +290,7 @@ pub fn for_each_index(n_tasks: usize, f: F) { } /// A base `*mut` shareable across workers. Sound only because callers partition the allocation -/// by task index (disjoint regions). Reuse this instead of redefining the pattern per crate. +/// by task index (disjoint regions). #[derive(Debug)] pub struct SendPtr(pub *mut T); // SAFETY: accesses are partitioned by task index (see callers). @@ -335,7 +332,8 @@ where } /// Parallel `data.iter_mut().enumerate().for_each(f)`, chunked by [`recommended_chunk_size`]. -/// Hands the closure each element's **global** index. `#[inline]` to recover hand-written codegen. +/// Hands the closure each element's **global** index. `#[inline]` folds the per-chunk adapter +/// into the monomorphized [`par_chunks_mut`]. #[inline] pub fn par_for_each_mut(data: &mut [T], f: F) where @@ -349,9 +347,8 @@ where }); } -/// Parallel `(0..n_tasks).map(f).collect::>()`: runs `f(i)` across the pool and gathers -/// the results in index order, writing each straight into the output (no `Option` slots, one -/// allocation). Folds away the common "fill a `Vec>` in parallel, then unwrap" dance. +/// Parallel `(0..n_tasks).map(f).collect::>()`: runs `f(i)` across the pool and writes each +/// result straight into the output in index order — one allocation, no `Option` slots. pub fn par_map_collect T + Sync>(n_tasks: usize, f: F) -> Vec { let mut out: Vec = Vec::with_capacity(n_tasks); let base = SendPtr(out.as_mut_ptr()); @@ -366,10 +363,18 @@ pub fn par_map_collect T + Sync>(n_tasks: usize, f: F) out } +/// Parallel `for (i, slot) in dst.iter_mut().enumerate() { *slot = build(i); }`: fill an existing +/// slice from an index closure. The in-place dual of [`par_map_collect`] (which allocates). +/// `#[inline]` folds the fill adapter into the monomorphized [`par_for_each_mut`]. Always +/// dispatches to the pool; guard the call yourself when small inputs need a sequential fast path. +#[inline] +pub fn par_fill T + Sync>(dst: &mut [T], build: F) { + par_for_each_mut(dst, |i, slot| *slot = build(i)); +} + /// Give each worker its own persistent `Option` slot while it drains `0..n_tasks`: /// `run(slot, start, end)` fires once per claimed batch with that worker's slot, so state /// accumulates across its batches. Returns the slots (rest `None`) for the caller to combine. -/// The sole home of the cross-worker slot `unsafe`. fn drain_into_slots(n_tasks: usize, run: impl Fn(&mut Option, usize, usize) + Sync) -> Vec> { let mut slots: Vec> = (0..NUM_THREADS).map(|_| None).collect(); let ptr = SendPtr(slots.as_mut_ptr()); @@ -384,8 +389,7 @@ fn drain_into_slots(n_tasks: usize, run: impl Fn(&mut Option, usize, /// Parallel map-reduce over `0..n_tasks` = `(0..n).map(map).reduce(identity, reduce)`. Each /// worker folds its claimed indices into one local partial; the partials combine on the -/// dispatcher. `reduce` must be associative with `identity()` a neutral element (rayon's -/// `reduce` contract). +/// dispatcher. `reduce` must be associative with `identity()` a neutral element. pub fn map_reduce(n_tasks: usize, identity: ID, map: M, reduce: R) -> T where T: Send, diff --git a/crates/backend/poly/src/utils.rs b/crates/backend/poly/src/utils.rs index 3f531a19c..9ab4ee03d 100644 --- a/crates/backend/poly/src/utils.rs +++ b/crates/backend/poly/src/utils.rs @@ -79,7 +79,7 @@ fn fold_fill OF + Sync>(len: usize, seq: bool, compute *r = compute(i); } } else { - parallel::par_for_each_mut(&mut res, |i, r| *r = compute(i)); + parallel::par_fill(&mut res, &compute); } res } diff --git a/crates/sub_protocols/src/logup.rs b/crates/sub_protocols/src/logup.rs index d4db50ce7..60cdce340 100644 --- a/crates/sub_protocols/src/logup.rs +++ b/crates/sub_protocols/src/logup.rs @@ -1,6 +1,7 @@ use crate::{ENDIANNESS_PIVOT_GKR, prove_gkr_quotient, verify_gkr_quotient}; use backend::*; use lean_vm::*; +use parallel::par_fill; use std::collections::BTreeMap; use tracing::instrument; use utils::ansi::Colorize; @@ -86,7 +87,7 @@ pub fn prove_generic_logup( // Memory section. assert_eq!(memory.len(), memory_acc.len()); fill_num_from(&mut numerators[offset..][..memory.len()], memory_acc, true); - fill_denoms(&mut denominators[offset / width..][..memory.len() / width], |p| { + par_fill(&mut denominators[offset / width..][..memory.len() / width], |p| { c_packed - finger_print_packed::( memory_domainsep_packed, @@ -103,7 +104,7 @@ pub fn prove_generic_logup( assert_eq!(1 << log_bytecode, bytecode_acc.len()); fill_num_from(&mut numerators[offset..][..bytecode_acc.len()], bytecode_acc, true); let bytecode_stride = N_INSTRUCTION_COLUMNS.next_power_of_two(); - fill_denoms( + par_fill( &mut denominators[offset / width..][..(1 << log_bytecode) / width], |p| { let mut data = [PFPacking::::ZERO; N_INSTRUCTION_COLUMNS + 1]; @@ -203,7 +204,7 @@ pub fn prove_generic_logup( _ => PFPacking::::ZERO, }; - fill_denoms(denom_slot, |p| { + par_fill(denom_slot, |p| { let mut data_buf = [PFPacking::::ZERO; MAX_BUS_WIDTH]; for k in 0..n_data { let col = data_cols[k]; @@ -525,16 +526,3 @@ fn compute_total_active_len( .map(|(table, log_n_rows)| offset_for_table(table, *log_n_rows)) .sum::() } - -#[inline] -fn fill_denoms(dst: &mut [EFPacking], build: Build) -where - Build: Fn(usize) -> EFPacking + Sync, -{ - par_fill(dst, build); -} - -#[inline] -fn par_fill T + Sync>(dst: &mut [T], build: Build) { - parallel::par_for_each_mut(dst, |i, slot| *slot = build(i)); -} diff --git a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs index 1bcb50e37..b2458fac0 100644 --- a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs +++ b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs @@ -361,7 +361,7 @@ pub(super) fn run_phase2_sumcheck>>( let fold_eq = |i: usize| eq_table[2 * i] + eq_table[2 * i + 1]; eq_table = if new_eq_len >= PARALLEL_THRESHOLD { let mut out: Vec = unsafe { uninitialized_vec(new_eq_len) }; - parallel::par_for_each_mut(&mut out, |i, slot| *slot = fold_eq(i)); + parallel::par_fill(&mut out, fold_eq); out } else { (0..new_eq_len).map(fold_eq).collect() From e992569739cb1afbbe97260d6b2b6a74b7061ba4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 14:24:41 +0200 Subject: [PATCH 53/65] par_for_each_mut2 --- crates/backend/parallel/src/lib.rs | 13 ++++++++ crates/lean_compiler/src/c_compile_final.rs | 6 ++-- crates/lean_prover/src/trace_gen.rs | 5 +-- .../sub_protocols/src/quotient_gkr/layers.rs | 32 +++++++++---------- crates/utils/src/multilinear.rs | 8 ++--- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index eb7ed9ff9..8fb1823b0 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -347,6 +347,19 @@ where }); } +/// [`par_for_each_mut`] over two equal-length slices at once: `f(i, &mut a[i], &mut b[i])` +#[inline] +pub fn par_for_each_mut2(a: &mut [A], b: &mut [B], f: F) +where + F: Fn(usize, &mut A, &mut B) + Sync, +{ + assert_eq!(a.len(), b.len(), "par_for_each_mut2: slices differ in length"); + let bp = SendPtr(b.as_mut_ptr()); + par_for_each_mut(a, |i, ai| { + f(i, ai, unsafe { &mut *bp.add(i) }); + }); +} + /// Parallel `(0..n_tasks).map(f).collect::>()`: runs `f(i)` across the pool and writes each /// result straight into the output in index order — one allocation, no `Option` slots. pub fn par_map_collect T + Sync>(n_tasks: usize, f: F) -> Vec { diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index 003607d70..87e4ebf23 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -132,10 +132,8 @@ pub fn compile_to_low_level_bytecode( validate_instruction(instruction)?; } - let mut instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]> = unsafe { uninitialized_vec(instructions.len()) }; - parallel::par_for_each_mut(&mut instructions_encoded, |i, out| { - *out = field_representation(&instructions[i]); - }); + let instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]> = + parallel::par_map_collect(instructions.len(), |i| field_representation(&instructions[i])); let mut instructions_multilinear = vec![]; for instr in &instructions_encoded { diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 86a736e25..07d7433e6 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -93,10 +93,7 @@ pub fn get_execution_trace( *trace_row[EXEC_COL_ADDR_C] = addr_c; }); - let mut memory_padded: Vec = unsafe { uninitialized_vec(memory.0.len()) }; - parallel::par_for_each_mut(&mut memory_padded, |i, slot| { - *slot = memory.0[i].unwrap_or(F::ZERO); - }); + let mut memory_padded: Vec = parallel::par_map_collect(memory.0.len(), |i| memory.0[i].unwrap_or(F::ZERO)); // Write [0000000000000000 | poseidon_compress(0000000000000000)] (to make lookups work on padding-rows). let padding_zero_vec_ptr = memory_padded.len(); diff --git a/crates/sub_protocols/src/quotient_gkr/layers.rs b/crates/sub_protocols/src/quotient_gkr/layers.rs index 4dfaf82e4..eb8a769e0 100644 --- a/crates/sub_protocols/src/quotient_gkr/layers.rs +++ b/crates/sub_protocols/src/quotient_gkr/layers.rs @@ -129,17 +129,18 @@ fn sum_quotients_2_by_2>>(nums: &[EF], dens: &[EF]) -> let mut new_nums: Vec = unsafe { uninitialized_vec(new_active) }; let mut new_dens: Vec = unsafe { uninitialized_vec(new_active) }; - { - let dp = parallel::SendPtr(new_dens.as_mut_ptr()); - parallel::par_for_each_mut(&mut new_nums[..full_pairs], |i, num| { + parallel::par_for_each_mut2( + &mut new_nums[..full_pairs], + &mut new_dens[..full_pairs], + |i, num, den| { let n0 = nums[2 * i]; let n1 = nums[2 * i + 1]; let d0 = dens[2 * i]; let d1 = dens[2 * i + 1]; *num = d1 * n0 + d0 * n1; - unsafe { *dp.add(i) = d0 * d1 }; - }); - } + *den = d0 * d1; + }, + ); // Boundary (at most one pair: a/b + 0/1 = a/b). if full_pairs < new_active { @@ -170,17 +171,14 @@ where let mut new_nums: Vec> = unsafe { uninitialized_vec(nums.len() >> 1) }; let mut new_dens: Vec> = unsafe { uninitialized_vec(nums.len() >> 1) }; - { - let dp = parallel::SendPtr(new_dens.as_mut_ptr()); - parallel::par_for_each_mut(&mut new_nums, |new_j, num_out| { - let i_hi = new_j >> bit; - let i_lo = new_j & lo_mask; - let i0 = (i_hi << (bit + 1)) | i_lo; - let i1 = i0 | stride; - *num_out = dens[i1] * nums[i0] + dens[i0] * nums[i1]; - unsafe { *dp.add(new_j) = dens[i0] * dens[i1] }; - }); - } + parallel::par_for_each_mut2(&mut new_nums, &mut new_dens, |new_j, num_out, den_out| { + let i_hi = new_j >> bit; + let i_lo = new_j & lo_mask; + let i0 = (i_hi << (bit + 1)) | i_lo; + let i1 = i0 | stride; + *num_out = dens[i1] * nums[i0] + dens[i0] * nums[i1]; + *den_out = dens[i0] * dens[i1]; + }); (new_nums, new_dens) } diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 0147d28a0..c7540bc7a 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -14,11 +14,9 @@ pub fn multilinears_linear_combination, P: Borro let n_vars = log2_strict_usize(pols[0].borrow().len()); assert!(pols.iter().all(|p| log2_strict_usize(p.borrow().len()) == n_vars)); let n = 1usize << n_vars; - let mut out: Vec = unsafe { uninitialized_vec(n) }; - parallel::par_for_each_mut(&mut out, |i, slot| { - *slot = dot_product(scalars.iter().copied(), pols.iter().map(|p| p.borrow()[i])); - }); - out + parallel::par_map_collect(n, |i| { + dot_product(scalars.iter().copied(), pols.iter().map(|p| p.borrow()[i])) + }) } pub fn multilinear_eval_constants_at_right(limit: usize, point: &[F]) -> F { From a5f578dd3779d382fb897d6386f8034485a46bbc Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 14:35:08 +0200 Subject: [PATCH 54/65] simplify --- crates/backend/poly/src/evals.rs | 6 +- crates/lean_prover/src/trace_gen.rs | 3 +- crates/lean_vm/src/execution/runner.rs | 84 +++++++++---------- crates/sub_protocols/src/air_sumcheck.rs | 13 ++- .../src/quotient_gkr/sumcheck_utils.rs | 4 +- 5 files changed, 48 insertions(+), 62 deletions(-) diff --git a/crates/backend/poly/src/evals.rs b/crates/backend/poly/src/evals.rs index b11f2ff81..45330d60e 100644 --- a/crates/backend/poly/src/evals.rs +++ b/crates/backend/poly/src/evals.rs @@ -87,11 +87,7 @@ pub fn scale_poly>(poly: &[F], factor: EF) -> Ve if poly.len() < PARALLEL_THRESHOLD { poly.iter().map(|&e| factor * e).collect() } else { - let mut out: Vec = unsafe { uninitialized_vec(poly.len()) }; - parallel::par_for_each_mut(&mut out, |i, o| { - *o = factor * poly[i]; - }); - out + parallel::par_map_collect(poly.len(), |i| factor * poly[i]) } } diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 07d7433e6..9b91604aa 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -195,8 +195,7 @@ fn pad_table( trace.log_n_rows = log2_ceil_usize(h + 1).max(min_log_n_rows); let n_rows = 1 << trace.log_n_rows; let padding_row = table.padding_row(zero_vec_ptr, null_poseidon_16_hash_ptr, ending_pc); - parallel::par_chunks_mut(&mut trace.columns, 1, |i, slot| { - let col = &mut slot[0]; + parallel::par_for_each_mut(&mut trace.columns, |i, col| { assert!(col.len() <= h); // potentially some columns have not been filled (in Poseidon -> we fill it later with SIMD + parallelism), but the first one should always be representative col.resize(n_rows, padding_row[i]); }); diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index 8a47e716d..d0262ec5f 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -451,56 +451,52 @@ fn handle_parallel_batch( // Release the `&mut` borrows so only the raw pointers alias the segments. drop(segment_slices); - let mut results: Vec> = (0..n_par).map(|_| None).collect(); - parallel::par_chunks_mut(&mut results, 1, |i, out| { + let results: Vec = parallel::par_map_collect(n_par, |i| { let (seg_ptr, seg_len) = &seg_info[i]; // SAFETY: distinct `i` reconstruct disjoint segments of `right`, valid for the dispatch. let seg_slice: &mut [Option] = unsafe { std::slice::from_raw_parts_mut(seg_ptr.0, *seg_len) }; - out[0] = Some((|| -> SegResult { - let seg_start = split_at + i * stride; - let mut seg_mem = SegmentMemory::new(shared, seg_slice, seg_start); - let fp_i = batch.batch_fp + (i + 1) * stride; - let mut seg_trace = Trace::new(); - let mut seg_pc = batch.batch_pc; - let mut seg_fp = fp_i; - let mut seg_ap = fp_i + batch.frame_size; - let mut seg_named_hints = named_hints.clone(); - for (name, delta) in &named_per_iter { - if let Some(cursor) = seg_named_hints.get_mut(name) { - cursor.index += i * delta; - } + let seg_start = split_at + i * stride; + let mut seg_mem = SegmentMemory::new(shared, seg_slice, seg_start); + let fp_i = batch.batch_fp + (i + 1) * stride; + let mut seg_trace = Trace::new(); + let mut seg_pc = batch.batch_pc; + let mut seg_fp = fp_i; + let mut seg_ap = fp_i + batch.frame_size; + let mut seg_named_hints = named_hints.clone(); + for (name, delta) in &named_per_iter { + if let Some(cursor) = seg_named_hints.get_mut(name) { + cursor.index += i * delta; } - let seg_start_indices: HashMap<_, _> = seg_named_hints - .iter() - .map(|(name, c)| (name.clone(), c.index)) - .collect(); - let mut hints = HintState { - diagnostics: None, - named_hints: &mut seg_named_hints, - }; - run_loop( - bytecode, - &mut seg_mem, - &mut seg_trace, - &mut seg_pc, - &mut seg_fp, - &mut seg_ap, - &mut hints, - Some(batch.batch_pc), - )?; - for (name, delta) in &named_per_iter { - let consumed = seg_named_hints[name].index - seg_start_indices[name]; - if consumed != *delta { - return Err(RunnerError::InvalidHintWitness(format!( - "hint '{name}' consumed {consumed} entries in a parallel iteration but {delta} in iteration 0; parallel iterations must consume hints uniformly" - ))); - } + } + let seg_start_indices: HashMap<_, _> = seg_named_hints + .iter() + .map(|(name, c)| (name.clone(), c.index)) + .collect(); + let mut hints = HintState { + diagnostics: None, + named_hints: &mut seg_named_hints, + }; + run_loop( + bytecode, + &mut seg_mem, + &mut seg_trace, + &mut seg_pc, + &mut seg_fp, + &mut seg_ap, + &mut hints, + Some(batch.batch_pc), + )?; + for (name, delta) in &named_per_iter { + let consumed = seg_named_hints[name].index - seg_start_indices[name]; + if consumed != *delta { + return Err(RunnerError::InvalidHintWitness(format!( + "hint '{name}' consumed {consumed} entries in a parallel iteration but {delta} in iteration 0; parallel iterations must consume hints uniformly" + ))); } - let deferred = seg_mem.into_deferred_writes(); - Ok((seg_trace, deferred)) - })()); + } + let deferred = seg_mem.into_deferred_writes(); + Ok((seg_trace, deferred)) }); - let results: Vec = results.into_iter().map(Option::unwrap).collect(); for (idx, result) in results.into_iter().enumerate() { let (seg_trace, deferred) = result.map_err(|e| RunnerError::ParallelSegmentFailed(idx + 1, Box::new(e)))?; diff --git a/crates/sub_protocols/src/air_sumcheck.rs b/crates/sub_protocols/src/air_sumcheck.rs index 8600de649..d8a8031db 100644 --- a/crates/sub_protocols/src/air_sumcheck.rs +++ b/crates/sub_protocols/src/air_sumcheck.rs @@ -89,8 +89,7 @@ where let _span = info_span!("chunk-bit-reversing columns").entered(); let chunk_size = 1usize << pivot; let shift = usize::BITS as usize - pivot; - let mut bit_reversed: Vec>> = (0..cols.len()).map(|_| Vec::new()).collect(); - parallel::par_chunks_mut(&mut bit_reversed, 1, |i, out_slot| { + let bit_reversed: Vec>> = parallel::par_map_collect(cols.len(), |i| { let src = cols[i]; let mut dst: Vec> = unsafe { uninitialized_vec(src.len()) }; let src_u = PFPacking::::unpack_slice(src); @@ -101,7 +100,7 @@ where *slot = src_chunk[p.reverse_bits() >> shift]; } } - out_slot[0] = dst; + dst }); MleGroup::Owned(MleGroupOwned::BasePacked(bit_reversed)) } @@ -659,15 +658,13 @@ pub fn prove_batched_air_sumcheck<'a, EF: ExtensionField>>( pub fn compute_shifted_columns(n_shift_columns: usize, columns: &[&[F]]) -> Vec> { // Convention: the first `n_shift_columns` columns are the ones that get shifted. - let mut out: Vec> = (0..n_shift_columns).map(|_| Vec::new()).collect(); - parallel::par_chunks_mut(&mut out, 1, |i, slot| { + parallel::par_map_collect(n_shift_columns, |i| { let column = columns[i]; let mut shifted = unsafe { uninitialized_vec(column.len()) }; shifted[..column.len() - 1].copy_from_slice(&column[1..]); shifted[column.len() - 1] = column[column.len() - 1]; - slot[0] = shifted; - }); - out + shifted + }) } pub fn natural_ordering_point_for_session(sumcheck_air_point: &[EF], log_n_rows: usize) -> Vec { diff --git a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs index b2458fac0..42732d61c 100644 --- a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs +++ b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs @@ -360,9 +360,7 @@ pub(super) fn run_phase2_sumcheck>>( if new_eq_len > 0 { let fold_eq = |i: usize| eq_table[2 * i] + eq_table[2 * i + 1]; eq_table = if new_eq_len >= PARALLEL_THRESHOLD { - let mut out: Vec = unsafe { uninitialized_vec(new_eq_len) }; - parallel::par_fill(&mut out, fold_eq); - out + parallel::par_map_collect(new_eq_len, fold_eq) } else { (0..new_eq_len).map(fold_eq).collect() }; From 9c290e92a18107af5c0a4d8b337a420cb317ed19 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 14:45:17 +0200 Subject: [PATCH 55/65] wip --- .../sub_protocols/src/quotient_gkr/sumcheck_utils.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs index 42732d61c..35f5a84b3 100644 --- a/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs +++ b/crates/sub_protocols/src/quotient_gkr/sumcheck_utils.rs @@ -379,18 +379,18 @@ fn fold_normal_with_padding>>(m: &[EF], r: EF, pad_val let active = m.len(); let new_active = active.div_ceil(2); assert!(new_active != 0); - let mut out = unsafe { uninitialized_vec(new_active) }; + let mut out: Vec = unsafe { uninitialized_vec(new_active) }; - let compute = |(i, slot): (usize, &mut EF)| { + let compute = |i: usize, slot: &mut EF| { let a = m[2 * i]; let b = if 2 * i + 1 < active { m[2 * i + 1] } else { pad_value }; *slot = a + (b - a) * r; }; if new_active < PARALLEL_THRESHOLD { - out.iter_mut().enumerate().for_each(compute); + out.iter_mut().enumerate().for_each(|(i, slot)| compute(i, slot)); } else { - parallel::par_for_each_mut(&mut out, |i, slot| compute((i, slot))); + parallel::par_for_each_mut(&mut out, compute); } out } @@ -479,8 +479,8 @@ where |c| { let n_c = &nums[c * in_packed..][..in_packed]; let d_c = &dens[c * in_packed..][..in_packed]; - let nn_c = unsafe { std::slice::from_raw_parts_mut(nn.add(c * out_packed), out_packed) }; - let nd_c = unsafe { std::slice::from_raw_parts_mut(nd.add(c * out_packed), out_packed) }; + let nn_c = unsafe { nn.slice(c * out_packed, out_packed) }; + let nd_c = unsafe { nd.slice(c * out_packed, out_packed) }; let eq_o: EF = eq_outer.get(c).copied().unwrap_or(EF::ONE); let mut local = RoundCoeffs::>::zero(); for i in 0..in_eighth { From 2b471758bc0f6d9cce147fc8f1fdd25ddd73b9f5 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 14:57:10 +0200 Subject: [PATCH 56/65] wip --- crates/backend/sumcheck/src/product_computation.rs | 6 ------ crates/backend/sumcheck/src/sc_computation.rs | 5 ----- crates/backend/zk-alloc/src/lib.rs | 4 ---- crates/lean_vm/src/execution/memory.rs | 3 --- crates/rec_aggregation/Cargo.toml | 1 - 5 files changed, 19 deletions(-) diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index f0e46e3f4..375d7ebaa 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -145,9 +145,6 @@ pub fn compute_product_sumcheck_polynomial< (a0 + b0, a2 + b2) }) } else { - // Per-worker in-place accumulation: each worker folds the contiguous range it - // claims straight into its own `(c0, c2)` accumulator (no per-chunk tuple to build - // and reduce, worker-slot lookup amortized once per batch by `for_each_chunk`). let half = n / 2; parallel::map_reduce_with_state( half, @@ -217,9 +214,6 @@ pub fn fold_and_compute_product_sumcheck_polynomial< (a0 + b0, a2 + b2) }) } else { - // Fused single pass with per-worker in-place accumulation: fold both polynomials - // (writing the disjoint `i` / `quarter + i` output slots) and accumulate the - // per-index quadratic straight into the worker's `(c0, c2)` — no per-chunk tuple. let quarter = n / 4; let p0f = parallel::SendPtr(pol_0_folded.as_mut_ptr()); let p1f = parallel::SendPtr(pol_1_folded.as_mut_ptr()); diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index 56d63d0b3..c7b03c603 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -549,9 +549,6 @@ where let eq_lo = &split_eq.eq_lo; let eq_hi = &split_eq.eq_hi_packed; - // Per-worker scratch reused across every `b_lo` task: `rows` ([lo, diff, hi] - // triples), `point` (handed to `eval_fn`), and `block_acc` (per-`b_lo` partial - // sum, scaled by `eq_lo` before folding into the worker accumulator `acc`). let n_mult = multilinears.len(); let sums: Vec> = parallel::map_reduce_with_state( n_lo, @@ -639,8 +636,6 @@ where let eq_lo = &split_eq.eq_lo; let eq_hi = &split_eq.eq_hi_packed; - // Per-worker scratch reused across every `b_lo` task (see `sumcheck_compute_with_split_eq`): - // `rows_f` triples, `point` for `eval_fn`, and the per-`b_lo` `block_acc`. let n_mult = multilinears.len(); let sums: Vec> = parallel::map_reduce_with_state( n_lo, diff --git a/crates/backend/zk-alloc/src/lib.rs b/crates/backend/zk-alloc/src/lib.rs index f0433d3a4..a3ff9d1df 100644 --- a/crates/backend/zk-alloc/src/lib.rs +++ b/crates/backend/zk-alloc/src/lib.rs @@ -100,10 +100,6 @@ pub fn begin_phase() { /// Deactivates the arena. New allocations go to the system allocator; existing arena /// pointers stay valid until the next `begin_phase()` resets the slabs. -/// -/// Unlike the rayon-based build (which needed `flush_rayon` to drain crossbeam's -/// arena-allocated injector blocks), the in-house `parallel` pool allocates its state -/// once at startup and nothing per-dispatch, so no flush is required here. pub fn end_phase() { ARENA_ACTIVE.store(false, Ordering::Release); } diff --git a/crates/lean_vm/src/execution/memory.rs b/crates/lean_vm/src/execution/memory.rs index 38b5a41e2..461390507 100644 --- a/crates/lean_vm/src/execution/memory.rs +++ b/crates/lean_vm/src/execution/memory.rs @@ -11,9 +11,6 @@ pub trait MemoryAccess { (0..len).map(|i| self.get(start + i)).collect() } - /// In-place version of [`get_slice`] that writes into a caller-provided buffer, - /// avoiding a per-call heap allocation on the hot interpreter path (Poseidon / - /// extension-op slice reads run hundreds of thousands of times per proof). fn get_slice_into(&self, start: usize, dest: &mut [F]) -> Result<(), RunnerError> { for (i, d) in dest.iter_mut().enumerate() { *d = self.get(start + i)?; diff --git a/crates/rec_aggregation/Cargo.toml b/crates/rec_aggregation/Cargo.toml index 7cadde763..cfa996cc7 100644 --- a/crates/rec_aggregation/Cargo.toml +++ b/crates/rec_aggregation/Cargo.toml @@ -8,7 +8,6 @@ workspace = true [features] prox-gaps-conjecture = ["lean_prover/prox-gaps-conjecture"] -# Skip the zk-alloc bump-arena phase ceremony (use the plain system allocator). standard-alloc = [] [dependencies] From 260f818003c29d7d120a97c702e7091ac62491e3 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 15:04:38 +0200 Subject: [PATCH 57/65] wip --- crates/backend/poly/src/utils.rs | 5 ----- .../sumcheck/src/product_computation.rs | 18 +++++------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/crates/backend/poly/src/utils.rs b/crates/backend/poly/src/utils.rs index 9ab4ee03d..1f42ee790 100644 --- a/crates/backend/poly/src/utils.rs +++ b/crates/backend/poly/src/utils.rs @@ -67,10 +67,6 @@ pub const fn must_unpack_multilinears(n_vars: usize) -> bool { n_vars <= 1 + packing_log_width::() } -/// Fill `len` output slots with `compute(i)`, parallelizing via the pool when the work is -/// large enough. `seq` forces the sequential path: the batched wrappers below dispatch one -/// pool task per poly, so their inner fold must not nest a parallel dispatch (which would -/// panic in [`parallel`]). #[inline] fn fold_fill OF + Sync>(len: usize, seq: bool, compute: C) -> Vec { let mut res = unsafe { uninitialized_vec(len) }; @@ -176,7 +172,6 @@ pub fn batch_fold_multilinears_at_bit< bit: usize, mul_if_of: F, ) -> Vec> { - // See `batch_fold_multilinears`: one task per poly, inner fold forced sequential. let total_size: usize = polys.iter().map(|p| p.len()).sum(); if total_size < PARALLEL_THRESHOLD { polys diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index 375d7ebaa..52c2c5acf 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -146,15 +146,10 @@ pub fn compute_product_sumcheck_polynomial< }) } else { let half = n / 2; - parallel::map_reduce_with_state( + parallel::map_reduce( half, - || (), || (EFPacking::ZERO, EFPacking::ZERO), - |(), acc, i| { - let (b0, b2) = sumcheck_quadratic(((&pol_0[i], &pol_0[half + i]), (&pol_1[i], &pol_1[half + i]))); - acc.0 += b0; - acc.1 += b2; - }, + |i| sumcheck_quadratic(((&pol_0[i], &pol_0[half + i]), (&pol_1[i], &pol_1[half + i]))), |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), ) }; @@ -217,11 +212,10 @@ pub fn fold_and_compute_product_sumcheck_polynomial< let quarter = n / 4; let p0f = parallel::SendPtr(pol_0_folded.as_mut_ptr()); let p1f = parallel::SendPtr(pol_1_folded.as_mut_ptr()); - parallel::map_reduce_with_state( + parallel::map_reduce( quarter, - || (), || (EFPacking::ZERO, EFPacking::ZERO), - |(), acc, i| { + |i| { let diff_0 = pol_0[2 * quarter + i] - pol_0[i]; let diff_1 = pol_0[3 * quarter + i] - pol_0[quarter + i]; let x_0 = prev_folding_factor_packed * diff_0 + pol_0[i]; @@ -240,9 +234,7 @@ pub fn fold_and_compute_product_sumcheck_polynomial< *p1f.add(quarter + i) = y_1; } - let (b0, b2) = sumcheck_quadratic(((&x_0, &x_1), (&y_0, &y_1))); - acc.0 += b0; - acc.1 += b2; + sumcheck_quadratic(((&x_0, &x_1), (&y_0, &y_1))) }, |(a0, a2), (b0, b2)| (a0 + b0, a2 + b2), ) From 0adbef22a9ec592dbea592de579601c1ca5530d7 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 15:13:31 +0200 Subject: [PATCH 58/65] wip --- crates/backend/poly/src/evals.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/backend/poly/src/evals.rs b/crates/backend/poly/src/evals.rs index 45330d60e..218117ce4 100644 --- a/crates/backend/poly/src/evals.rs +++ b/crates/backend/poly/src/evals.rs @@ -297,12 +297,6 @@ where // the overhead of threading. let work_size: usize = (1 << 15) / std::mem::size_of::(); if evals.len() > work_size && PARALLEL { - // Flat fan-out: peel the `n_split` leading variables into `2^n_split` - // independent subproblems, evaluate each over the remaining coordinates - // sequentially across the pool, then interpolate the partial results over - // the leading coordinates. Equivalent to the recursive `join` split, but - // flat so the in-house pool can parallelize it (nested dispatches fall - // back to sequential, so a recursive split would lose all parallelism). let log_work = log2_ceil_usize(work_size.max(2)); let n_split = point.len().saturating_sub(log_work).max(1); let (lead, sub_point) = point.split_at(n_split); @@ -342,10 +336,6 @@ where } } -/// Multilinear interpolation of `values` (the `2^point.len()` hypercube evaluations of a -/// function, indexed lexicographically) at `point`, using only `Res` arithmetic and the -/// `mul_res_point` scaling. Used to recombine the partial results of the flat parallel -/// fan-out in [`eval_multilinear_generic`]. fn interpolate_res(values: &[Res], point: &[Point], mul_res_point: &MRP) -> Res where Point: Field, From 8d388ba2600e36c8efd7964603e159209d139a95 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 15:15:42 +0200 Subject: [PATCH 59/65] wip --- crates/backend/fiat-shamir/src/prover.rs | 4 ---- crates/backend/poly/src/eq_mle.rs | 10 ---------- 2 files changed, 14 deletions(-) diff --git a/crates/backend/fiat-shamir/src/prover.rs b/crates/backend/fiat-shamir/src/prover.rs index 79d3859bb..7989f3a5c 100644 --- a/crates/backend/fiat-shamir/src/prover.rs +++ b/crates/backend/fiat-shamir/src/prover.rs @@ -132,10 +132,6 @@ where // each batch tests lanes witnesses simultaneously let num_batches = PF::::ORDER_U64.div_ceil(lanes as u64); - // Parallel short-circuiting search (replaces rayon `find_any`): spawn one - // searcher per worker, each claiming batches from a shared counter and bailing - // as soon as any worker finds a witness. Bounds the work to ~expected + a few - // extra batches instead of enumerating all `num_batches` (which can be ~2^31). let next_batch = AtomicU64::new(0); let found = AtomicBool::new(false); parallel::for_each_index(parallel::num_threads(), |_| { diff --git a/crates/backend/poly/src/eq_mle.rs b/crates/backend/poly/src/eq_mle.rs index ca54a2e4d..3c7b9d34c 100644 --- a/crates/backend/poly/src/eq_mle.rs +++ b/crates/backend/poly/src/eq_mle.rs @@ -19,10 +19,6 @@ fn parallel_split() -> (usize, usize) { (log_chunks, 1 << log_chunks) } -/// Parallel equivalent of -/// `out.par_chunks_exact_mut(chunk).zip(buf).enumerate().for_each(|(i, (c, _))| g(i, c, &buf[i]))`, -/// dispatched through the in-house [`parallel`] pool. `chunk` must divide `out.len()` -/// exactly into `buf.len()` chunks (the eq_mle fan-out always does). #[inline] fn par_chunks_zip(out: &mut [T], chunk: usize, buf: &[A], g: G) where @@ -34,12 +30,6 @@ where parallel::par_chunks_mut(out, chunk, |i, c| g(c, &buf[i])); } -/// Shared parallel tail of the `compute_eval_eq*` family. With `eval` split into -/// `log_chunks` leading variables (handled one-per-chunk), `log_packing_width` trailing -/// variables already folded into `seed = buffer[0]`, and the middle variables left for -/// `kernel`, this builds the per-chunk equality buffer and runs -/// `kernel(middle, out_chunk, buffer_val)` over `out` in parallel. `kernel` fires once -/// per chunk (not per element), so threading it through a closure costs nothing. #[inline] fn par_eval_eq( eval: &[In], From 2f695245d8a3c561493b489fc51a2888af8797ec Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 16:36:56 +0200 Subject: [PATCH 60/65] parallel: chunk par_map_collect over recommended_chunk_size --- crates/backend/parallel/src/lib.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 8fb1823b0..74cd91097 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -365,11 +365,17 @@ where pub fn par_map_collect T + Sync>(n_tasks: usize, f: F) -> Vec { let mut out: Vec = Vec::with_capacity(n_tasks); let base = SendPtr(out.as_mut_ptr()); - for_each_index(n_tasks, |i| { - // SAFETY: distinct `i` write disjoint, in-bounds slots (each exactly once) and the - // dispatch blocks until all writes finish. A panic in `f` leaks the slots written so - // far, which is fine: a pool task panic is fatal (see the module's "Panics" note). - unsafe { base.add(i).write(f(i)) }; + let chunk = recommended_chunk_size(n_tasks); + for_each_chunk(n_tasks.div_ceil(chunk), |cstart, cend| { + let start = cstart * chunk; + let end = (cend * chunk).min(n_tasks); + for i in start..end { + // SAFETY: disjoint chunk-index ranges give disjoint, in-bounds element ranges, so + // each slot in `0..n_tasks` is written exactly once; the dispatch blocks until all + // writes finish. A panic in `f` leaks the slots written so far, which is fine: a pool + // task panic is fatal (see the module's "Panics" note). + unsafe { base.add(i).write(f(i)) }; + } }); // SAFETY: every slot in `0..n_tasks` was initialized exactly once above. unsafe { out.set_len(n_tasks) }; From 3b4d53168924ffd95757cdb21118bbf03681af17 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 18:16:59 +0200 Subject: [PATCH 61/65] air_sumcheck: revert a5f578dd's par_map_collect swap (thin-LTO inlining cliff, ~8%) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a5f578dd ("simplify") switched the column bit-reversal and compute_shifted_columns from `par_chunks_mut(.., 1, ..)` to `par_map_collect`. This is runtime-equivalent, but it grew the air_sumcheck.rs thin-LTO partition — which also holds the hot `compute_raw_poly` AIR-sumcheck loop — just past the inliner budget, de-inlining the cross-crate Poseidon `eval_2_full_rounds_16` SIMD kernel (its self-time jumps 5.3% -> 8.7% in perf, the 16-wide state spills to memory). Bisected to this exact file: reverting only air_sumcheck.rs recovers fully (xmss-1550 2.30s -> 2.11s = the 83b9c4d9 baseline; fancy-aggregation 16.78s -> 15.86s), while reverting any other a5f578dd file has no effect. Fat LTO also recovers it, confirming an inlining cliff rather than a logic regression. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/sub_protocols/src/air_sumcheck.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/sub_protocols/src/air_sumcheck.rs b/crates/sub_protocols/src/air_sumcheck.rs index d8a8031db..8600de649 100644 --- a/crates/sub_protocols/src/air_sumcheck.rs +++ b/crates/sub_protocols/src/air_sumcheck.rs @@ -89,7 +89,8 @@ where let _span = info_span!("chunk-bit-reversing columns").entered(); let chunk_size = 1usize << pivot; let shift = usize::BITS as usize - pivot; - let bit_reversed: Vec>> = parallel::par_map_collect(cols.len(), |i| { + let mut bit_reversed: Vec>> = (0..cols.len()).map(|_| Vec::new()).collect(); + parallel::par_chunks_mut(&mut bit_reversed, 1, |i, out_slot| { let src = cols[i]; let mut dst: Vec> = unsafe { uninitialized_vec(src.len()) }; let src_u = PFPacking::::unpack_slice(src); @@ -100,7 +101,7 @@ where *slot = src_chunk[p.reverse_bits() >> shift]; } } - dst + out_slot[0] = dst; }); MleGroup::Owned(MleGroupOwned::BasePacked(bit_reversed)) } @@ -658,13 +659,15 @@ pub fn prove_batched_air_sumcheck<'a, EF: ExtensionField>>( pub fn compute_shifted_columns(n_shift_columns: usize, columns: &[&[F]]) -> Vec> { // Convention: the first `n_shift_columns` columns are the ones that get shifted. - parallel::par_map_collect(n_shift_columns, |i| { + let mut out: Vec> = (0..n_shift_columns).map(|_| Vec::new()).collect(); + parallel::par_chunks_mut(&mut out, 1, |i, slot| { let column = columns[i]; let mut shifted = unsafe { uninitialized_vec(column.len()) }; shifted[..column.len() - 1].copy_from_slice(&column[1..]); shifted[column.len() - 1] = column[column.len() - 1]; - shifted - }) + slot[0] = shifted; + }); + out } pub fn natural_ordering_point_for_session(sumcheck_air_point: &[EF], log_n_rows: usize) -> Vec { From a94fbfda53c522be91e35f60b7f8c11757cfd66e Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 4 Jun 2026 22:00:29 +0200 Subject: [PATCH 62/65] Revert "parallel: chunk par_map_collect over recommended_chunk_size" This reverts commit 2f695245d8a3c561493b489fc51a2888af8797ec. --- crates/backend/parallel/src/lib.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 74cd91097..8fb1823b0 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -365,17 +365,11 @@ where pub fn par_map_collect T + Sync>(n_tasks: usize, f: F) -> Vec { let mut out: Vec = Vec::with_capacity(n_tasks); let base = SendPtr(out.as_mut_ptr()); - let chunk = recommended_chunk_size(n_tasks); - for_each_chunk(n_tasks.div_ceil(chunk), |cstart, cend| { - let start = cstart * chunk; - let end = (cend * chunk).min(n_tasks); - for i in start..end { - // SAFETY: disjoint chunk-index ranges give disjoint, in-bounds element ranges, so - // each slot in `0..n_tasks` is written exactly once; the dispatch blocks until all - // writes finish. A panic in `f` leaks the slots written so far, which is fine: a pool - // task panic is fatal (see the module's "Panics" note). - unsafe { base.add(i).write(f(i)) }; - } + for_each_index(n_tasks, |i| { + // SAFETY: distinct `i` write disjoint, in-bounds slots (each exactly once) and the + // dispatch blocks until all writes finish. A panic in `f` leaks the slots written so + // far, which is fine: a pool task panic is fatal (see the module's "Panics" note). + unsafe { base.add(i).write(f(i)) }; }); // SAFETY: every slot in `0..n_tasks` was initialized exactly once above. unsafe { out.set_len(n_tasks) }; From 6dca18b6c4730bbe01b4cf4f363a2faf130abe7d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 5 Jun 2026 08:56:45 +0200 Subject: [PATCH 63/65] faster combine_statement --- crates/whir/src/open.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/whir/src/open.rs b/crates/whir/src/open.rs index 6fa6fbed7..cb773528e 100644 --- a/crates/whir/src/open.rs +++ b/crates/whir/src/open.rs @@ -593,16 +593,26 @@ where for (e, &scalar) in smt.values.iter().zip(&next_gamma_powers) { combined_sum += e.value * scalar; } - for (out_buff, &(origin_index, _)) in chunks_mut.iter_mut().zip(&indexed_smt_values) { - let out = &mut out_buff[..1 << shift]; - let scalar = next_gamma_powers[origin_index]; - parallel::par_for_each_mut(out, |i, out_elem| { - *out_elem += inner_poly[i] * scalar; - }); - } + let n = 1usize << shift; + let mask = n - 1; + let ptrs: Vec<(parallel::SendPtr>, EF)> = chunks_mut + .iter_mut() + .zip(&indexed_smt_values) + .map(|(out_buff, &(origin_index, _))| { + ( + parallel::SendPtr(out_buff.as_mut_ptr()), + next_gamma_powers[origin_index], + ) + }) + .collect(); + let inner = inner_poly.as_slice(); + parallel::for_each_index(ptrs.len() << shift, |flat| { + let (ptr, scalar) = &ptrs[flat >> shift]; + let i = flat & mask; + unsafe { *ptr.add(i) += inner[i] * *scalar }; + }); gamma_pow = *next_gamma_powers.last().unwrap() * gamma; } } - (combined_weights, combined_sum) } From 4e681c4694108279354efdbaf74ce5b6172839f5 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 5 Jun 2026 12:19:04 +0200 Subject: [PATCH 64/65] wip --- crates/backend/sumcheck/src/product_computation.rs | 2 -- crates/backend/sumcheck/src/sc_computation.rs | 8 -------- crates/lean_prover/src/prove_execution.rs | 1 - crates/lean_vm/src/execution/runner.rs | 5 ----- crates/lean_vm/src/tables/poseidon/mod.rs | 2 -- 5 files changed, 18 deletions(-) diff --git a/crates/backend/sumcheck/src/product_computation.rs b/crates/backend/sumcheck/src/product_computation.rs index 52c2c5acf..343141c4f 100644 --- a/crates/backend/sumcheck/src/product_computation.rs +++ b/crates/backend/sumcheck/src/product_computation.rs @@ -225,8 +225,6 @@ pub fn fold_and_compute_product_sumcheck_polynomial< let y_1 = prev_folding_factor_packed * (pol_1[3 * quarter + i] - pol_1[quarter + i]) + pol_1[quarter + i]; - // SAFETY: distinct `i` write disjoint slots `i` and `quarter + i` in - // `[0, n/2)`; the dispatcher keeps both buffers borrowed for the call. unsafe { *p0f.add(i) = x_0; *p0f.add(quarter + i) = x_1; diff --git a/crates/backend/sumcheck/src/sc_computation.rs b/crates/backend/sumcheck/src/sc_computation.rs index c7b03c603..344a27015 100644 --- a/crates/backend/sumcheck/src/sc_computation.rs +++ b/crates/backend/sumcheck/src/sc_computation.rs @@ -399,10 +399,6 @@ where + MulAssign, SC: SumcheckComputation, { - // Per-worker scratch: `rows` (the [lo, diff, hi] triples) and `point` (the - // evaluation point handed to `eval_fn`) are reused across every task a worker - // owns, so the hot loop allocates nothing. `acc` (length `degree`) is the - // per-worker partial sum. let n_mult = multilinears.len(); let sums = parallel::map_reduce_with_state( fold_size, @@ -474,10 +470,6 @@ where .map(|_| FT::zero_vec(prev_folded_size)) .collect(); - // Per-worker scratch: `rows_f` (the [lo, diff, hi] triples) and `point` (the - // evaluation point handed to `eval_fn`) are reused across every task a worker - // owns, so the hot loop allocates nothing. `acc` (length `degree`) is the - // per-worker partial sum. let n_mult = multilinears.len(); let sums = parallel::map_reduce_with_state( compute_fold_size, diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index a9550baf0..e9f90dea2 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -163,7 +163,6 @@ pub fn prove_execution( }) .collect(); let _span = info_span!("Computing shifted columns for AIR sumcheck").entered(); - // Only a few tables; run them serially and let `compute_shifted_columns` use the full pool. let shifted_rows: Vec>> = ALL_TABLES .iter() .zip(&column_refs) diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index 3f60efdc8..6c8a92be9 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -440,19 +440,14 @@ fn handle_parallel_batch( type SegResult = Result<(Trace, Vec<(usize, F)>), RunnerError>; - // Raw base pointer + length per disjoint segment, so the pool can run each segment - // on its own slice without moving `&mut` references through the `Fn` task closure. - // SAFETY: segments are non-overlapping `chunks_mut` of `right`; task `i` touches only `i`. let seg_info: Vec<(parallel::SendPtr>, usize)> = segment_slices .iter_mut() .map(|s| (parallel::SendPtr(s.as_mut_ptr()), s.len())) .collect(); - // Release the `&mut` borrows so only the raw pointers alias the segments. drop(segment_slices); let results: Vec = parallel::par_map_collect(n_par, |i| { let (seg_ptr, seg_len) = &seg_info[i]; - // SAFETY: distinct `i` reconstruct disjoint segments of `right`, valid for the dispatch. let seg_slice: &mut [Option] = unsafe { std::slice::from_raw_parts_mut(seg_ptr.0, *seg_len) }; let seg_start = split_at + i * stride; let mut seg_mem = SegmentMemory::new(shared, seg_slice, seg_start); diff --git a/crates/lean_vm/src/tables/poseidon/mod.rs b/crates/lean_vm/src/tables/poseidon/mod.rs index f2dd173af..e36dfc3d5 100644 --- a/crates/lean_vm/src/tables/poseidon/mod.rs +++ b/crates/lean_vm/src/tables/poseidon/mod.rs @@ -240,8 +240,6 @@ impl TableT for Poseidon16Precompile { } else { arg_a_usize + HALF_DIGEST_LEN }; - // Fill the Poseidon input array directly from memory — no per-call Vec allocation - // (this runs once per Poseidon instruction, the dominant small-alloc source). let mut input = [F::ZERO; DIGEST_LEN * 2]; ctx.memory .get_slice_into(left_first_addr, &mut input[..HALF_DIGEST_LEN])?; From 70d28827bf747b807227d82dac1e6b7ffd4c8b76 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 5 Jun 2026 12:57:32 +0200 Subject: [PATCH 65/65] update benchmarks --- README.md | 21 ++++++++++----------- misc/images/fancy-aggregation.png | Bin 345118 -> 347074 bytes 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b945bd705..f8306ffb8 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ cargo run --release -- xmss --n-signatures 1550 --log-inv-rate 1 | WHIR rate | Proven Regime | Proximity Gaps Conjecture | | --------- | --------------------- | ------------------------- | -| 1/2 | 1319 XMSS/s - 338 KiB | 1345 XMSS/s - 176 KiB | -| 1/4 | 961 XMSS/s - 228 KiB | 969 XMSS/s - 126 KiB | +| 1/2 | 1453 XMSS/s - 344 KiB | 1500 XMSS/s - 178 KiB | +| 1/4 | 1058 XMSS/s - 229 KiB | 1065 XMSS/s - 127 KiB | (Proving throughput - proof size) @@ -54,15 +54,14 @@ cargo run --release -- recursion --n 2 --log-inv-rate 2 | n | WHIR rate | Proven Regime | Proximity Gaps Conjecture | | --- | --------- | --------------------------- | --------------------------- | -| 1 | 1/2 | 0.39s = 1 x 0.39s - 278 KiB | 0.24s = 1 x 0.24s - 147 KiB | -| 1 | 1/4 | 0.32s = 1 x 0.32s - 188 KiB | 0.27s = 1 x 0.27s - 100 KiB | -| 2 | 1/2 | 0.7s = 2 x 0.35s - 293 KiB | 0.43s = 2 x 0.21s - 157 KiB | -| 2 | 1/4 | 0.56s = 2 x 0.28s - 194 KiB | 0.43s = 2 x 0.22s - 102 KiB | -| 3 | 1/2 | 0.85s = 3 x 0.28s - 312 KiB | 0.63s = 3 x 0.21s - 150 KiB | -| 3 | 1/4 | 0.94s = 3 x 0.31s - 203 KiB | 0.73s = 3 x 0.24s - 108 KiB | -| 4 | 1/2 | 1.27s = 4 x 0.32s - 308 KiB | 0.78s = 4 x 0.2s - 166 KiB | -| 4 | 1/4 | 1.02s = 4 x 0.26s - 206 KiB | 0.79s = 4 x 0.2s - 108 KiB | - +| 1 | 1/2 | 0.22s = 1 x 0.22s - 285 KiB | 0.15s = 1 x 0.15s - 143 KiB | +| 1 | 1/4 | 0.24s = 1 x 0.24s - 189 KiB | 0.18s = 1 x 0.18s - 98 KiB | +| 2 | 1/2 | 0.52s = 2 x 0.26s - 282 KiB | 0.33s = 2 x 0.16s - 159 KiB | +| 2 | 1/4 | 0.45s = 2 x 0.23s - 198 KiB | 0.33s = 2 x 0.16s - 102 KiB | +| 3 | 1/2 | 0.7s = 3 x 0.23s - 317 KiB | 0.47s = 3 x 0.16s - 151 KiB | +| 3 | 1/4 | 0.67s = 3 x 0.22s - 191 KiB | 0.43s = 3 x 0.14s - 111 KiB | +| 4 | 1/2 | 1.02s = 4 x 0.26s - 309 KiB | 0.61s = 4 x 0.15s - 168 KiB | +| 4 | 1/4 | 0.85s = 4 x 0.21s - 208 KiB | 0.62s = 4 x 0.15s - 109 KiB | (time for n->1 recursive aggregation - proof size) diff --git a/misc/images/fancy-aggregation.png b/misc/images/fancy-aggregation.png index 2b729a0ff1cf63256a098b97bf59ad1d2808de15..b77ceb8c29c9210968bcfaa6d3c9fb6ec99918e1 100644 GIT binary patch literal 347074 zcmZU)1z1$w_XbLLNq0&~4BaqvOLuoDokQr*ARW>vT}mn;($d|aG$J5M3Ihx?chK+q ze*b&#JkObP=A5(7-e<41SG?<;S2|iM_&C%!NJvQdYO0EQNJ!|2V`L~62I5TaYPvQO z5}uB$f`X2kf&!zCkEfHXnZ?p+OcVW4@*;CRCN!)lX4Xr#I8|&$)+ln>;75wA zkCEkH@iSGg%qpQT^D>(l`H>BNCKxH@II87JunXi#Kqqsww4k7Q;wg`0d6juGc|BPO zhxzUo`%V>xoZul{YIPgrB|Jk(WgpG{9D~-##$MH^I*t5jD-7c@jK!KU+o!aQ1&Mp$ z=jM1m4U+B$W4cT;_%XaeNrNLD7fH61NsD3(J&1?_8Of7e)$s<2s3Ns3Clg?}gHQBh zq$HBan9I8oN0iIE6UUmq^;OC@c_<}PvqUjq98I>2;=CS3j&0%1fJ=CbB1e|&F=3OZ z)dn8ZU;^WZXb~^1PQBtnSfZh7uQHsHI` zTU*46n`AhIcX1`=tlUmqrvTMTr>8=15<%kNB_~+p;uq9-lvmQM;*RdaJ3(b5FQr>V zGfcB%9}VsGic4agO5Iad##<$IjEl?^;RCyLh!s;1HpvVwG6(5=O`TL*OcLx>*~81`hFe<{yO>@~)bMf1VB0+z3`gG?RE-m6On&`+Fq&|+oZQKq zCDz>`-o%kRi)dUiJryZa#wd#kH<_+^GoPIynNBj7bzg~Ko9o8(>`kqOZ3;S64sOeB z(xBR}M!EE8lwAdHn3)tEqsDG{lvz!_Ab$9F#SL;(ZBzRvA<^IwdP1UaF&sg@al7Bp zy$9@Z&$o9B4tGD#K@UKg#ULdSAh+2!!k0*jNW55R+$CYcbQ4IcvR^mmXHZWJr2E1M z9T-XC!gh%SrI=kCRZtqdONgKZX;>pFL+E%{@v_K!z$ClO0#KB`C7s-mPO%;C&5Ntje$Qop{W8{BL zso6S!?`{2+CMC=mY+~sp@=Kg{m?ET~L>_aSXV=s=SJvVa==Kx z%(4g{3x8EMc-;Y$06wvvS2>@gJEW2ZUe_Jt+S16Z5t}E|4NcirHR75GE+Lti;4a0z zw;j#|IeeC;x9}cl`Al$|f$Fc^kPFZ@%2n&|ee2tHp<-y=?Gn&^?zlIVI+gZHu;tv8 z7BWoK1R@TfH_1w9NsvSugQpx_TuC^c_C)Y~>GsliiP~G`Xk)WA46rc4MZ%QcV&7h& zn99Nj^E^W;0J7mu69jwINWLA62Q4&zB2MsG*;+#-ySrHn&4W2DlI+;Hym z=S6Mlm1?9Ujnr$za6==rkzB#9?IYYGazUEhB86aEpk?>@dttsS;yp#Vj1VnhkRu|N zSD=hTwu?7mB8bE7R}M{Me2nr+DLqZ-F-aODjstx&8mH0{6WItsIfi%~t%4R4MJmCb z$T`xXGE7mXgt9jIsAQ;GSc1T@j8=#?Nu{bZ_dv6i$TI$&{P^q>KL)duB6&dR@~qY+ z`dTcqCceF$Qx40}P`e3fR)nLCfE%(=M5e8w3EdJ}@r7YM$&bE`UB!a%7B7|d2!%5= zi?HcIiY=})oG$8y;pr{oGfWTwmMhCb`G^+F3XR@YfGLA7K}VB=<(Q3@wIKRLk764? zP1S+eJ&yj=Qj^Vz8g@zsYR)u?rn{k z0ZlETa8cevD>)VC3I)Awzxgu&` z%p_IKt3MfpR9(6LRKZKM?b{MSOJq&7Ol(Qyr7KNNBG6_xuQK?KSr|}gW$A8dyso&u z+L_ro>@S&1IiELbzb{}cNXYNW2Rxwq;eDwX%yoHm`R?G;B173p17Hp?yV(G?ghwKe zGmbD0+q{*F?2}gbIFxR>xiU)aZNt zhS?Vrc2hu$N8?MUGS7h)&eodu9nUpdezz35nmdL}Cc6xO7;pOWEO3?nePGLUb3<#- zUYJC?WE|ZG2|vVC{ifxjG8ew8EbC+$Zt87XItwS6=j^W;sp&iXCN}QPxx17sDf+9W zCExem1@Cvs-S@+>NdW3iA(%Er2t~L?#rS*dR_s0!axIsF)3L=q!@bVQ-&r&R?&)6y z+Kk$?f6@OExR$u~Ak;_^O5vdwU6nX?xl+Fh(vENr`z}B2G<|LL%j(7|whML|a5;6h z@#EgDbW(A4(2|7VCg<+-MF7O_wmEb@wCCdIrQpTxo#Bo2W!ovm?a38*tZCQ5_sZJ} zLkx2SB^jgZTd?Ss`1Q5j**V|$a)&-S+1Pjv2y9`Py>dMQZvN7*l> zFN)KNF#Z@y5;}dVi^l<>$6YJF-ihsp-XXgigP4O2$vqi1`6a3@Ps`aW*_Ix6KgH2B z%U%!<`nq?M0brwHOUR(nSLJ-g`BDG267A`#SOwQ2&wH`oLk9UZEmc-IPGUTPYiEa- zhtR{=c`R2o*N}Oc^D>AbwA;_>HtW&~*|<^dRQ>?)mGPMTCT{6fkrTwBejJo_n9(8A zA~_B~sDsJ`nQ@%8W2}?s^visb%ocWMMp4c{Fugc{=OjnbJe@^caKK^cn2Jsh)3vG9 zw{m$RNif85yLsJ5QO0m1rvX*;)*Z+EvuHdfdN+C|`iT?kl!TP|^A8RIUuf^dx%Q=a z@OYswd1Z|G+Wd=FyFOpgPkf))Y(g?k?Jn_u^SQqQ_ak{UcdeDCWkCxhoB4aGzY>^7 z&1|l90d!beklhc6?to?tRz$KG!^#!iCNS zh6O;|zHJiJkC`lXuf6#FL3mI&U08JK_U&7%s`u}_*2{ipEG>K5xO1$YE)N~j0A6=w zxo;}2m#-|C((7)tulii%O)_vbKY3@2Veo6yuGR5#R-1XWd4t=paPw8q z$M-A_Dlanq4Og{xrXmWp16Y4aTua~G<*mnj)CdHxv8>LU(wV=g94f1HYM9+veDCtM z^h1dCQTI3y4K`!)-K5PP=nFnX_LBA9y6J0;6eWmecqTm|V-YxbG?G=8SjPHW^}gVC zBc?uPYG`jL+1_^8EWay18Fp~FTjbQTKepRBY+PJYWD;ywQqfp(Qhw<$T{EGdVpi3* zAufG$`CgFUd2KqTSQ@T>s$DhhJ@vleT6{CK??-=A@hVW%!mr0k+kN)srGKn$tT|p-`=y%oguICgDDpn`$Xr z?@qWoHQLR6v@fhyUzFV$_N?B2=$Zd?0g3FzbSK=U-w1jPy4-1ko(;dN(00)tE6S63 z8?=93-yzw4cYeV+{n1L*`tq6-{_gi(R`v%#3UnK;Vt%r>@y@u}7!~|3)crJg+WH;% z{gu-7IymFH67D`K85y{EvU)Q8LiP5v6}+S}H+-%gP@E?d3+|Ro73E|6HV%n2N5iz2_WHtJj%6n3kflCOC{zBm`{rvkMP7zwx%iwejVBeZ~fY5L&6L|w!!~> zSc9ZEj}&)}b`XWl;t$~#1^qH0XO^M~6}{IFwvvE|D*3bBkDbi&=+t&b%>L{_vO|t! z#0?#AMWh&L_G$_`c=$-j9;mw~h~M#!CTdPkpCUa$9AhD&AyXrvBaV;}n+!6|f5*zm z97w2t&Z8h9#kwM){d12NV*l_-MQjgk{_auJV~{WrzlaeVs08KTyU}q}SosUi0I_CAh|9=f_K8O?8UaF?PNJ!+Y4;!+Y-s4lm^Os!>O#DoqYD(C9 zy7SsPc-lGgg513xo`WO>l0Y1~JNnr&g52Fad?i5A%zy5XKpa1u1~4=JxyA3fG_%Q5 z9YzIDA4f(JUOrwvW*HntMn)+g2PX+VMdg2*BYsIUyZHHeNdN$Wfq}e%g1nwS&H#RK zad7~j06;*12XP0FZ;*$dEr`d%m*ww6{(X+3qp!V>tCydvrw8N1b8YQB{r#kwnIAg( z@AG$@jv&|n_2l9E&ubxG5b$sXz|YGE`0v?>rcw`QC3IXtj&7!kuI`9AL-Zjd%qJ%G z=l=iSmH+kle_ERSuO+{LIN$$m`hPC{f14WmI{GMhx+D7Zllh;#{%QPwFaFa|3h?mi z|7R-x&hwwMh>4cLkplcDG#Q*~R%Z=_9BEw@wG9w^gqHpJz!2XYi0xqyLqf^@ti<8T zi-aVLq^2lm075>t#0)e1yeQ*od-$nsJ-UM85fSpCA!kH9vk~d@1sBcp{G65aoE%DX z;v{9{3X?_~WL-2@a#`I@(u(WO%kFRRZb9FZ5YJUd&e8~H8_!%xLFfGmSKp1-sy;sI zSy-?;qS+8dfk)qq)?=~K9oQ6!zNb4oZs(?9b+K*bdQ)4V32cR*jgXm>?=q|Y@VQ6` zv9rpa+MYbp@=fisd%5eBocn+*WM{#TMn6Ra10(MCrX^+*?;|{gm1<-)y zfol-S0B%PZ=0#5q{7~%Pb98j{nfq|Z#fYI!a~c-TZcEGX84Qm9N*0ZXlo=Lb3EL(>g(WS|~XWHIlF&utMe_#K&cFJtJotCY}Ib`f&@dSm! zFppy#pz;BSgVs6Y$&<`ksz-`q3RxXkY-{|Q<=!%5Tauwfk#ELlPCYN3AKq9p@hL11 z_9iRLte1>>;j&?s;hC3sFyGzycHgl!7UqBNUHkIX-rZSPR*Y{2@&!uR$T|s^H|hmn z^rSNQTmY5y_{wI+iOanWND_onKDT?m>K_wfI5Rr!6pv49$Cdl0;b_da19HPi z`UIRNcbhwX@MIe0Xq8?WJt|`vaM>jCCMeD;$nkF%`ZF^@F#a3+^X1EwL+cjc1We#= z{@8PEL zvU>G?u7N)DIMBq9*=c!v`f)t04(#+YG*HxH5qShl>H>Ii9)9M>OdTrvT%F$uVl2e* zg7S}O_-{0#ZEuG!0S8FoPG~=I{~HGlSclQa#c>xv;hTqnm)s zU(Al9UjXSc;5?3{-m?^=QEryXu&pgM!FBpeH`&Q`5xS+7noiphI+CjXIehdmI(-LWFOgBSwS})09(54&NPD zYLaZsYO!L(>ZS;+@hm$D=nj2~ei%y%I!|e3d{7z+qS%|_^D7{$?IHh-D!7upzv}{E z!~RlFO+lGO>#Q#7j?5Uyk-UC53_X}T#T}m*jQ-4Dq&0v>cYI!^p&A=!UOgeUuhP|r zNgpS@!+|x3Ulb&WkWjcb%8^di8I*Jet0soj0s4Iq`^*lpf*KAPsE0@iv~qJ`!gmA5 zfNO=e)nXxDCW;AIWg0$HO?vtqad`WgXiLu|Oyo{-kAMa&E3pFM(ozn7<+`g~jn9gI zkUaka`id%At(BmFelKJBRpPujcic#X&{jX-Ww}ipu2t1h(k6t61QP$b)wXqwDO4X% z;LUisu?5~sn^H=|g5Dw$f(ibsKEebOu#!nql7&;BtIVCD;|9t-v96r^!&BM8ocpJD zYeNhou${P!zM7+OsnIkT*Ffi4l6OI;#bCINqel7 zEta(BPn>lO6|*&-(Y}4uyOqdk_;|wYlq@zLW1xp!+K%OM(&_5KbeRhO4qym;79}5U zcdpX$M}+a1_ZkjDVD;{OY=+P2JBCG^!!t?I8jX+VF za*k98J^Et{XuvCw@u6Z7mq!*3qH$x`+ zaUT{1rA3pequy^w6XRz}xUr9T8Mwh~{BECsIcFxbMB9s+I&>=%VU$%WBu;Jrn#KoJ zp6@Y8k=trnWzdq#`#Td&bA2hi5!*2@eTZ375tQ+kyz{2tY* zm*!!pe?A7bfaPeNz)Bs=dzq(llK@~0{~Z_Ym;WZsb~i=P1G=3>FZl_R39};Bn#M~B zFEGZA^i$G8YHpnOCO>*=8DaMLX0@<*erkupi&{dn_{v3g3xlS?N?%s0(19L=qB1;2 zWsM$X$joK~KZFj^QhX0~uLZa+C*lPD0*PEgj_C28L3oS*c`dtHsJ3iWGP8oAT-*99 z$mR}hG;N9gjG{*LIrffJ7ZSJgy`$ZEN&Y?NzC$$h$kABV%Q479RH15O_6U?#jtzXcGI$8ARg?qefA9}Ko*qr}{UZWAwrHY!3j7CyEdn?p-TZWtO0`1nPrI(+Cgn} zVl1U)lMky(ozuUb=fvPeQv8PV%V}=a2^wZrv|8*BEJfj@5HvE*ZK1}}Gcb9O$@Qjf zTOSU-8Mb8~e8ijtON48}bhNerY^RHRfLXiS*q%0hr}ODcz^Uy1RjHkF1pJW$X$S$d!Bi7I2cznE7tlW7%7KL}D8IeHJFZYX0yZ3eV zIhee<>D+uz*P>#}jf*y*b$>(O zt&p=R)LN|yO}c!PS-~p+A+=7=tMOn)IzNp=VP0U$xjNgLu=QO~5 zT0c(;gv%7Y75wA2vyb}!#(g#Wk5qRGjcHfKSe0~ZF7Z5r{n~lmGNpth=b4e zN))>v9SesB+`j!Tsi&7l%R5X28u<`>AxxbZIwCeUf5N-92({MI+dlxSX=#O%`u7+* z&GBO$)~aK~m4Tl20t8I^hUl3D&d}*yloxaBi9!EaTfg=aR1H{^-R`u$Anct*N4k*Pg6hxN4&VKe zES`LKs3$CCehIGsUY*Q!CH1m^)7NP_^R0PXJ44O{JiQLinx39MVQ3{ws#1BoRgT|q ztEECue^#Zv9jUAWpj?dzOr2uKx;Uo`XW7J364$_<+rqa}i`1?s3hKq3C5a-6Wg4=h zk5GhpAY{mb9o?m5h6BPEvV&rV=0LqshV~kE!ny4p7H_TPLsY@Rq=?Uj$aNSC<&K0A z@hcN^sY9u@6FC3=1;*Ou4GD>3uRm(JtexD?%}*g$W2cV>S{?zdk`Owoje5&2Z7pgO z_Lv+=S|RIXy-g(spTQQEEd|w{tr*_mC^eO}q&!gglz{{(V(1R&V?y?%(S#g+n}Ngvgx+i1?q@GRx4uy$@YzKk$@qa1D2@dXu> z{>Z*-h!=<-ic0t328hJ9dO>!4f5YD+UFu&C5krZ?LNBJL)eU8!Lcyx_e+gy~GS!&6 zRKfo4G}_9bVVzO5y&&_)z@J%kk^zISz(-N`nedy7@7Z+?-OBN#ob|O1lShjJ0mmk< zRiaiQn^1oZ1S$R?q<)xSRm$rVe-+zRvV8gSyl*JAI=R1>t3z4BqL^N zd|hN)*Vgm;KclOQwhPkD9~AM24i)zWg1jh2{-oQwylCOg{d*}H1mTG~8Z&=m-v>YY z%^>;+do4p^SYfwXrC`D@P&w~!%=uax5p@a`sO?UdevTgvLtd`dvgq{Vq>V-hN_T%Z zB{-~auV)mT0WYHP8rO$;AM$70+eo51J|-9vkAB_9pD7t2gHkUb;UENG=V!3Dq~EvS z%KxJGC8$w&F6rv`Y=zei-h*qNhNK7#{PoFN*?F*v8k?(FGo6Rf7rV*ks*OfQM;@b$ z09Jim&wia`{Icx~6YTck%y>lD`4rOPlc`QQ;M)#Z(Qd5vJDj5emavTsCH?p|4sd=J z+K7+d;dosn-R%2aOUm1VfeuX0ZDI1nuzG9muFi~Q0SMas;Gci2v{80u-Qn{ss)d@6 zR*W%U)IEL7uuj!SuerR!J%fzP=!n?qSD8u0GvOMex3$NcJ=R9=ENXs>=iN^qz2-94 z8q4D0su+K*`);!x27x ztzW3njWq%Rt1&?&%*v{88>H_$@gqxb%^e7}&ZoxVI@LC%2K5%=1z-1Z1`VaBx`urR zrwSz`Lw7+%-5a>Kb=Yb#i_<3JPv<|jtxa}R8|F(fg72O%E= z-mcBPe@;P{--73e%rhbVw3z?t{#0%qgHKCO$n`IDOdH|VPM)W8TN5?iGyQ?MsszGCMPCVJOnBvZxZLkIX`wXVWit1HXr1ag&0Dk-_hMnP|s>xo=J$E#!0 z;NT_}qYr{>@b}Lniaz?|x7>Vv*_Fd;Jpqy5pD6mtAK8`181^XIn5CoB?_`#RK&A&{ z@l8wNslOWm1eby*y+*$(X0v6t2lOg^u0~%)Y`l)6!}lrgd(DK|no61AoFROT2W4{@OY8w240D8d9GQlGFMTyJzGoxOjAG-0jh=W{Dpe zwXQ>FL}T-|>iXE(@DRDdUY-kM-2>W)fV95-7evbuF`hpAn;M*wm)T>{fbTVjieJ4# z->b~`lm(Z;@5Q7Fq|pznTQ0i_%Dc)3Et=dqByhx+{D=*Js&CDNE6m$27So7d_dT}w z*5`B3l_MB3r8^9#umZh#%*xs!;1gDy^}%q=j+_j&Fez%F*ED)p21=xTw!5iUp`p>E z0muw}^(s&6$Ie&FdPk%AL>v0 za_*C}woGT|U@zeTAPY{;W5%IhrD=GZC_jIkgKw_R`{Ro?2*sEC{$syhs<+CLzx{P)kVVppwGQ&iCYi`DwW_o;v|?#POEY?IPaopP5)hl?!~ z$3xt!aTPtqFK=ryQbEgxu8r)3$D8aS*FTj)p%6(FO!MvjXup^&-gT8U3z4ri`t0SC z#NSQA%ZT7UEkG0%rtOA4fw9B+Dvikn1M%+#zVZP3dWy$Lf>f66yT3VnNOy3pHFk<{w@|z36AP=?QSA8bI0V#jn|7J(Pv&#E=2VzrjqmC|uT;Ma_<5%ESX$aLhLC|5laPKP zkXU{Oo~9~O;auTE`Wib}t0s%NP$E!q4HUdDM2g>q08PG+XKRCQ15;)14KKY~eA>Nk zPH)yb!vk6xTV*Uag6`;6F=9cz*2qxq(`BVkfPT&Xu{=+%taC7m%t|8b4Eok-gLzwW zt3(4Ch>L+kjQIqN6>RlBPj;t&76SNiuhOr=|Hj1On`QocX%$y6ntYhaXE5dZ+EEVo zASzKMO8B&duwKS<_0Jj1@rD=rW~;RQ%8u=h5rPhut6dmmyOvBh4?rFwq7|fs>-OtJ zo&E!G9$@^1#wv_h{3G*{#z+iEf6?t%4%MCWJ;2lt&{RT%dNDF0nzuesq!=D zSv#@ckUZpPK!9~}ZX`p}by+;gb8ym;aJkAfy}Cw$u2>)eT{MRb#Z8E<=LAQ6Uis5Zw zi^TkhUV9L9C3x82{^dhD4yMp1ut-Sl`pUjbnYQA7v93xrg zMjr}##ff9KdQCd(dpi=gm)sgX_q;0i=45w12*onz8hs(J(tVp(?p!|H*I?Wb&a2_1 zztmt2DBBDt`A8%4l1Nxk0&R74Ga2|43G&cUI(!aU;r%#BK zjWzPWNS99V#@679-{`dLQ}T=(6jO36NvTFoYz4}h0!*+4ib|Mr{Ky7aBUgP@>F#E) zgzMEBXBVx1yjNtRx3G`Mj2dGf-Rp^y^i;8HCQGF1$&*-on#$N5 zqI~x+ZQ08BG^G>VR8lUD4%IBFM9t;iyDb&MSJk_7`csL}{rqU#ATw-IS<`^uhQ>eS zC}Ig1(wP)uAle)Q$D8v@uun%vb$khgQ=(CU%1P-@^F<>owTs~CO^NMQ*2+?-Etbv$ zXFuavCm*4R56>3A)JdK7T)ioFw8aJT*lj>N&TqcY`&9I_&=j+NfA8_O`^i+IWxGvy z<>G|Z(5X&o-RxEA=Y-L1g+qb8&wWU_lA%gOGR5j}V>ErqmVQ%7GxJV6Y-$PtswX+o zzu^{w$Dhk!>Ag$a{}xS*20QI8^F{Q`OVc{vhGnIQ5!gkTCYUAn1s?pZ**y4{f65bo zJioZ?_}4GX57@=PkhKVGLI$=(6}?O_W4qVF&hbq|EX^%mU@yjAu zibAk4Sp;^HL>0YQ%Q|}1Dnxa2OQWjMk;(c~(l0f}3#-z;xI->7wjGoRH!t`ipUtZO z0iXIZVyY1bN=TPh^)fP8l_u?asEa*c#AAs4L(T?9rH?>U$Tj^6MN{4o9^EZ*K`LhJ z{XV#ap>Nxo4AV-R0}|>$I7@I2Hnv(}xP#>Pf*Ey8|KW8y znH{cLLyTCi&uNb0^P-R#iYvzX63zV!%6aA>Z_cK20tR_y5lFl!0)AvDw5J|X#&cjg z)#c#097TW@ga>GTasqz@k$_Wy{mk^Qt@;DwrzUQcmnQn9Jk*MobH!oYX62Mlq*i4a zUPj!JWkiBF+A%PHilX6T`Uxw`@`6Zu(^S7yEr1R56?& z#?M=Qk^N`jadMvWRq?0We6J3O=kY?9k!D}?p;M^h_jE0s@%l>?&jfDRs^W@0gFB7V zcy_UV@mgzM1y;4)>wmB?JzTBJCgm|RFf^Hgo0`OTxz3ZDzIP&X+T(exAjU7b23J)o zp4UCLp7SM~NUiYIy8b#?Gt9mNSFb6mC-Cz~YIWYE54xCabAWfhP>E)k5&RT6B_7x5 z(?w{uG|;YNbae)&@$3$tADCZZE)9E~0`9o5w)zQVy3dZP7*?zc&VLb&Dw`M(p43O; z=@NIb(+S8^5*N*ju{Nm$MyXw-yOJQOaUIT+FnPRrEXW7!KF>dK$=a1~b^GHNa1~>m zsdj;Juc{MECjLpd^{Wgz9}@0=kV98mB3utyr(MEpg|eicse&_hIxbWUyqTse^9Pwz7(-F%A4pBfZ?`$3 z2H5bEMeMBLt4Ob%)zw9=`BkxCH3fAc!vs; zHeV&fw(nHxV#V+Xh!$2wypnR-tO3XvM0qum#d-2clv)r+bZFK^>vWo3x@rmQ-X-_c zKkcQZ@}_CYH=C$wZO`tr%F`JVVpz&Iczdphg(4UQwAL^ zY&2TYVx2|UL{7;__y=72y)+p}2S4^1b8p!}jD7U-_--TH`TkeU`&)z+t~nBp|8^6Mv)K4bIJJnK!#IOl!NyL%LCgNqrl8$oIemTb|&Y zUF^3QzDCL>=>E4Cbebv}|Bz#_rZF-W@0`^^4Y$DTYrU)B>nW<}$V*c5{vAj|zJw9T zMv0uZX!lY!Lu{t$^tS@J6Uoq2w*Y(~R~D}1J;$#-^3J4*SPSnRbA?7W##25q6iV#a z%d6I(zYKTRqpe%ofE^N7UyrF~E`(+-EX@&>5BFknbIq+y+QTxg>(2Vp- z(nW`3a@{`daiI=;)%~3M1FhVnUcDI|di=Na9@m^*5|p6dh^i!IXe`Zx#R$TJd$T-^ znkbzS~G@ue0{yTuIVtq2cm;F`ts`Ua>Q|lWG zk^$w}zz0+UXYV-07VMqU|GY_s2|M63&M#dt`V@8#cvLK~gnu4dGi)WWuL`HQ+MnBf z_A;e^S>dkEa+qLFl`h&MI1!aP)CF6`cE0i{lO6MUKdu+)5ohBsdm37(m(lLft@}g{ zf3wq1vqfW-NKa_s{)P2w^SAOZH|ynoqk0l-FuNfNfsiY1*1WD;Ai5wj;}Go0&>teazlY9zXIO`~v;@9(pQoQvP?&hwx20G=Vr7^5b0^i9werqCtXP;Y`4cu}0e=OmxD&$YDkw#6 zZ_Y-}7TZ%s~9|X6`0nqwAcCL-F&CVO1s>~i)-miyT+U}OB{t8vE!bjMSfIV zL>7W3J%J{Old=TcpumXDb<0lkOnfXGy_qbC)1hK3kws7F6PxP=-HaE+$+u0BFQ-l% z(7ar*2Y{)CB{bIFKaT}4U+hn3Ps`jP$h`vq>+Y_`_4?hs#kzGuYfaZHa2(tQ3hKH~ zK_UGv_wq_904j*D!%%$s_!NOa$kiBhq?5Nl|E{&#{o*6JHXkxH2LDJSmVoXNYAj?d zQ{WdW?b<+!oRV+&bj7fA`)hh35qyoq?{>;>oX&%OXC=U%`vi~_n1#~II;gFEa6iBL zasr`hr1zjdr4fFH7VMEQ_{bC~7^ZkAPTu%jzT^Cxlr(OZVZ&@E#-!bmeiB$zoG6U+ zO*>na%^`m@2I{kZm1~TBjErqryBE%^oNkQ@hzC*BnA!s-hLfmrG0l00gyLCQvScB0 z<7X!zd>N`x=7E37E_)5j)PJkVwg{Cjb^;GSQKBCA?)wAZ2BiR%JUVa4N3+A4f}JW8 zgTK?Qb~@sM_6CPG{YbBRYx@28Uho$Rws}Dm7B~`irMpiTc5B6c=O7G8qIqzlG!BhO z>ZO87FqGMe^cLY3ea*M|GMpq}U}TLH!MXYO@Ah`nE7UdMuzTcWSd?ZE`0}pVW4%)h zVpwZLCc#cJv*EY*t!CI_!wnWYzYN!V|4jJMSJ!j#Hzon?!i(FhU+ef!;%P>Hmz_2cZ=IforkiuLl12MTGOTlIKEj{n#;Mwj-zdD$I`?E{&y zrIK)~p1Vjn1SdRZt>~?prY?QgWF_4-uu8bJ@NRY*nJvd)JxrCTFF8r-1&7E%V66M! zqWawbtEm2ItT*=Gl0F~4fs~##pV-fBw5Y!ZCzYjC?uLc0?J{FXkpx6whlaV}1V#ob zragr3==#F4gb#RiQ0H1Pg>`6}!8?oGUj+W-9(}f0KbGF6y$HG1=!Rl92VIc+w>xIT z5m?Rgr>$z5YI!l?YP)D0J%*Mx4rXK9eR5~d=w0wZ%Hnh5@uKyR9HoIrvALiZB)@b= zsG`LtqQXzj-@7qAVyCgXoZ=BQ#2gb~h2WzF?6Qb@Ownqc#mIocJy-!$pXQ{F)H`R=FsrfyS`H(7vZ>io;ALlW- zY!Wo{LK4_7MczHK0UJGxUWC{v34c(9S(d@O4_o~bU!rgf z^wJqQuXodXoQaR@!;^Bs#lPeQ1ZoS+VvKMu15UHskpCqJJcU)RcIJ?hs7_CfMR(#!QC%kv`L9e-v3Y+ z)&^_RyjIF95OZw~t-G86B#S>=uH~`(=r8%?-kVA~H;J~zZ&fds&lUwY4!7QNmRYGB zfnti~RG@6o@(=eDf;@JIlFnslOJWHb5?z6FaI9Upst+-NAo3PB$cYR$p5hZkSu7?= z1t{Z+T3E+7NKZtj8~f(DF?n!g53$aS!;xq)p{t*#e1t9ap9mFIFl;;#UT~_Tz?7=+ zCh85+nOrnxQKV3N{-0D$6PUTF{*A3qyW&=ym8RaT=ir zwk#Me?T;f;NQa|^8kNMJ9}90;3MB)*+nHH=ygP2O5s3%iyj+HQI#D-hC z(wd3hr#tkf;-+J=Lc}SJOvSvFOB#w;=wn?7ShPku7@;MsZTl8dbVBr zAt{bjv@f1;S&Ztj2pjUYZ$ZT%ycB}Nf0zg_xiPj`#dm*Ljm6MhFHh%Mc@1Tt2%JEcwa#G3|F2ay^_J4_gi z(j2mWYpj*w&Bi9mL=X+RoT1@ZM`m5p^bmf?pc*L`)4P7mSkr6jNON@+M2Pl>{Dgza zpa~lqQB9#>m!yj*O(#DA@&6$}c)sS6x58$i-E3c`+z|N7HJOA7-CeLdgW2|`*>9S@ zo3a&9Q&CZM(&iEuF+Dk4AJ<-oaT?Z=I#KOx5sQ%r{#xU?yE<7^&VeIS-{58S9A6+* ze7r9b({cHoNPIjs`F!1Fx%>WZOC!O@(Ip_7aO}ed-St50CXaN0 zmw(hC4vLUnyFk7!U#=@se@JdGz)cpdh46P3x}xACR9Di?97(qn85hnx#bPhrDqA;+ z`!NBF8;s%{0Vvm}zdXxuDZYPUgPC|lm3_s}sI2Edl8)o9HVrr*-uKaQ0*<0sO(Exw z{-Pg2zR*&S3EknYDZ71O&-l$W#c>#=iN;+(2xg|N>DnKa-Tg-He?_-r48ZhSFZ_1H znGPy{QN`Cx6Rb+sZ>LPik$7jg)=XUt zSOvG_Q5mo~bs*x50X=3QW|To)C!>b|H(5y&9>y#mrQ^|Vb1TZXI0@SdB}vDEPry*< zM`)oJs{D5(2d9v@cw%M7cIzAI&acaS`00oU6fv){p`~AmtsqAhtPi124(xX=qRhy+ zgpEhuR55(lVv-GP02yK&+XJ`^ToPie{+Dw2lo2|3sHY5ptvb=6vh>6~>)S3+~dI+6a zkdIGXb~sg2wEc}vbeJJkLc7kA&q$0lHwD1`V#whaKaP^8qXmf z-8Wuh@@%$K3UulN1L-N9DjXkg$ft0LJOoSTx_7-=+)_F8H{nD*7rlk4oX97d&;j5f zCnLsRX37=@f~OgRc=@uCO8{9^8F~va47;Ug!EOc@Eqb;1*Y}q#J$(p@Om!EdWA$n4 z8RpBd)-wu!SBu66k|Yy=L%=|11w!JY!&55851*b6g{Bo0lpepqAjd{sGaN;Y$k5C{ zN(nV;Y|`e?JVS4hUT7Lo1Z@!w!DAl9Z<*}$!&=@j_Ol4yV0CwFjtTx<(U25kWV^n9 zsa%sVofG8+hf)Wj2R>J7xNl7EUxNKABxCoPAqkA$-vhyG|41Vb&4KrE>Rj}AmAk?K z&|VNJ+#FHL%B_QjaU@UtmWxibx-5*^!}xg{^;k1lv3G?21DDP(lBl4Y=gd$#<*>sH6o9T+{`@CEjuztssXrz z4-g7_lcP7tRM>|ZQEGBot(RDfy7W(qgc%V&w`BQ$3Rk-0uS5|1WGz7=3dG+M{sD4Z z&)UZ1FMwjx2_K1Ru=c_H@Nr$Jwyuipi1bEnm_GC6o$e@Ko@<1fxP(c?H4YoK4BC*y z)Cjf(coUZn9S_Yq7u?k{7YfP%$vJxXyKZGc@5h9 zP1SygMNHYzfcKr z1$WwM>tmLw=soU&{>nW^-oiFQC0$`reiqfZi|!Zi0o zZB*u|SnskQuRb}_LCt86RdwUR=#t$(OoZtK(lN9QU}_p0xQsmE3C=uFE3;PG^LE*k z5Zae^zGLu(0Ud`NX>2=nWF&PUcK(5)p&dO(s01E68G;kw(@Id@(^1?D*XSBq-9MEi zn56y1C<8kiYJ)n1_I&Q^S`n+ra#il z-MtA5D;kI;92vR89Lu}0uC>(07nYGm>G^2o~dCZYFqEXRk)gFm#FQ*bFn6efD( ze7=dBSM7owHOW6(jM*TjaobiG-b(Zmfob*-T)%w$<(dv*m2Z17Wr#oH{n70Qj#Sgt zFc17rXRK<$8>QcxLNnNSBjmV^-&>dIXAwyH>CYYnVF&6#TMZJlX?}{qR3=B!5NT)un52$QL>q=+2TB(F<=+vpF;SL zas@_~^Eu#WuGgux{!hH69*4A@WGrshC-B33mx2Ck3$M|j>6!y!xk0|+Qm=RfiTY66 zDVKOn03m_La`oELp#3sf-F&6UoP^B2aq;IvZN;ct!s+L_ZPKoHUqyL-s=@#U&5h*+ zhKA`hhM71u-yE-(q?<8LjR7Vs=F4u&w?wK)z-p3hjME@zn>-?!jTA!|IhTe|`MrgGV2d z^hMMunENN;7Tg3P3So{WAt`?v8J|wrFYky^?Xuj7A+WN^b z%VD&1QPN>jBrCF=_MjNzY1NYWnn!mevjS;b2|Y_%F4%_o^p9)}Q=k9WSkbNsw-dA_}z8i~n2KptHf5A!V0GkcQFS=#Pk zX);`cD>B-rP;yQ?#PGYeGr0M!CT`kehUxW^pFd_AOG&wN0B-QxpLdS`4^3y;5LE-M zZ9?e~K>=x`8w{FJ5J_pIyGu%>85*QJ1Q{9xrMrgimad^2fuRPNnf>y-=bXQ=KkU8M zy07crUBf5xMO3qwbs+9kP97UfzK!O1vdr_7=Zp0>3B^NkfV+KaYPbSxkx$>IM!hoA zV#|GkU#(Bcs7bw1hf$*v3GKLxRdtgiB0d#5Ht89Qk?vIyELM0T#R2=h#B`xPm*gS7 z^JLg8tER{14fRhN^u>-+f+~IUJH=9BysYO=PxvH$ zJ>oZcV~FGpCK?VscYA#|5(hA^r+!<1n4q2jg+rN2jhxjxz2NFCp!k>7_VHm6YcunH z^H;M)PXU>?o%Hu#R=-e#28LpF?Gc>)fh&ZXL5}CDxBeEb>7MP>=DWaCb~b_DoE(p6SOm7F$2Iw!mjtBSR5x@&FNy z`uO!Hjsdx|Z(MgK7_}-R6${ouOZ-@^uVvB#1o6w?Psis1Zk>9!haOt0V$PpGr1?u~ zlVC;=EgqZ#AIDO$WTTkq7tZhYmJi6c$3OtH&aFX}E&i{my4W6u+Q%n*uS_jzrDAqQ zxGibWrB%p1+Kptn*OcpfePAi81ep!49Gbl^9*33-{l zW}~!sw5({pf$XKHIQ!=PZ#SL8G_DK`r&LVwh22_}!ZLJH-68=-DEDrG4f}|W#qMU` zRVKe7oN|=^ro)%g92+%&$*ZAW?O_?$fl~-4|^hhAO@p8A=5oamy+_$A8zm z$}Rzt?eKe`MFyr-!_e)estmvMi=xWeO-n!Ja=4xh1!FtzRjk%rww_w9t$0%nTAXq_ zu{+Br;YBj!v$qb8vpPH_*o2#AB15YsY3J4!od{@~=8=P+`=y4#aMy8KF#FH4r3{@1oXA(`hJhFaj-ICG&C8gs?Od3s1L6w&VF{kJz|GnXfPdLsYqZWP!5)op>7TJIw~H~tTcnLOYTw$#)$ z=szy)Bc1cmAetXJ4c{UpeE%wOeR_ysG@dC9)5+k96_QyA!gXM|G(XYUZ0A6MO-ZLZ z9xek}`GNOPibksLcbQJC*6HXXsytfRsO9XhGGBC>uI5KW)3?YSg$b8(@|;og)eVCw@Ad z)9#xP7wh`Df9edPd0#&xaUB0)bj@M*PQp%&A%w+5*xY`k_><5fod|*I(}2|_k0STI z@uhI@i#0E~i5AWf_~)TdDaa#%!=uXWqsmDWuebNyj4aH9AaZqj z*OCyFCI(4aE8lPac~1rH%mT^S;ztrv=EFkXkK($w@&Gf>#>CzF;jrUxPsR@_Nd0*1 zd`Wf{!6_gHi5YF1LNb3$Kl9Ws*L?72T0-mFLZu5>%+-F}CRC<}06zNX&q~ZkOQxUw z!ysN_R1q@%)cKPED{1>M1CN>uhWzQjFjfZD`w(;`Ci`gXjYk?%B=lt<)CSygiTK;g z!H0)s;(V%S@NZvxVz#RhICbWGvI$Y?)l=-~2mespq1! zDSH9{H`6*UzyDaIY=WNE7Oo-vaZ^SZ3Nupb39Ul}5+j0q*&g+iyL=%*dHvdrr318X zb|~iDbj7!4ubXW6rzwCGan)`;83H?u*? zV3m~@Nm{}Qi27A~h&Dq0ShuQ%qTP}}kI%NqZc#1cZ>CO`^g4~|06AqnCcw+R<0vV4 z1|-@QKMTUg$A?d*#_mkd%?5>MAH+M^=r))&uG}3H>BZ~$d&fd6Pcpb}jK6)B2YS~l z{14m?Kx`v-ug?gBr^yL*C`6DtUB@tW@IDJG0lOG|&kaUL@qG4RzW0Y5EJ@Ptc!nPj zZVQv9>(-Mj10emkn)Vm;DHW$~on<6=9vD0(`y(r$}|LX#{6CMDH$_ociWY|PsT%18#f0a89#7NGi^JJ3xx$l@n zC|(u+F*1gsNpk|;S%VQTHVwNLHz+yumBmPrKU02bV1uX_Qe+`<$=>9!L>?8R30d@v zGojpu|3vSs&V9JYxy#B}m-0aTu?2v_)f4cp`_g<_a0g0C)~x$s`xA%xXK6{P>%@z^ zU}o}mMW6rP<+ZqXfoRHUXTtByKLTunNII{?CO?TEDg4b7^;*QjFybHaIOm z_ud+7RQZKgRrtk6Csvo7uS<}B9lpdyf=(OD+U6bQ_^SJDA4GP)HQnJ~DB&sMApdna^jSS7skybwGK241r9Cxcx+GKpR29>a<= zG7-bh%bZTOq8Uv0cQ)MH{33qA4cAl0r_`#KDVrH|bc7~^)6o&9M}wuP2%&WjKbU4w z0TkXxI2-{!?>Syt$*m(TMd=Dvo{a~yz1x&fsNt-#m9+8Y04;=Pfu~VY7+VydHP`K? zpZgvHw^>+qN|nM!v+u29vTsprPkcgG_3E73@vrbPvfq*J$J<6>^7;c(kCiSx?SICg z<5a*EfNTXYZ=g~1CAk#Iq?ihvKd}Hdd5J>6i%M_#4UW|rVEBIJ`p`_no8Bz0XsqE3 z$?Ip-_ppdMP^O?b>hWdeJBol0W%8lBj~m@LmxtgJVsagIQb41 zxlq&ys-qZ!NvgZ9%aOQP)1(YZ1NU!|W3cbzTTzP}5eb{?3@_0S%oAZQ23{ZaMK@KX z_-+v`^OAOW?Kd&}QPTBpyc^Sp8+h)cqNj2^|DCN;_|M&VQeLxx zn{?JxmC!Zi_U1VK)5NWvLjjAadk31lfV0o8{V!c|CBxUI-JrCLF-V=-xD+(8-*Io+ zLWJOA8swp0qzDCFslRmC%vH7OZ#B{UNk<0Hhl>5NSAa(NyfpbB6>f1l(r9BR>fNRD z?n7ESIQ6L8^_HJDx2rTpnfi8JyvA?Nem4CWEGCPKz9;ylR?ues^L$Q6$aRptgb46& zx@@x74g;AZTozkBA>Fa%ZOge^va9E>C$wBp}>|JG~23w?;#0nfu3y@!CWpeAXZ z#D+HQK_OJU*d4M04mgWM4i6&&#!6PQ<7st(7Bz<7?IE2NY26 z70Ci~&DVqfV$R@vwluW3=P|R#(d-mFjn{y0Yao6et_*OdsfeQ>p^{x9A@Ru4P@H_c z9fqwZB9nOqXQ$3R>VpcnnD3h+XzwAfZpAE`IqYKkT%4D;z)y1-!y`$ zMB&-7w%+@D$TLQ?4c{WyJh129>4qoH-JBAGH1ci3e&FB&{$67Xy%?X<8~GcmqQqPB zz#w0651ZaYyXjr$f#@AeQo+kTg3y8BOrWBG1Nm=J5dd@v(*e|AnvalJq#mJpM2{B7Vn`fv?Hgc z9~o~Bh_WIgCB*8`f|&ysIWPorK3<&x>OS(ixMjtkd3zYlsFf?Y%ERVTYz z!(y!!HYrOl*UX>iC;OQmrONK_g98`bBHI`(2E9$E;)WP11)NJbMc6Jh9n)mK-d101 zwtM*eIkLsJ7&6aJNb$#^**eZhGtaLXdHXbr(sl)FQ7b{bPo|*_#769 z2dX?Dup*TzxhEGQkD6NiHnBGSsiD&7psQvVxw%B%IbSMq2k5S+mu0gwLPhi*NDPdd zV_p}E3cWm2ho8&pcYwD*XAsseLKoO0Br)p5Dk<*7s>2!3ksRk}1E6JqHh>Y~4021S zU1%r2e>s;nvrAXxvz3-q`7U2IM1Kny#C1+8GHLl{Ya!7A?*?vufnN?O3~h5ENf%O$ z)?10rkymQmy*3jwULRg;Pl@PwbhN?{2uyGaS)@zM@$l}ffA3h&1*+f)ekSXGnT$f@ zSIhrj77-B@Q#13B&GoB(LMQ1k9D^G!Bi`R};Pmvc^WDGS0T@6{;;#+o{9E7mONqWG zVURdZ1F%e5pqT?*6J1|2nqVwhCZFF2zhP0@a{;|_nJ0S(Bcli9>jkOMtNWtshN2;4c~bj zSq`WC5*s=14-)1o6E^#}v0PBa@(56wLGRC07tA;ZHstaTs3PP)cQ&Wf09X4{nn~p) zr55W~!XFj&U(iwzh!Q}RGpDDaz(dBcH0PAzdW)kWLc{3`?9HN0b9?U|6h{PBDf0cv zgnnw&h%f2a&s9qdfl$*Ol3x)aAS@d!hv!hayra{VW7rQ4_fhPhb?+5vI4%nGjy3|b z4%QROo}_C6>D+p1Qnv=4Mc@Ef+eP5lw$qnI00QasXAJ8t9b?ms^-qhH@MOaq#z|2>> zz>p7XNF%v0n)R#LiO<6jhKBM+8d~bvHzP(-s~SQHDW(Lm2j9RIq|RBc$+X)KZ|Hy5JUs{fmfBi*R|277-ck zotv&c@>+%>qDI{sxSri%+E>Bp7bLdP;q6ks(nW*K8HYtngy2<`Rfb5lmh6$8O*}u) z3xKq}wLY6Lf^39s@=Diw^@Ir-8RTP6!AH{2L$Tb85b7fsBsQ||`K3vMTSt+o(f8B} zh9=VM@Q&w!)frq#JVY6U9o)0^v9h%;u@~y1Qw$pe4M2%GzyXDk2H2p~z?=4KhbJ0P zdxg;s8GgoKl$bf!HOtQQR_>oD)?#Y}dI?q>@Ys7JeVn=wj={Kwuq;CE5RXfIs~QxJ zCzX)fu-|znPK-Jfjr*)7g zcx<`Qjz7sel{#-`-SENO+p?!nmENQs84{@qL-?&&_SOnabK~VI%W>=N2&Hu{&s;w% z@f}q8d$61Tp8lbsGMA>7;^sU0X}}Hnu2b@3#D-i3tINl`-Tre1%xr~Axs|G)pZNCK z+XJph7?0x`W^kYlo0ugt$fj+L4O#ROp3X#IeMopoN|i?llH#2`BJzIUB^zuq;>Zx-dzlZ?YL$htA|hWpYchx-n>L#ZeMXaS_Jde3k;m7b*hW4?a%Re2ShbHykGl>j zkS}AWGrbP{`91{#{4j5IHll#-rZa|1a3{d71SC>`53Ap*C|RW%717;&C#&PxrP*2( z^w|)``H;{g^q=$dHbPxCquAA?Nk=YJb1QSeN)jtP=D*mCdT-@o>Z%rDuLc86W(#;7x8jP;d=;wuDL-PU9AIaZBRNhZe?n@*mEW zfQcKB9dUIuGWJG#_DapQ8xq~mLHH>}}H#5hNxir5ddov#)5fk$mHtgianW;FbiJ34rn{Q2uGw}U zzx$pJeDcj^l{`H@EW_uv-mf%CuaEZbW*{LZEbmEH#MdS{Tmk}^cf=sCas?`ydS z32P^u#(C!2d8tlst&2Wv)Svl`Tz$@Co;ab`@7C(H!yi68QrT(NttqER|3GD!Oc+ZR z8aBh-_9vOBMd5GGIFdSpW5XZ}BT3FJ2W)A9n)#lC<(20A*^W+MC66&J@1HNDkI%p= z*E#{03Sk7#Se$%+&@O-(Q@1aM^ zk7uiH8gn+i+dJ@mgNt}BFxCq&;s=}Ckhg+95cu{Ebd9e9-I;FlLd!iW=)S&q66A?q zz$dFf!MZoT4#7%n^}L6m7*b5gGF8b@s^23V+ha)~i1UqXlfQz`NFg34%UrCzL^pQ@ z672;K&WF|m8?}I{=Bf-H`|&aE&8Z;rJgP`kK%eOk zrb8y{Pa2)1f%TLfx08KgHWK@F>vAc{Z`DMZXLQV9`Yq^>48Z{u_o8q_*QqKP<6*c= zJYAi~Hz0kd!~OX&h{zO2@VbAL4}hwoB}S#%f7IHYKfcvFBM|`(+Sb~(7@yWYYJdF6 zN=qJ0*{Gaoz*idUsgoXGErI?u6>VsgAYco?D(tw@>lCGlFc{k+GMm7(f;7%?=sGnC z&+s1f`B9b`j(Y$^s5EF~VDRgxoBu9~RS2==oT{Y<4Fpu)pzGm+C!SZJtFwu2$F2q) z(?y;s3`8^tz`H&LotN>FF2nW!`qbJM<6^1}O1?jI7c-}?<0lSB2D{N~KrdHzfV?b` z7*B`S4Qq`VKJBo6%}5tN`W3`bPY}M>Z32pNPAi@D~??y5z!DfQb1)S(Jd-ls>C`t4VRARt41|`<_Vu3`K3QeD$4sSMgG<4j_L;7~wj! zOv(s|8!qeb#zFYSMVic?VL-LQp1KWG(Oh|pUvv0f7w35&LI}ScF^=&EC&mbD$IVxT zT9jy&85wgGn}fyn%XqmPH&|-MYH*cK;_r?7Me>d0D~N_PjrJEi)5U>b7Lw zN0k<)ulcTQ&wSEg9k{5YhlV*BWF!%d6cN<808PM|gVt~;RJe2>X$o5}TqhDa%!oud$60kmuQRUTvKzkx z9urU`(9EiH)R4t>H&$QjzRG@gKViS1w$$KU-r4cvD%J0n)%q*u7YyLTF{H3;cDs8` zH!ZH=k6z>F{uuf#>pxudV`$gy58WayD9tCY6w29~gLeO|`c^UQT8r3radd!JVs{eq z&pfeEV4iC^`=X~pFJ8s$Rw|PJn`n#?!dvfWVzZ;S)UJL7rscW(T+0jdH8YK!PN^ZQ%rxRq} zzIR;Bz>X+A`X(OCSG`>`=%;J`sVzx`q9#I>wtD0g?A_4zD#T+)2bl*n``tRwTTAP! z@ps%SXZ}WoXxdNo{pyhc&*FGLiEgVtf$qtQ5LDOw=v$p`k6T%#iEE`oY}q&`fQTSP zzEUk;hFejM>xE0LzO9S%Ts;X^$m1&x8xm_V^YAT&IA~Mlma~ifo9cI>BN4`=s!7ji z-j_7TK+AO2nRtH|a?mLWq9z&+F;slh+he<*cn(~c45!?#@F&6~TdqEOg~mWNd%>ay z439md)|1hLrx%~Uq~FVpCRx$kU_8BTCvPz3%$sm@6A3|3Gy-7HY<8z39irP)2S-wu zlVr&4mq>ZVTV}Y9$j*;h9`bAOJ7CZB<6*#LM#a+mxSBrl9slEVJ*@lq{%PRs`-l}G zZT!lS+r|$%A%}x+i!FPd`0+qAB_{^`zbAZ7Y`|&>vSoOR)Pc06EY51pnc0YJ^wGCC z?B0Ph?|0xrfnkVAFbVq@Y=4m0RgtEI3IM%ECC19bEc@Dy;AXHEJZRBbZ-a z#CdPt9P(&&#B2cOJ6u~4R|-ji3_|tf7^YV%)Iyu2I#>(U_h3bgxf6bsMC5I?YVEZ} z+V7kQw}0*W(b}$?TkgpqOr*sWw0Q%nY?b39LG58fFzhNq;QWt^m+u9~7+)LC`S={O zw;%fOuh7*Fkh?0nY%(v2eoh-Uc1FjP5aDMjhd3fn|H+H%w{<4%GsXhCA`@6mF`-!)m6OaL?P8 zOrdW}1vNewe)%7v>TyUl_)XWy#2l;c?-66F?1J<1vmGbR>bgg-W9*UcGr}!Ay@dXM zwn$k4CDVj}J+;vlY02rYA2*b|yP@0hl!_q@^>0>7DwWeO_&_@F9(mS})HAV<2iCIV zpAYxuL54AT$~PL1?3B0QexHz}L~(I`buw0HMBSn#Uk1{;ZbE2J#eD9@ot! zqW(f4B9@BnUhOn{307f5W4F*mrs5DiSac z5AIj~b{k(w5Yj9cL&fBKbNrFpy%F@3oUmJ6L@hRVjAnylMl(Kh`Ei<@1YI<03JL~u z8sXHYOz!bR;Wfuy3ghgo;K`0?z7}~dowC9c;PUT;C8mlxn(3s{?-t&rZa_O?Iqk}P zWgG#JS3%)s(3<~*mY$QInk*b4EZ@tBBv2RuWy*FTMcA*_;yeq_lNG)P9w|MBXwUSP z=~z_^BKD?&W2@?Q#yuX+2?L6e7B@qEKLKk1Gi?Ax7m>Lw3x*El(an2oD3nz=WB?7o zc!KKlFG&t!#hWZsNX7O8mkj9TxlmndD_OI~ci`Vx*$}y~sag-^*^D1g^tN3~OGKtp zKPf_4v0s_~IPB)^$bEgqoKqdm9r-P`H}8>0nfd3!iF8ckol(de1E$J%uXGw!Q{DQi zFJbH=UqxiTASS*ZPc@#T3^HixIQXl=YANI#0~=nJGQJcj{aNo=ZWt|zs0)h1-KF?R zCnsk%i6&kkta^I=v@kj7zWxe1!g9--Z7=+7D~2PHSY$EvzV2}!0O6A~jy5MR99ev} z0}^hOzzSIU*W>cTDS^LZnX5Lvct~vU%b$ojMz5G*2wz6?&E=oHGUVT`HU_X$#4Ass zk=y4xJg_*;%&v000dDX|(4<~diV^nYE9cp7&70M~Sq|7&6pL<(cUHUE-(EIxLVtnl2M|YOVxLUJ*__Tq_^>BlxE-0B^*!G z>J828h{Z-9I&R^6FaO1hAsn3cGZn#dTR_chSg9kJP$l`h zO~ohj1f2G_=9HM}Q#FH}luN<*ocK-tky@N*v5cH5!yBsQ?cD6-T;TW&`;J@tjov>h zAvm(8Yc!9U4(xfk3Q%~DhV-p z8uD5SSGD&W(Kz#aJbaGyzNQBRxhZTo#$tmf=KJ|^R?l|i*UGks(o@j>hXcG6vrD-- zrnmVLo1*;)QV#VQwZd#nc*z$<@#bn_YYLoR)jJ)Pz^%hpQp=SX_UaW&-d4zri{d>* zfPOtE^h5&@i82@3p0lwwy|{9kuJwN7ec5826iDs-`>0@H(sw_l2-Jk+k9&V-B=gX~ z>_pQr0(_x8+z#KPd{MY~H4!*)cfi@*>-c0QcirJFs&-@flMvF=Szat=M}H4iUFUCc`s%5J(dGl$c%TjTiyuw+wpgnj5^^ z9?eMKuLJaU9RRHVR}W32Bhfw71Ugda=so(I1+8vY7SIRTTwxMRIJ?7Fx6j$I>K)-9 z=~(?r(bBnVlMe2RkuF}Wu_GsW)iu3Ez77nha*y;;n*4S$s&-3_p^324XSV~twwu<{ z|2eNQvIw=QYzwv;I5;?dP&R3L`fFz26E3O*Tr?(!LP$qE9AuZzIuryGM3*b~XM3-`=3Vw~`-# z=byrUkavP^cz=g2`VwseI&77J1y58q3I-X*w=)rEw<2~_1%3=222cY&^s=Sw;XH82 z8@7>aK*A3CqjJVU>Bv_aCoy1Q^|Vo+S=FP@O*~z?j~V z)XxMjbqIWeTuHlqW~<2E>0OhYo!pPc827r|tZJ zOY%{_c9dNjCJf8u5MoWaFj(xEBD}LkI9_N1TluAESC|~Jl zw=rsNFEPLwd;Rt7YqK+w?5y5?kbE7YJ-UX@UT^IesNQ$1=|#pd(4u7PLkcWe%$bG1Q!-J{_;PJo zQ4f$EbLMN$0tr7Sa=WO%nbdAuK#;0xI={eH#-#yf+v|;uiUpn2rn#KywoYqA+D|}% z5=H-$IcpO5Auq0}{tOOQN6Me~X-fV?g_zflKz=T?o_)?PlweS?g+)F=Tg*dVw*Qd3 zb*9PQ`&sq-M+B8%0?BEoNhg@*+i^1Ajr($lCN~)Cc)Je_!$onw#TOvG<5nvzUAY*S z$d_F{t+j<#0i@=n9rOR7#&h7kbh3dn@Wy7*_P7~DF{rzW$^YLNclu!NvX8wT6nsN7 zWRd{F6TJiGwf|U>UOxa(Xz4zs!Jj>#^x&}Q(CsSkzNg;kIQn%O$JIc${a&JDSV@JL z>dj|Mpl=wWdnZ139f9G0uVj93&L5#nvJA;Wp)3LSDkPj}h|~G7`~9sVjbK3O3AQ@<%N8C;W35uJ1#diuFH{`&ssFVmTDDHYqv~ zTRgQ5J0BOvHsfGU38Cu2BJMB`@_LkyP|l`8cfD40|1WKh6Ac*u?>+0=Ts{9J3Wwj3 zlkHNrNYDI4{TgY{{w&s+Pf&sKeDg$ai5})*{fu}RB=7i3sf^O*K(+vc-!r0Am z0)4vl5Tqi6SM$ag-$oK#JBMyCASZYs4jsYTOY5zqtkstYl?N45;y)wZCz}$vjAO!X zRVKRiL$=6i<@*>UeWdvGt>VVhup)dFbv_ep60t|adzEVt_ARvd0Un}}xW#z*GxwhZ z3UoLfe3f#fT5iOeGt})NTf&et@%G!B#~aS%W{?bV!$=W~QF2P-(UEfF{QBj&iv4o9 zNpOTx{hju94X&?JVv;NcnrVEq*Nn4IkusI=%V~_`LI_SR1q`@(WHNlS47B^osY>Rc zZ(s8mxM7Dr63J=GZhC4x%&M(qe)YyL+G>D7)eT|x=-i~(P?R%%?#-YvM< z?qeUf0pmLYUy6#qHQu6x>U|Ehb{ zoY;W(tO0jGemo)833z_|QupcC!dkrVak(dG`VnCJR?tK&=M+}lg8a^I!A&2eDO+>| z=-z2sGOGsy)X|NDpHx)|FT4eOEPiy0*!i)kzL>z2auB9)+j7m0{LTG7ad38i2m%cv z@s@VOc!=phSkz+A$M4z;WeWEcRusKT$$vgLMj_7~jye0FGQlC_>tn*2&W;zqjvsW6 z?_nDpel&5?uOVLe(t)x30>1I=%tyaHzg6I@y`090@c8=T%)m+h%<9qtntGcIlYv^B z5z?M=TrwPRY;SN@pDm&rbXNK3Do{?eNKf_gV1-kzI^PYBQJqc{V|OIjXjS7H4f37G z)RjHIWdACSZ)7iBNia=O==tOqm#ia&uPhn&g6#WFJ}x@h$BE30?Szr?iEF-up)VL` z{GhPo6JP%LCs>c9&Y?<8o{f#-wXej*Tg8Q2ea`~tM57DcesiKws?uIE zsHJf^R}XSwe+O79rjuir&{EEVI5i8?c&Op;dq+QTi0$Umg1#4NuCqlj9+&cQfjh+T z6H^u<-zxRHn9P0{_@Hq4onwYF*DwfOiy)OdO?^3VqfqUaBggbzhIQGe0oqU^>U~)1 z2fFpvgIX9T9hnfA+`KI+B2J^cIJ#0GnxpTVH_eHRknT%_igRzD$AlrJ{f&Lh1qO#K zUDSrGKC6bN((@;JN2Kda0-;@5dz@YrNdT@VT}mZA^>K1^r_iHK@W$T2EaPC)&!g)> z)yjvf76w(11ae4Fz!@&>-`vc0;}?}$n+yEKg(g$^>5&!!vfX(R4CoWRR@Lw=)0B+l z5`RRpMfcKn7*3!C+vSSySuO6p1*dyVWvpDv5I3oxSLHD5IvZD3Lm+DI?_q0Kb+b+XVvqig#!{#th)ok2k4p4@rzU+@_c` zO8@#_*Fz~6`*7yp%fqGs6j;$YUV2W>lI4bj)_)Npb8dwS!lmz&vK?|=A0%^M$GZ$P zd7JtwT&?{M;(tPRo(3$|SS4EX{;<$UR@_S4V%dL8NQrPa#=d2DiH5;JL?%eP(b>TC z*ZH#DM{u_RFf~3q<+|67lp7-QEYvO>vEPXJV>}nS);CB+FY)}&+b<7!3k#F~@!HVm z_`o3R77JPJ_JSVjdwtw*MvJ~qr$7*&Kc5b}0{tVV{etmQOa4sbLss8?{bflI<%X75 zn$=Jz3QctBfA+EbW|zWil2OnLb;Isyc1z#>UeE*Ql|!459*J(e^dBO>-Y$iC zKlFu0trt&J-lOD5WrmWNo?Z8HGmU+T5H8liDMs2Xo7pY9$C>n>CmASFRk~(9r4F3m zwDGS|k@!$Uv*TZNDq$2SKlxE2Vf{rNK+3@7(1yY~(=<B-_1N~~YiBTU`QjCoq^)(JxhQC=& z+nHn3B6Zw@a3K+qe&XKk3Y$Bo_apHk0%F)Css;_R>S!M=#49Q^7`}XnA%ce6(qKyX zTuyjZc32KXa7KgKRMW!tY)vP#_0@Ve({BxQViSay*M`d)v~1H}J?ZsmjSIAq3-#SN z@3r#@rEbCR-L{cxAK0;fC}4Qs^C?q4gc1X3{@~{TzMvnmk|Oc>YV#cc^*nI|r5RL% z#+GCIGq#QrSbA$B&yHYmf8%3X3dbJ>7wj}|g zZBKyuZK#fNrC)fd>0KoyS}QLPKFvWzz==f<=EVGl?s`VQoW`3nB?*aeCHW}DUl>}l zt{GyZ)S_&d7c9RGY5>Rkt#yP?N@l@tgxa(;*G+}PVp$;FzF==g2JS``*z<6?XsJPzC-F* zfC&Ya;w4#vK*O~8=9Z>{qwV!chf2PJMM6b?MGVYqhB@~OW;{Icl7z8j z4fD1`JlF!AO=))^BBgi;_4Ip(!^wX=aP5Kj#?@+_(d+!i)VNNT3MH3+vYVfq z;ECebTsha2^yL`F_gn}VedH09p*kPe)q|S8GTjVf4uqL|Qlt^-{oT5j=qRp8{>!d% z;85IH5dHc(saIJC?_+sR7AZJiuD#$M zATia$lKyk8T{PVO?AbuJ*@e|V7Zn`LA$BlZBb)V#>^?h2G>maKnKHt}Jwt(>SBdu4 z=0*2q_d{KX)rNEm)Aw_7iE5TYV7A~%M5e%pe|ytgQ-ki_moWY7&%eHk-xT5}f&fE~ zk$0r!=^GjdfBej+mo}pr>iP|~nEZ-->J*D@R>{ga5|2L^8r3fxpTt5c;(-z>~|2{veQRHpsvzS0h=UR zeD_*1$vS*&nz2>5(BRRv!Cvm7UsFQ<1X{>X942Clb=5;^@W%Kj&;9ws!WIF2zqm48 zYDoVfFEsZJPL8pJ)9s{d!3Yo9Ij59GTWKSKjrEbSMZ!SMM66Ccc{liK&7g`se~~-! zY%!Rm(?x(NqQ-H_UAc*;*}KT)S7&m#_7~leU=(4z5;v7?Tuz|j;~|C(U8GI{FhViP z18P$vmc>>1qnc!PM&9QjqGovvSqOiL2f9f&SZNd_4AQBtFE1S<8gePP=9 zbCR7;I}ev(g>5qlUHDsY#E^m>^L5M zel#n`G=-)iY`~N6(${f^p_Pv7S9Lr0+pCbYg7)HGyRrIDk~PW+c?|-k{4h5vtC-Xc ze(hv~my>B%-*de8szbqVNgPDOXOzvP=hGHsp5e%>S}bgVR6Wu3m;DNZxa&4z|9TWA zKt7_oW6z^@dc8MThj)G{%QRKjI03HBge57pKXp+MY;N`daCscPbcz~kz!W^2801ze z`vy_@1|Zf+3H3UF8YA=oS#FC15h5Q>jp9V*X86!p{f~8FX}!_Am*u4Y)|gE|8Ybp; zk3Otwb#~DcLwpQO?A>L5aKhTSqx*^AZk@{y@6RF4Ol`){sT(OTEPAm(aWIFfh$nwWJBhyv@eInx0*i@?6}Qy3H1DK96|xq1Lu5cg=DW}c>#XwMIqH+G=LY{zi!~;_%c$PS`k?-az#ZS zwoowX(me@Wh{tygtw$Di8Bb8VZzoRIXDQ@tTKU6OgX6jv?sj_KOz}fBC3sxT_jRlv zt=~`*dY;FCc#>0e9{#`+-#KkzZGHyNVt<0gLM6U{RQw&FQ1vcyair|FJ^J5!j10d| z@w6ay{YRSe9cm)<8oO6t=mLa#&S66k2e3jOSbz3bez-?%5Hq@rXZF=c?*Vl&%(6({ z$$h@DyrI&|+nqFfC2h7Zcwu1IkfEL~64hF<2_ih5DDc7Exe#r5_vS*u85tD;e>jA0 zXtTb+ayWltJ%}oE`!4^F_Xuj-SpV@Y=UI@f_y^N(J>Tjpv0^ zT1z5j*CqQ5@aA+$O>wr{dUa5ECuQSxXWzQQW-)?vxe?cpQ(c`;+Ez;#K0mlLZae>=0m1WY86aByy>;650#KsF5N-@r>OA1!VPu?bh+h=*j4{?lDhDuT-5UvDMW@z6ORIf0%Fop;Y0fWGbGYUo;!xZiW%sHKjBGn>ScxKC!2Gy*cW|5@3dL4e#l~!ry>u+}h}Y z1DeBAMwL!^B{HBQh=MBQ?RaB}!sSq{>n_7(|4ffEMkuX*ScvgIlx>`Hr$Xgpm+4A) zDhuPv)xWQKQ&-e{kJ(>!%~uFexZU!){tzjSp>e$DkAuHym(ap~`hpLRN3hQC;%1EqU(-eFS9}a}!QD#x+oHd&4^Q28 zi6S4KP)3Z4!i!2-)9rt&dBLJX^93-kfxB}Dd&`UtKT?%BV@o$S4fIn3pvUC*|KsT` z*1pfon=a@B zLwpwN{qD->hxO?l%P6Q3O4{}iF^kjhq;zm+lzEq(?tu!4y+dApMjQ_V2bez={hBL) zULvwTZr4dcKmf0!A2Id`v zAIAKcWowC9GXJuTg$T)f%-$KZm^%n;Y%k$|=m>1S{kFaM=$jBMQYiK-PgB7>a-+mh3nff-%cRfHEJm!iDWjZ?+y`N%~dRQAnR_thqY z*hu@NUcPnO*3f=%w*ocVtz{xJhTMO6Eu6J6Z;qWsx9Cw4V>qW8eu20daHiwEkSX)L z2cPf;yxDi?p__L%O!XQL`W+JRE|gzjN?ycrYxwhQ=BdV;v<+_reS)CUj~&;$7=0#+ zNd5CU`yIK~+s}c2yl5J@nMnMJv9GlNvle^PDhg`le@u$|6@*gG2X`Y+!t|rh{#yF(b>jskf#S{F9DhhLY6m&XmcED2hQ;Masq#~HVAXj0(Slg)t8PG@0 z@U+$D>@$gnA=Z$c3`7A(WZOABIn$h(41YQ1AdSXVJ*@Mzeptz8y;|Tv>tc@}eq`Xgxur>!|lpl39NRH47A1`>MGtXAIdTs3y#p?`#$=Hz7jjqydq{J-}qST8_ z3QqC=$X!0ib3)4sPCQY-779ceUq|c5Jw;H(iwSLFO-TDm-92iU)yZmm#!up#Sc^@I z1(d0|YipK`lQ60rEm&s=$>alHVK?bQ-=e%29FDzdvifPwdu8>NDsRJnq=r#j^rkZU zn%J8p3S+nAX`povEQ9!c{MhspyCT=_CavuE_Zf1?^(eEM$@eXErJ3U+5SQhh`p*H$ zKNHS)dq0Dk$!6W`#H0OS)uDwxh(@Rm0$BTYv-|fkyu&17gAN5^A&@S;yt?MLNq0n^ zjGtjin4Iw8UP-DrsbgPesI5<<3}6%0G4;Ll!#8l>H`AqhSqsX@Nt>9f?E@G|Qdux= z5B1D-VdRQk(&Z^KF58NCh%BB_XUG$tz9>uFV1`d!G!bX7hUXZq4kMX1;Y8OfxAIz!jlKE6vgy(=ruH?SEbZdt&;#?w6UWsLk zF({fJ0xi^AV@rG@$vZcfl?u$k1v)CcTTP%he1fkZG&pnzzjui0*Y`g`R@@Mq2miV~ zAiN;nGA_*u1vF+L(2p=%zO_wtn`HmH$F_WU7uK=Zogs7bH6L|v=RU#JL5GQN&rk5j z==Cw7*O|ZxaH~ufZke;{wY;A0*zT2j16mZHdlwTH&a**^^!(d9M9~@MuaJcB%yH&F z5T_jjT2mqr1`O$6AD(aLji3GdoFVg~uV?Za2>eY@{p>-5H3p&_{Pp5C=A>V>}uJy*e7%xv7RU3OZdP7^9Y;|FmL zrEG7WV$Ld(h88G@L2p-&WxLR3yQk%0ygV{$_h*67e?v8HyM4558-$6ej4bf(PE>wG zkFH~sKlLSOH}`r&QqFw0@H~mU&Pq-Jy8S+Ory8=$@9nC5huezvJNmChrwLJ~XOad8 z6j)R`I_AZ$dM9`kP(hau5e}diXm{@mS?Qj=Med7Dd2}Lxch@!&He9fv*Uh?s71IG4 ze}tC}_UGbEignixz}TpH(1pMWNs?-{wX4-q6Qn5~3@>tDZZl}AIC_)2ardv=nsp5} zsP)~`NB50(-`nqGMPm~v*LhWOi;Y*90N zU+<4#lvuC)JTDM;(V}-Mc&-I`^48KE`x{MT*XPo0B+il@2E8A!L}a~qCAfX7*ItNm z?lx+2j{X#8#ngEtN~N?EQU0gs|7ih0F@;Z=KD_uG%-3lGtT&QoYV3^H7(`PrZ=DOw zX>0r&NMOCaO0aBqN*K#~q%*rrj%a!J5vTc@I!Mcn-ZkS^MkXwS_eOg`R%l`@6Z+ zBSCKj;xlMpyN!%*<}?!05-ELn5)?{JLlSxcT4-!lT)y)w8V(PO#$UzG5Jaxf4HRAf zgFd1Pwij6a6pGc#ii%S8W%3JIv}3f>@`#=U9^hE&G|#bhq9p0b+#brV-1%TRtHUCrTypIg8Qq6?a#{Rs^R!q9@64oZ@cM^oFqK|kZp&H zIbQL3;d@n*DwXEHH8SFGGw&OO_?y4zBORCny$8J!$oUx*QHPv~` zov^1C;l&9Vj6JzpsA&l?3|~rHeY#ofdNT1MFoQU4l|8aK^zvtzovO8@jJIHZ9-Mc@ zC;|vhIaByHOJ$xp&yMgyacbA#Q~7#sx(X|Lk;|tvm`Pb!u&ZLoAddlhvb#NP?c-yE zz2Lzj3BUSs)0>hL_XI`0g!LTThNm8L-9=Aq1-LyQ;AOG3AZ_k1(>kH>FyJz}C5H!k zs{9m`&#_N3QAs!Ni8<^YrQ#u0%C&ScJ_Vb13WC$(X(j8hjH&=#XF*2e+)@7$pkw=kKN*$nYJymmLccPLFT4t??1W=LLfFsSsm^4U#Qr zub~qYI;ybM_ph#bWDC4nC>P(H$(>soku~+P0Rz)MnmYe@?m%(M-MDxDhk@Aw{Gwf~~n-tAKg?iDSA3Gp3|! zynjp9Q_(7I1RS#s)FIq1bU)#Xx0|K9t{=!vE=2|`^=}`x@s1uV+V%} zN7$a-W_jcgTLp=S!}mXv$XzaO1B2J;{Tz`p1_2Z^Xe>Fkv@2ecYP-;jk4lu0gVP{d z(A`7vJyFBQ>AA-poL$LF>yWSyzF0^yl*XNieKd+uXH-n5dRHp)TynUB^kIQ(p^7fi zIXP*uf*}6kW|i#)-2=q_CHp#@5RQ2D^g#mb6nFnksu^ek9T5mopY>K)qV+#tgb7iv z5|^5?zc0cTY1+y&k?DU;%zU?QdZ&)n7+_dXBD)QD@a1R!!<&XZ5!I3H&hDQK*@3Aa zK<}fjxdLDFYt_@+LUJ9Y3w;>@VvrQl??7#VPRjm+JUe@a80_{N7)Uul@( z7uWSZ^qHf+=*c{VG{vWK2f!kG@O*Fx+pzJQr9=Ha(JP+Fth)LDFd^XAM{${_{3!v8PN}jz(h&P@7*ap;#rdpCU$-k=fmg z&19(aoe5qUzIxL4ZToP}=uw3I;MN7uAA2v2f_a=yhh6+ZO-|k&2Iq~Tcmc|YZS~Ns z{oMk`(VsqanKf>)$U#oUu~PhN0fO`EQ(2D47+Y&?8;%L%&&>`n7OC|?Qdgvt5fg(~VxQ940ZVou1KGJEflZoCB8 zZ@5>IQRkO@oh&?mn?LcYc{h2LSdGRj2z&6?h&;_@>*tfI>(+Uxz~A>z+=z4310RVf zU{6wF%}2D&BXV>z-zKJh3fkMk3wXUd{GbF}vnzgQ#yufE^Y>yva>W`^Gc@btq?Esq z-*D(BnyaF}q}*CYNx*uy)PD0kxzOuOEF(%hZ2B$r)#a!l804i8V|c^;b<}II2g5m; z@?L<|Z&2wP`%lfo*7$cqd!qI^hqS?j!%VB9VwogmvDYU=6h3ZBMylYgkrlFMv~+OB zo{5XkMv$L-pu^f&2|AWX93*wc<_;Wk)~8s14AH;g@5-0^P$rfsk()zPUyQ3Bj4Pi` zc88Sh++~Nf#N1yLUPdbQ*|6aNH5t1%c2>qc^k>{mSeDf?wM1rWgvVwX#$^d zltDz+x$Xkj1F}T-`BYu`)LiOI1p6}_aYW{$y$%u4?)a5I`z}B=7k6@`qWl=>#K%H< zPFiYb?Nn2T9CJ?;n8wbLQrSvL#^n@2EOF;H?nVvi)rT-9AdEs*L`)Rw2b0o(^j{cs=HYM;S=+l%}Zj(W2rzzRTh7SD5tKN zvd4-Jn?YRsh2^y4Qr40^+`Xs%V2ttOMu|LZb;je?4>|Fj6kuDN{}k^ zkw|L4?YqxY9c5Efm#L8wV30fwB0hvKhn6Ncs+%TI!s~RifTf!Oz1=VzQX;e0Hgy6XtufMxx`_4xGFcK+Y^6pkC1Y-ZAMuv=4NC5+~-X%2XH zQ=RUtAUa1;sTDW>YuHY$1x*ZJeB|27gx!x)ddMt6QjSen^;QW!A`+K48Sj^>pDL1j zTEFX-z`K$mu&jS*l;%-SmW3KkxGMO<-UJ z10@BZ5#!T^IK5O9uDkHHRg){^-wLB(i6-WY3%X_fQ*OFAI3MPoqu~7GyG2v1P2lCJ zu*}Uj1mhJ{k)X_K$@|O3#o?^fyAwIKtDnHHQBZ6Jm*6o-^1_%(5cb$g7+qycGm*w$ z@XS{Ff?LxjZ`u!b$q8|aWZX~@*(_T~ta9LHCG_rP(hG`$%<<1fDjtN78E$iJ4T`La zt@i{ZoCuL7{j4)+xJWwF>_S8yz37-sm2A53Z&iHi@2P(M`45?z;m#%#o5bC4+7Di% z+O%g_%p%QK6?}NXllN2=vzoSX@eJ_7?R+O)8@2Bjy;T?Co8u)Y;@Z7ln4!K65zUn} zdL`hXlp+)_LWf13*40p6RR{)q4z9Nm5ZQk^S*1B^O_*a78@-oyDvniLO;}9FfsI#eQ`jtL6^2C`*wfrj4x+^Zm^~l6r4+!5O-)k~&SI>kmIYUSAkt++JYGQTfElM$LhJl%=rGiY zJ-~j^jZ1?~_K&m}KOXw^RnzmX7fV~MKi=h$RG{R=!trCu(cfI*0>rk9$tIVa6;pTK zxwIMp)^6yfx}Cso=?q>%iua}K7Z5IZ^D{_NY$4hh>teMseD#C(Q6O8w)f(sRO-?;4 z6<3=wH2U#My-}-jWk*O-pbru$Sao*BllGa`nxqtH&}%t#T?$2({B-c^|E)HqGrZGW z{3;YS#3{TlcP0Dd62-vw-9GaX{r&5^jU2(DNFR0fLCvsjZR&(cK`#MPawh~3K2QGF zfBX@DgBo_(B%HSGBQv+#mkAr?c;3r6n-KoVZy95WjtD_fL1d)lJ-ZJ9Sux1$9;uWm z8l?y25*;!LdA5o%NfYZEeefoQByGJR+jf>nPYXFEOGB1%y6P2v_{B$7tlOFM;kT*Q zuQJ_q7PCm&<0gn0Xbnq!<@(G7S2TG)vcM;gNJjWCK6&I%#ZhNd$dhNKHZn^wV>ff& zn%#2N*rt52eO0zJ8kEuh%{h7RTKw1O=4;Dc0lnQnQ2H|p2YK>8BCejEthAO&8pxM4 zzAWAiS+@^V*WcXm;ps7KJk$J}fv>QkHPT^`A)>@PW_44Vt$Hch+05x9QFdbA+^SFdjIy0ia9vs^A8MA$R zx-p)j+_W>$71WHQTwgz))hzn{JwUs=qkwVqcvJ7b*=>QhPv!uYw+UHj=q%~-`cZ%q z5p^_=h^0oJlht!oX)l{^!;W*e+m7Q_BC_1N2V&f>!Z7;UIxy#>ke>2q_a|(9A3F`@ zbL8qU4&0%rz~(HR9TU?#j4PgqGbdr)ZkBP|ZQyAY}N6 zNsf~w`b<|9Cp^UZ!wu0A&o-7DkC{@^Opbj*kx2vJRf@;>hvMdih5squq6;grJ)fPZvEL=`Ki15p|*FX9-*e<1by^D-$}4L}?iq;En@)6h|d@a_~> zLqSqkAMdNP9d7`0z0I@UxCjTnQ!q<0ti^5YI>tk^{{+UFRKFY#A!tZqj~keAq<~Xg z24meEvk|{>4og2HSOYu4t}1L|M4w|xuoiO=34lF#T1|ql!*%&ra25Y4Z6lS5^%Bhs zcoV1=3%Rjx|G@c<0?7IX`3IJ(AYRUR*<5AKvDl*(XoET5h(Gmu=vhUghYk5WvP9m4 ztnn{=x~c3-pCA5oC=t3mFY>^Dt2XlZoWW$}zr+P5AhHIz3B9PDT552SHmkutqp3$}j+yzwjIPEmwJy9&d zt>7EmI>_=5`(K(x_a*YLt`)PK7xs6?r`8Gc5%j=X&7`~yN#18QgPC48gI6ch9&Ile zg7*Z7`fq1!+9!)rQrIMX;S%>63Lebl^yW}>_{Wp)0i)qSCIWYwhl+3ouPT~XC$n(k zBP`_+^PriZd0#Z}XriVyzwhc5wn<;To%5tcD?dR5Unauj4*fH?u6yQL$Jt2sVvm7! zDL~{7{t@zDQwD@b#2gzG7;|p)>H!q{^TnD<0RMGRpj7S7qdecWAO4}_M+KaCZ@QEP zdv0W}|K4&0#PY$2`y=2hDB#^0(and|FXTtS$2@otH~}q?5*fBaa0C!gB;Z;*yel>| zO=np`>fR#uKg86}rd%S%mgOi??n7z4MslC3yQ2*xJy{Ye<7LjFnn#S86EC=;hiF8A z<_Kkm)>koai4JfmcT^9@UBx4IF*9lGjqD{v5dO3gR$r4ZYDTN+=Qxh z>9bF%VcF)_>jt0Yx~bf@per!^G9s)URE$CnY9M7rMI%O{7SX6Gvh2D9tYM?I)dQ>n z6B(GPYiJO{ck?ZcSaa5xqq4``*bD?r=CKAyQ)T0sXL7?E|CyUnoveLtlz43VEB*}G zlE$l+$tJ+~@H0ymNRzskE0Qy$RzSa=;fzJOKDi+?xLoSVx`X6(FzP1wFr8Ma=u}vD zSX0otCS~>fh|%kyWxk{mAD*lD`3{yD-+vgL0_I=D#%vj(`CY#Q!YP1Ulz4O{_dzg>MAUNfCSqCSjkbdJOm7$2 z=JjOxaB9pdJpV(^+4^rb9X|zIt!S!WFY*p;hQ`;7jl+RG&C@uG*>wL)R+qJTI8UCm zPa;%Lk>)<#=D-3V;}&IH^xoF*0sTWO3TUk0h|c49HUzoep3=r(e>}%S;k2t8Bjh9Y z^IO%K-%ywbk6<14+#dR!)cg}uCE%{)>0QL(T8koUwbG zi-%lU5afA{waRS8Std?0Txw~Kt?+qldfvlL2DVPVE;sj`gvow^l}wdBS~lfW z#Db#B23H5CXC`5~d}k8$wV3snJk^1h4vtFfPb4n^n8=B-yf-hL9xv?LnAQ_zo8xFP z$9g!oEWB?)1F7Dz4{i800H?kl3C^B? zCL*dN8-E?h1Hd#9@mL5&5ii|+L;uV{j*0@bw*a zv?6=JLuCqcRFlMaX$e`qTf-BFUr-Dt*0JHw;#&9-r&igM%;PIq{htjK;E1Qpw=u<% zno5J2_n6?~^r;j$+>rcEy4|Dyjw0`umVeS~*HoH9M)Sc{%NDa+vKg9N1Jn`w=k01h zZMx#=s8Jxv8e=VXvuZ`rfj2pL%d86#)f2E0s_1d|B^#FOg@y` z5pU*CZAUTpZQos~>Cc`0jL)gfSke^a_Ax+d9J3JR7KbzJFmOFH@({FcQ>L&;27QvE zuAl{t%$1Tf-MY-Tt+_XEqhIaiKc3YWgL^ScK)SKs8(F> zl>|)Jd{@ThWFA&rne!(EvGy=YNRIxk*RgnypQ(EKm$YF4;l6!589BrWJe+?gVUQ&W zhLlWk$k~Q&CMIdx#HN9`A1>!V;C^gijm=u&%;Ly8&*Xr{lntgcV|`G>frXw-k;S@g zMi@YXFJ0VLb+|iRr*#;0vFjxL_m~SZNeyA(|50X)6s~jeo%J3uzEfGWmLEIt}XlWdjg$6=M9I=n%MA*lxW0Z0ovRmaKGVWy(4EtTSD)^n13fj|PE)J?q8#1MCc7~alWqc)OZVsIf(~2=o zZ&zMQMTWNY>BoBgy!2plL6(L;*;@EuUW~crS!#B~v0C@*k5>Ksilz15vP<3_Xcth+ z^?9KStX%xyOO*v%XZBThBM}YmsMq)IUE$#VY9H?H@90A%l{%5DFHGo#U)Wb-nZpU7 z4u3-NGY36kjX!|_TzjUsr+HneB=MZ!`4dm_$H=26I%E@W3RkBJ9-nz*Sf20CCxVES1EN>0DMtBUsYLu) zJlp)?<7|_&CBJL`08`}|1~(V^O1V(ONded=|B%YOt#={!Qhoo%_ch(jSq*WSD0S%5 zz`zSoY+CZ+s|udb{mBbU9%-`^bt5Ne zOPZrkH+S|o-#5FwSpoW@LD|7{X9hK4zd95li66|pV^=u61lkF`WFabiVd)tgvKENghJfm;fgC4>^snZOg(gN zp7GRUvF`w|TXeqpd#kOZ#pcupGc+J8z%GT>=o8s2oj><|WK98m3sC;7*u%heMMHP7 zu0O57u4=pWSo=Ed4WASBrQ-!txpAb7R)v7I0g&F5cu9WT^W^5INUm^Z4chP32f;Jv z?FAd1h;WfN9jIv3Io=Q#wRv&M1@7~9Qr6%J}siI&mc1}9k<7~6Dz+(`(x>iz0rvv%&fqAFD0Gpc3u$KCseE-fK zsmPQ%{8s2f>ZX9pP0!EifUVJZNWAC$IwIO_1UD3LDW8nJFAjOgTP-VDnf^S*^gZ8D zHpk?VcA8iTwug!B7rUpfJSnX{a{6k*QhiHl`)~12H3w~|j|}xOR3bC9CCDdvYoJ8m z%s)AqQ`mq?<7j3dWBGfycN%{ep8r>Oc!v)kv3cb`TCi_8L^Vjl5NAVHtbQK*%#ffe z{oxVE_di_k>#9R4BJRG2bGSYVWvm4~rrg;6`pZ*D0>&4;jwm1-|BoyB(pZJbgaiKd z{M&_o?k~BqQgd>79GcL6xvv$X2Uw9~MjeMHWrZV^NWHc=_>7mqg;^5g%fF8j%5b_x z^3I#N63?HDIZH0ZuSVh17jN|7dy7>7I%#4FJ3@JCsrVCq?h+hrj-V^H9)bciUO3$f zz#H8op+BAHUpOMqcJ$N!@}2y@aM+cqay#}KH!+bT<(T^T?TX8bla)C7S})a~q~CL@ zXF;;{Xk%q|jO-^&MYr|G{xgTvyqwts((>$r&BFMSMGuJq)@Xh`pj0Q#NMCyVD>Fa2 z+W3@bBWtw7iJYeqRZs3<1n?FEQ{p4v*B0NDaG6SOW{)=iE9a?tn(#y43dlE&L#xPNSxsjE35BhNBOs#OiwhaDP?5}tT9$s~!#}Mp1Cdsg2>W7&;Hr4}uyu5zt3ef`;FaY>11cxaS zQQ`fq)Bi(U)UMJ+#ga0R8*Tja=B*#!Ojj(q)_l6xvu7Od42OvvS?(Dx)5vCN?ZRgZ z|Bnf%-)_!}-;`pGnPrLUjqT61jZtmvAWc%OfaGC6VyL5KegI7+)azr*ah}`M%`~!D z?`?5eY=_~%`jS`c$oK2xW40Gf3UdlsHF~d_SH_`*3kcV_anSUBJEE2q`1Rkz`Zp4! zsJN;y6uGpaJqF2+KPP$n(eR2PZoPYu^a1Gw5aBpNC<$3&|$gyQlh8gj@-PS3L@S#QHHi6<3T-Pd{K|AF?tRht# zm@28eo5@k27;c>O@$l#WTMHDA4YyUY*xn6T0%#REngIHPTp0(gqPeT~I47{1C!pmD z&k?VZWoR#q`-5*OkunPr!tZH9;EAw`nUpWVz;Xg%P`GPHPnPhblPU@(&6jW${F@#c z7pp!T#1~rW=fREaL)SU~w-`OF)oqDJh{bauj6IdKYa&dyPSX~0V)%TwuUbgrf8Sx! zG2{;tmH=HvE+hbd(1YVZ>L>ItZFw&6>jwHU!uy?JC`87L@!d z#(K(``}Z4ahsM(ZzIxYAUh>DG;^NW|Joh+NjUR6Msle4%3jYUA$I(eEAeA&#BApwv zMal`xp7!)hje5XQsw8+_)q1&$NYNWZjod_mXS-LCYBwAIy{E&0^>^4J7?mvc!?ylC z0-$>)0n{n!&|SB}`UIJ?Gq65_7hn4and`kp`nmQmoOzR|0D{co2#8p!%6LnS8dZ)E zOJg!&s&3U%-=<==zkjxrx1%j}N_k3~%^8xNb&XBGPrk(-ETJ*(DD);%8l{7k|DPb5 zJmeH0XmDTj^}?BPqc|6UFU{SVwqdN0?3A2A+-khH^<^k!u^IuM|LsZ`Q+9rs*@On| zDtdMt^as6C!RkIb1$KrY3tFH^%E-B_Z%TsqT0;$cW&aJ7Sj6QFJmIZ;KJ8DqegAve zr}{AIneRuBo|xM{1ly=siBPF&MNv}a=!fHWt+sgMKN>EHD&0}RciYWWUNvj^k9#=h zy%yb-oc}+N-k~@D+bmM5RzwRcq#e=XO7aeyTe|HrUmMCel=DA7u|n2Wwol>kKybXBr!$xmyqc}>nbYb@WNn!Y*r+#O3f%N4X)BT(8trxsp z|Mg-2Cy2x(xT-GntDh)&Wot>t^w{!w1e!esxyxa;FKUk=i=qwhKVfQFA+3lkR!G}_ z4_*v%bR1ah@s=8EtIGvuehwu-ae?w)Cw_Egf?H7tvlQej7!5)9_hb-Sono&R;^^)!w1?FOT*>?{G>rxcvV6U!H|y zyQ!o+B5s_0eOogZP?TS&zP@JZB*27nKC?UIIQZyO7oGc$^I254zsr-SK{fed168Sq zIWQ`QvD$&nd$vbPVpNa^1^-+3_p6BKY0XM-%er|k#1LHspOZXHG)^+z|G(|gwd}?% z;|Fk=G`osRHC)znR}FAtKke>;oHR&YAOBxzdWR;YGN4#Ro`aRKJ2Upb)cC`0 z4t%0Z*`k>^RmSFA4KD=!gY0AMzb_o+5cZfT6SMv%q)l;Zu04ILhyCA_*NyZ1D}IQp zg};=_tbXo!A6-ZuMwJcn8K^OL?p`GQ7d~#fkYd^0~9PqonNy^fB#kQyxZ5RBi(MJ`^J?5+EjM(WbmLIh@IR zzO(V8Mo6~9AZ#qfAE3Q8d-kmL2{B5E;3*gJhrDOU*#oQZhb0wgetws7r`eE*?3K{(*(aHx%Wm=oHtR-x)O(M!GjR(1dzd8 z9`4fM!;7l;_|@9ne{~glvG`vMPn+df4_<-cQ9Rz!KHso6$=bM)jp4YZ)6Iluh(^cM5mCI2(>*?;`Nc>WR!=Iz3m`vb;S?c-G99)CoEIgXZ z)-Fgd&PyJd^R$eH!e^UP0(J<7su09Oxa%vjj5gCD&lw%mv&5cyF6x;E`&Lk+hc*`& zD<<=^n1K`1&gfFQd?gd@-O(@wiRD>H-hpHu|AW51U)SbjWc?!3NAA>Eu_w1L2Gk}>JX`P8NS4tYs@QcY;ZTfm&5g#(6 zm(YFMKNWL$9%>43+>`#`{#_SLwQTV3i1)qrvWXMp$FB7Nc!bFMClVjN8mtPiJCUmQ zPB2;PILu?H2DI=ui<^H!0K0Lcf1~=U%;Xv0Z~^z%ou_M+_L3Xpw691Pk3X@e%vKcb z(@O$^U^((5@ic~3!SJ0479rxqfuT2W-~MJ;{kwfy`A8HWj_^VGw_{)uV3@)*D7y>f z%#_cd_PLZA%udqyZ!uNyutT47&`?f|F9;U^$v|5}g!Yo^tI`k8oCH%9!cPGm=Kpo5|#%eBQ!-)ZT?aKh!L_vK>QZxm(E43ky#0YGb8#$GH7s zv(h^HN42le>XQHd2)8eYX1&I7FS}k;f5>AJ0I>coK9xZ7j!~i2yP!j^}g!*qJx^ws0jtth37LIhSRDS2kcIov9-+J*s&$@9gxo2 zT?zEP>&v+G8_~G^kt*ZCnhndXi&|#1K9F*&A4K-Vu{_wMT*rtrSGUed9ED zNc5wxIZ`;+nM9*MvA-Pmsks2BmA~Ne$Y1q>3m=)M-3# zAOs((ups?^ibf?)l%(XNB>7Q8SWJr4(So7bA8Z=0J{PqzvanPygN**tSn9)pggiN{ zvR0P^({nj#p`EXo)A%-^z~OnpQ$NJ0cuAvG7Yh$0n==&Db9p)d$^>Qo0FX$C((dVc z2vjG|=*;tLSTc_`(+?rAlg!Qn$af$-wuT>~`}$^geODm~c!GeM=9#+&e{A>mBBu z-ITQC_WE4{c0j+B>!Fx~p4ojAZ0<8Mpq1z5O5{PC2@pzzvDB30G=%E{u09WLyZk7o5?=^jk{X-lETVpkE+^vn3S$W&gyM@si&E|Lul2M) z+Y`*xS?*d;HLB1r^9#G>n+5ttzW1E(CKa^tlisZ-I$kdqpG%wh=#hBdGq$x*8#y$v zUt&1uXugnwO!a+k-(kd3_$;3Q^Iw+p`uyv-L+HU!`(33_Zp>iHpkGVn!-vLr4rvzX zAY9oME}Zm=u~4=SQ>L9^BxKs9rk&a6Kz{nUK#rUH`dbnF4T4eF!s^XJRhdK<(4cxB zEh$bF>?pdHs%r^k0=znz$w!agoYkBxb?OWxvPV1cMHdl^BZp}8F=;Plv3P?LUTx+W z{ys<=xu6U!SMn`ibZCX)4i*vy$k0`VQp+5XB>js2%SDCBw1QCw_1-G{nlbRz7=*mG zTq`CLYf}9=zS(&ex2m?i51I#kmdWpb782rPYMsxcd1o+QLWvCz8LbR96=Ik;>ZDGY zb)L}r`poqd<_5BSBlQ~#>0KOR!xZeo@H z&Wf#%Yo-J`C@$=-KFm7#TW$YTEPgf9+T|M2zBF7sC_3~4WYW1Y7bMQX8ADZ3OGM!QwutZ(H zeu!{;{lK{pYc@5;8L@45Hdv2gh4aJEm*bBecm63n?5JPi^p0}eq5j7O$-Uyp80@kT z2%s9+7{4q6uoi^7^8xQ(qMI=duh(bo%TL0H|Mm|+pwL%l59^&J9tG0gaJ!ZVt4fqV zj#7Qbvxq&dePv+XR`G!i^FDj`#}c1fAeb^s1}n}m0niZY}+=u@ap_tdLlSJ zExqc5_^o;6ceEtlCFd=8H9tmP5ZZCx>TV%*9fsN`p4XV{5O!p3oI#T|K`|@8?Ll2s z-ZI`kY$kJpGIY)U#|qsQ`H67_F&R<}QLc!WnJ-XUe1Rwwp$uOHC>g6_6n+WLT-`i# z0CJR3F{R*&C*C&uP9TiSAdUzTi0Aw;7d%fP?OJkh--%Ue)gm zCs9vUIR0H-|9Rmn(-A!>L@`s&V1eCk^PN}N&0kO^W&vTySWX$<-U*(8TwQ48z=x0P zy?>htKQ>S-v~rrcQH_55=k9b*B>ZPkZ{$VCSM!oa;3yFXRR zZG1(Ck!ud!^X=Dbxtvxh10~OTzF`fKpDSoeh)rwNBe&tKpHe;k_xr!y0yA4a5iP-B zm!!((G!vvkC>{JBHF&TKe8!GbZd*s|Tev}+{0<*4HTnv_S)|a=1?+#xSebVI0Jjo? z@7rd_MPv2AYvQu^!pw+K9kYY_!;RPm$Me*`sgFex<^oEiBC-a|{67VgV4hH>3i#qn z5nMqCN7aXdPMaKngvR30cxOrsKA4R9xl5Fcw@#;#QQ1fJq6Mz@!KU}WM{7B;iG2&@ zfE!s-ucDv9yANv+QIw9IUcU?GeQ&bZYyaUK>582Z!X);;F!kc%Vuzj2;2*9)0q7xH zl$>s&+U((j=;~l}HwA_~{M3=QgyI75L=p)zBENSyW-s?I5DOhg_DmjV|8MQLetfJ>T~S zFR7P%%9Jh6C(>s{4K7d`Wv5M4k_*?_oEJeq`W1y5%$pLBV9-m4@$0!!ZIQ7-G6b_p ziET}YX;gVXq|m-^8Cn5hO0GjuNpcjyTekih8~KpddHL|Cc?Sr{{vK~By<#g}Z-J6X zjc#CdV(AN4N}s73^B*fmC9m!}t1?}rj;;s&yXz{FRe-(L{GQCY@2)QY!fIlyNDA|Z zC<=QA3zeaX>gAon56QTzF`6Q7mSwl7cQ@&Qer+qvZS`*Wz`1-fFd5wbyj*nMjjjmL zN~}kpX;TW4$$d#=e#B#i`>*A>lgP?j-2LkToFg@!zrKi9T5}lIx7E$lOSJ1^w&T@PS(B@2tcT{b@gxnXoIjqt1+7S=h`+m05os%l`;z_OIcV`=A zh+ZPBMDim_LM!9`il)IQ(y>`6^wRJDqv@=pqWHtMPq(zRAl;49tV(xEONW#oh%^h* z-L0$&h?Jy+0!uf7Al==)z%K0WJo|f|_x*p)oHKLgeCLkq`dk*KI{k-Ks&=qi4$?y~ zwID!3Ak(n@L7jbsMgxpdQjK(fvQj|KL#{FG;IeV!V$|@^`K4P)v~!{|n0g+XXy(Cn z+m88S9^WmPG2G{MPrUa!0L8r&>#)j;$;b}h_B=fc(#5pk`=1*<6DS{RmNd)DyHi= zzWFPYzG!RL#{v1jajl2Y50v4nz>)FSkFGRw3Ib6j<*fhhGceT6&3}&N;GzD~IVcMr zU2I(czzS@vwet?@Q-(8jIPQ`du-M|aGhqvKens<|{rxB=)65+;8CjeM`IXk~o8+~a zW_XzC_;u!Dx_SOL{s1ow6S(XkyAaVv4+H;PTw@Pts`wtZc(tZeq&z8v@S*`q=ox-r+}B? zqCUtF4|KIrn+kajxb|;F`^5)C_8)3E@-xE8Ww89QA}Y|LUbV@g9QG*IgqwiKqxO=( zr~AC)AKraSq0+P4H?NH^jeMTsvpH>c8=m)w!#ODihvrq6cFuGO{k;e7=II+qoT8}h5YX3K;cmciXjtbzBv6zjj7hgQ5ehJ(-Yo|ROX%j=sl)bq;eYem+aewOmz?%6^vRs?L z#HQjwLAO$r03QK>6$rVBlossmKQHe>^NlKbeO<^u6=Is3WGs4^%DXz`v7MHYjo=)d z7>VF#7O?~hl#NHfn|TD^C*7r2nR(d;_6tZ!@Mc^N`qs~U)5&}0--EmqspHlYTDH&1Z{JFKJk?VE0iJlDCnR#E|L5_U9Q0( zV<6!#sNgbs)EL*rJ=|&b+`s%6ybg+gTW?hA;N^yT4^E+YZlO87s$1o(u@|yFPAKXx zk-I^1LAV6fGVzMWV2c>?yY%nW!_KSwSmcEk49#Lj=Avm{aBtYGB+><1c+Sp!!vrsS z$BTUwL#_E|0ihUpII=E^*c~rLJVwGKhz`^*6$k}>3Ws=~K${cqgU zBQ8u?={WXui4v(@RA=W|!?A9yh0j*kQ$hb#9+P@}4ZGxaxK=1kPIf>ju%ySU=P4!s#bC&I?=wd(a;Jc}kyf2jNx?3x@= z%plPzyh>l4ZrlGhbgiP{-C-i>J*f0Z;?Pa=Q*IWT5>eUvOtzKKCUx!^AnlflF-yYo z6CH=@*nKzga@qQ)f!T;S+u=;)i${W^xgt3+r1W32-yY~EqM>2;K5`nyXu+sWc@1@F5#2bTG?$Oib}4UH+}-5JcCJq_^vWFs z8vLpONTC4P+f<|a<7}zEnm-ub%TYQx8C1#@Hr=gB{a5yBS+BBdr0wWc$tqPJ;~JtC@m&d8=Ttw`lZjcqItyb8s9V9*flG^^hH z`HlzoBzP3Aq(UdwEk}I5U4gNrqkqel@hfbVS_<;F5PcPrM)jjAY$z|Rz42zL%?)k| zpQ>IxIvO? z#&d7M8k{UF!jnMhfQ4jfY@UaGg}K!4?fz~7`}x!@F0$dGTh^>`vET;<^~9`2u`~U+v)ZN69l&g!rjc$$`R=-xfu!yo=`Hui+Bj*h_h^0DPzK*Xt9s8&inzh( zGVQWUD6wy=^O`s#3FiqD3yTv%p9EcX={jip

aExk^}4WCr1-V9O3qo6Dn74;0J# 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

AF-H#<=%a$G#{>PKdhE{=Ljg!!CU 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

|n^hAs{%;@|^y{)>4xRZ}5It=g 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

R<*Z!rljMz< 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

9fO_8Dg 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 Sg6szFr>dl>_*>pQ=>Gw{1#%Gp literal 345118 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