Skip to main content

binius_compute/
lib.rs

1// Copyright 2026 The Binius Developers
2
3//! Buffer pooling for prover working memory.
4//!
5//! The prover allocates many large, short-lived buffers. [`BufferPool`] recycles freed blocks
6//! instead of returning them to the global allocator, handing out [`PoolVec`] buffers that return
7//! their block to the pool on drop. See the [`buffer_pool`] module for the concrete implementation.
8//!
9//! [`Allocator`] and [`VecLike`] abstract over that machinery: an [`Allocator`] hands out
10//! [`VecLike`] buffers, letting the prover's allocation code be written against `&impl Allocator`
11//! rather than a concrete pool. `&BufferPool` is the primary [`Allocator`], producing [`PoolVec`]
12//! buffers.
13//!
14//! [`CollectIntoAllocVec`] is the rayon seam over the same machinery: it collects a parallel
15//! iterator straight into one of those buffers.
16
17use std::{mem, mem::MaybeUninit, ops::DerefMut};
18
19use rayon::prelude::*;
20
21pub mod buffer_pool;
22
23pub use buffer_pool::{BufferPool, PoolVec};
24
25/// A source of [`VecLike`] buffers.
26///
27/// Abstracts the allocation seam so callers can be generic over how their working buffers are
28/// backed. The primary implementation is `&BufferPool`, whose [`Vec`](Allocator::Vec) is
29/// [`PoolVec`] — a buffer drawn from a recycling pool.
30///
31/// [`Sync`] is required because the prover shares `&impl Allocator` across rayon tasks (e.g. the
32/// parallel fractional-addition GKR reduction); both `&BufferPool` and `GlobalAllocator` are
33/// `Sync`.
34///
35/// [`Copy`] is required because a caller often hands the same allocator to several things at once
36/// — a channel and the Merkle prover inside it, say. An allocator handle is a pool reference or a
37/// unit struct, so both implementors are already `Copy` and the bound costs them nothing.
38pub trait Allocator: Sync + Copy {
39	/// The buffer type this allocator hands out for element type `T`.
40	///
41	/// It is a [`VecLike`] buffer, and [`VecLike`] implies [`BufferData`].
42	/// It grows and shrinks in place, so it can back a `binius_math::FieldBuffer` directly.
43	///
44	/// It is also [`Send`] so the prover can move pooled buffers across rayon tasks (e.g. the
45	/// parallel fractional-addition GKR reduction); every element type the prover pools is itself
46	/// `Send`.
47	type Vec<T: Send>: VecLike<T> + Send;
48
49	/// Allocates an empty buffer with room for at least `capacity` elements of type `T`.
50	fn alloc<T: Send>(&self, capacity: usize) -> Self::Vec<T>;
51}
52
53/// Collects a parallel iterator into a buffer drawn from an [`Allocator`].
54///
55/// The allocator's counterpart to [`IndexedParallelIterator::collect_into_vec`], which targets a
56/// `&mut Vec` that a generic buffer is not. The buffer is sized to the iterator's length, and its
57/// uninitialized capacity is written in parallel rather than zero-filled first.
58pub trait CollectIntoAllocVec: IndexedParallelIterator {
59	/// Allocates a buffer holding one element per item and fills it with the iterator's items.
60	///
61	/// ```
62	/// use binius_compute::{CollectIntoAllocVec, GlobalAllocator};
63	/// use rayon::prelude::*;
64	///
65	/// let squares = (0..8usize).into_par_iter().map(|i| i * i);
66	/// let buffer = squares.collect_into_alloc_vec(&GlobalAllocator);
67	/// assert_eq!(&*buffer, &[0, 1, 4, 9, 16, 25, 36, 49]);
68	/// ```
69	fn collect_into_alloc_vec<A: Allocator>(self, alloc: &A) -> A::Vec<Self::Item>;
70}
71
72impl<I: IndexedParallelIterator> CollectIntoAllocVec for I {
73	fn collect_into_alloc_vec<A: Allocator>(self, alloc: &A) -> A::Vec<Self::Item> {
74		let len = self.len();
75		let mut buffer = alloc.alloc::<Self::Item>(len);
76		// The allocator may hand back more capacity than requested, so bound the spare slice to the
77		// item count: a rayon zip yields as many items as its shorter side holds.
78		self.zip(&mut buffer.spare_capacity_mut()[..len])
79			.for_each(|(item, slot)| {
80				slot.write(item);
81			});
82		// SAFETY: the two zipped sides are both `len` long, so the loop wrote each of the `len`
83		// slots exactly once.
84		unsafe { buffer.set_len(len) };
85		buffer
86	}
87}
88
89/// Backing store of a `binius_math::FieldBuffer` that can be shrunk in place.
90///
91/// This lives here, rather than in `binius-math`, so that it can be a bound on
92/// [`Allocator::Vec`] — that bound is what lets generic allocation code back a `FieldBuffer` with
93/// an allocator's buffer `A::Vec<P>` without threading a `where A::Vec<P>: BufferData<P>` clause
94/// through every signature. `FieldBuffer::truncate` shrinks its backing store to match a smaller
95/// `log_len`, so it is available only for the mutable backings that support that in place.
96///
97/// This trait is the shrinkable-store capability alone, and [`VecLike`] is that plus growth.
98/// Three backings implement it:
99///
100/// - `Vec<T>` and [`PoolVec`] both shrink and grow, so both are [`VecLike`] as well.
101/// - `&mut [T]` only shrinks, by re-slicing, which is what slice-backed sumcheck halves need.
102pub trait BufferData<T>: DerefMut<Target = [T]> {
103	/// Shrinks the store in place to its first `len` elements.
104	///
105	/// `len` must be at most the current length.
106	fn truncate(&mut self, len: usize);
107}
108
109impl<T> BufferData<T> for Vec<T> {
110	fn truncate(&mut self, len: usize) {
111		Vec::truncate(self, len);
112	}
113}
114
115impl<T> BufferData<T> for PoolVec<'_, T> {
116	fn truncate(&mut self, len: usize) {
117		PoolVec::truncate(self, len);
118	}
119}
120
121impl<T> BufferData<T> for &mut [T] {
122	fn truncate(&mut self, len: usize) {
123		// A `&'a mut [T]` cannot be re-sliced in place through `&mut self`, so move it out and
124		// slice the owned value back in.
125		let full = mem::take(self);
126		*self = &mut full[..len];
127	}
128}
129
130/// A growable, `Vec`-like buffer.
131///
132/// Abstracts the buffer surface the prover uses: [`BufferData`] plus a subset of [`Vec`]'s API.
133/// Implemented by `Vec<T>` and [`PoolVec`], with methods added as callers need them.
134/// It is not meant to mirror all of [`Vec`].
135pub trait VecLike<T>: BufferData<T> + Extend<T> {
136	/// Returns the number of elements the buffer can hold without reallocating.
137	fn capacity(&self) -> usize;
138
139	/// Appends an element to the back of the buffer.
140	fn push(&mut self, value: T);
141
142	/// Clears the buffer, removing all elements while retaining its capacity.
143	fn clear(&mut self);
144
145	/// Resizes the buffer to `new_len`, filling any new slots with `value`.
146	fn resize(&mut self, new_len: usize, value: T)
147	where
148		T: Clone;
149
150	/// Appends all elements of `other` to the back of the buffer.
151	fn extend_from_slice(&mut self, other: &[T])
152	where
153		T: Clone;
154
155	/// Returns the spare capacity of the buffer as a slice of `MaybeUninit<T>`.
156	fn spare_capacity_mut(&mut self) -> &mut [MaybeUninit<T>];
157
158	/// Forces the length of the buffer to `new_len`.
159	///
160	/// # Safety
161	///
162	/// Same contract as [`Vec::set_len`]: `new_len` must be at most [`capacity`](Self::capacity)
163	/// and the elements in `0..new_len` must be initialized.
164	unsafe fn set_len(&mut self, new_len: usize);
165}
166
167impl<T> VecLike<T> for PoolVec<'_, T> {
168	fn capacity(&self) -> usize {
169		PoolVec::capacity(self)
170	}
171
172	fn push(&mut self, value: T) {
173		PoolVec::push(self, value);
174	}
175
176	fn clear(&mut self) {
177		PoolVec::clear(self);
178	}
179
180	fn resize(&mut self, new_len: usize, value: T)
181	where
182		T: Clone,
183	{
184		PoolVec::resize(self, new_len, value);
185	}
186
187	fn extend_from_slice(&mut self, other: &[T])
188	where
189		T: Clone,
190	{
191		PoolVec::extend_from_slice(self, other);
192	}
193
194	fn spare_capacity_mut(&mut self) -> &mut [MaybeUninit<T>] {
195		PoolVec::spare_capacity_mut(self)
196	}
197
198	unsafe fn set_len(&mut self, new_len: usize) {
199		unsafe { PoolVec::set_len(self, new_len) }
200	}
201}
202
203impl<'alloc> Allocator for &'alloc BufferPool {
204	type Vec<T: Send> = PoolVec<'alloc, T>;
205
206	fn alloc<T: Send>(&self, capacity: usize) -> Self::Vec<T> {
207		// Copy the `&'alloc BufferPool` out of `&self` so the returned `PoolVec` borrows the pool
208		// for `'alloc`, not merely for this call's `&self` borrow.
209		let pool: &'alloc BufferPool = self;
210		pool.alloc_vec(capacity)
211	}
212}
213
214impl<T> VecLike<T> for Vec<T> {
215	fn capacity(&self) -> usize {
216		Vec::capacity(self)
217	}
218
219	fn push(&mut self, value: T) {
220		Vec::push(self, value);
221	}
222
223	fn clear(&mut self) {
224		Vec::clear(self);
225	}
226
227	fn resize(&mut self, new_len: usize, value: T)
228	where
229		T: Clone,
230	{
231		Vec::resize(self, new_len, value);
232	}
233
234	fn extend_from_slice(&mut self, other: &[T])
235	where
236		T: Clone,
237	{
238		Vec::extend_from_slice(self, other);
239	}
240
241	fn spare_capacity_mut(&mut self) -> &mut [MaybeUninit<T>] {
242		Vec::spare_capacity_mut(self)
243	}
244
245	unsafe fn set_len(&mut self, new_len: usize) {
246		unsafe { Vec::set_len(self, new_len) }
247	}
248}
249
250/// An [`Allocator`] that hands out ordinary heap-allocated [`Vec`]s.
251///
252/// The non-pooling counterpart to `&BufferPool`: every [`alloc`](Allocator::alloc) is a plain
253/// [`Vec::with_capacity`], and each buffer is freed to the global allocator on drop.
254#[derive(Debug, Default, Clone, Copy)]
255pub struct GlobalAllocator;
256
257impl Allocator for GlobalAllocator {
258	type Vec<T: Send> = Vec<T>;
259
260	fn alloc<T: Send>(&self, capacity: usize) -> Self::Vec<T> {
261		Vec::with_capacity(capacity)
262	}
263}
264
265#[cfg(test)]
266mod tests {
267	use super::*;
268
269	/// Fills a buffer through the [`VecLike`] surface, exercising an allocator generically.
270	fn build<A: Allocator>(alloc: &A) -> A::Vec<u64> {
271		let mut buffer = alloc.alloc::<u64>(4);
272		assert!(buffer.capacity() >= 4);
273		buffer.push(1);
274		buffer.extend_from_slice(&[2, 3]);
275		buffer.resize(5, 0);
276		buffer
277	}
278
279	#[test]
280	fn collect_into_alloc_vec_fills_the_whole_buffer() {
281		// The pool rounds its blocks up, so 1000 items draw a buffer with spare slots past them.
282		// Those slots must stay outside the collected length.
283		let pool = BufferPool::new();
284		let squares = (0..1000usize).into_par_iter().map(|i| (i * i) as u64);
285		let buffer = squares.collect_into_alloc_vec(&&pool);
286		assert_eq!(buffer.len(), 1000);
287		assert!(
288			buffer
289				.iter()
290				.enumerate()
291				.all(|(i, &sq)| sq == (i * i) as u64)
292		);
293	}
294
295	#[test]
296	fn vec_truncates_through_buffer_data() {
297		let mut buffer = vec![1u64, 2, 3, 4];
298		BufferData::truncate(&mut buffer, 2);
299		assert_eq!(&*buffer, &[1, 2]);
300	}
301
302	#[test]
303	fn slice_truncates_through_buffer_data() {
304		let mut owned = [1u64, 2, 3, 4];
305		let mut buffer: &mut [u64] = &mut owned;
306		BufferData::truncate(&mut buffer, 3);
307		assert_eq!(buffer, &[1, 2, 3]);
308	}
309
310	#[test]
311	fn global_allocator_backs_a_plain_vec() {
312		let buffer = build(&GlobalAllocator);
313		assert_eq!(&*buffer, &[1, 2, 3, 0, 0]);
314	}
315
316	#[test]
317	fn buffer_pool_backs_a_pool_vec() {
318		let pool = BufferPool::new();
319		let buffer = build(&&pool);
320		assert_eq!(&*buffer, &[1, 2, 3, 0, 0]);
321	}
322}