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}