Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e6f9c3e
wip: introduce buffer ops trait and cpu buffer
zkfriendly Jun 2, 2026
3d284a9
feat: buffer abstraction with basic interface and cpu buffer implemen…
zkfriendly Jun 3, 2026
7f4dd42
feat: wire irs commit path to buffer
zkfriendly Jun 3, 2026
ee1b207
wip: integrate buffer abstraction in prove and sumcheck
zkfriendly Jun 3, 2026
595b19d
chore: add temp ignore notes
zkfriendly Jun 3, 2026
c09d54c
refactor: remove transcript interactions from buffer design
zkfriendly Jun 10, 2026
d54e9b1
refactor: use active buffer instead hardcoded CpuBuffer
zkfriendly Jun 10, 2026
23a408a
wip: metal buffer backend
zkfriendly Jun 10, 2026
f020e81
wip: metal buffer refactor and minor changes
zkfriendly Jun 11, 2026
dfddb0e
feat: add experimental Metal backend
zkfriendly Jun 15, 2026
2ff88f4
wip: rs interface
zkfriendly Jun 16, 2026
bf57962
feat: add merklize to BufferOps
zkfriendly Jun 16, 2026
9f29bb2
refactor: change trait bound from Clone to Copy for Config implementa…
zkfriendly Jun 16, 2026
b55662d
feat: use ActiveBuffer in basecase
zkfriendly Jun 17, 2026
9b8a9b9
feat: use ActiveBuffer in code switch
zkfriendly Jun 17, 2026
0f5f0dd
feat: add BufferRead and BufferWrite traits
zkfriendly Jun 18, 2026
b08064c
refactor: move buffer abstraction into its own crate
zkfriendly Jun 18, 2026
e5bba9b
refactor: overall code reorganization
zkfriendly Jun 19, 2026
85f44c1
refactor: remove experimental Metal backend
zkfriendly Jun 19, 2026
72b31c0
refactor: use ActiveBuffer::random
zkfriendly Jun 19, 2026
d68e838
refactor: remove visibility modifiers from CpuBuffer and CpuSlice str…
zkfriendly Jun 19, 2026
03d110a
refactor: use buffer abstraction in mask handling
zkfriendly Jun 24, 2026
f7dbd7f
refactor: use clone instead of mem::take
zkfriendly Jun 24, 2026
fbc6343
refactor: simplify the design by removing slice and mut
zkfriendly Jul 2, 2026
cfbcc52
docs: clarify caller obligations in build_nodes documentation
zkfriendly Jul 2, 2026
cd657aa
chore: run fmt
zkfriendly Jul 2, 2026
676809b
refactor: update lifetime annotations in prove function
zkfriendly Jul 2, 2026
4840519
chore: run nightly fmt
zkfriendly Jul 2, 2026
a3933f5
refactor: change hash_rows function visibility to public
zkfriendly Jul 6, 2026
2b5bec8
feat: add fold_pair_sumcheck_polynomial method to CpuBuffer
zkfriendly Jul 6, 2026
b752e1a
refactor: replace custom Witness struct with merkle_tree::Witness and…
zkfriendly Jul 6, 2026
437e267
refactor: rename as_slice method to to_slice across buffer operations
zkfriendly Jul 6, 2026
70fd192
fix: as_slice -> to_slice in whir_zk
zkfriendly Jul 6, 2026
10aa7d0
Merge pull request #263 from worldfnd/zkfr/buffer-abstraction
zkfriendly Jul 7, 2026
471ca39
merge main into parameter selection branch
zkfriendly Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions benches/expand_from_coeff.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
use divan::{black_box, AllocProfiler, Bencher};
use whir::algebra::{fields::Field64, ntt, random_vector};
use whir::{
algebra::{
fields::Field64,
ntt::{Messages, NttEngine, ReedSolomon},
},
buffer::{ActiveBuffer, Buffer, BufferOps},
};

#[global_allocator]
static ALLOC: AllocProfiler = AllocProfiler::system();
Expand Down Expand Up @@ -28,18 +34,17 @@ fn interleaved_rs_encode(bencher: Bencher, case: &(usize, usize, usize)) {
let message_length = 1 << (exp - coset_sz);
let num_messages = 1 << coset_sz;
let mut rng = ark_std::rand::thread_rng();
let coeffs: Vec<Vec<Field64>> = (0..num_messages)
.map(|_| random_vector(&mut rng, message_length))
let coeffs: Vec<ActiveBuffer<Field64>> = (0..num_messages)
.map(|_| ActiveBuffer::random(&mut rng, message_length))
.collect();
(coeffs, expansion, coset_sz)
let engine = NttEngine::<Field64>::new_from_fftfield();
(engine, coeffs, expansion)
})
.bench_values(|(coeffs, expansion, _coset_sz)| {
let coeffs_refs = coeffs.iter().map(|v| v.as_slice()).collect::<Vec<_>>();
black_box(ntt::interleaved_rs_encode(
&coeffs_refs,
&[],
coeffs[0].len() * expansion,
))
.bench_values(|(engine, coeffs, expansion)| {
let coeffs_refs = coeffs.iter().collect::<Vec<_>>();
let messages = Messages::new(&coeffs_refs, coeffs[0].len(), 1);
let masks = ActiveBuffer::from_slice(&[]);
black_box(engine.interleaved_encode(messages, &masks, coeffs[0].len() * expansion))
});
}

Expand Down
33 changes: 25 additions & 8 deletions src/algebra/ntt/cooley_tukey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ use {crate::utils::workload_size, rayon::prelude::*, std::cmp::max};
use super::{
transpose,
utils::{lcm, sqrt_factor},
ReedSolomon,
Messages, ReedSolomon,
};
#[cfg(not(feature = "rs_in_order"))]
use crate::algebra::ntt::transpose::transpose_permute;
use crate::{
algebra::ntt::utils::divisors,
buffer::{ActiveBuffer, BufferOps},
utils::{chunks_exact_or_empty, zip_strict},
};

Expand Down Expand Up @@ -402,17 +403,33 @@ impl<F: Field> ReedSolomon<F> for NttEngine<F> {
}

#[cfg_attr(feature = "tracing", instrument(skip(self, messages, masks), fields(
num_messages = messages.len(),
message_len = messages.first().map(|c| c.len()),
num_messages = messages.vectors.len() * messages.interleaving_depth,
message_len = messages.message_length,
codeword_length = codeword_length,
mask_len = masks.len().checked_div(messages.len())
mask_len = masks.len().checked_div(messages.vectors.len() * messages.interleaving_depth)

)))]
fn interleaved_encode(&self, messages: &[&[F]], masks: &[F], codeword_length: usize) -> Vec<F> {
fn interleaved_encode(
&self,
messages: Messages<'_, F>,
masks: &ActiveBuffer<F>,
codeword_length: usize,
) -> ActiveBuffer<F> {
let vectors = messages
.vectors
.iter()
.map(|v| v.to_slice())
.collect::<Vec<_>>();
let messages = vectors
.iter()
.flat_map(|v| {
chunks_exact_or_empty(v, messages.message_length, messages.interleaving_depth)
})
.collect::<Vec<_>>();
assert!(self.order.is_multiple_of(codeword_length));
if messages.is_empty() {
assert!(masks.is_empty());
return Vec::new();
return ActiveBuffer::from_vec(Vec::new());
}
let num_messages = messages.len();
let message_len = messages[0].len();
Expand Down Expand Up @@ -445,7 +462,7 @@ impl<F: Field> ReedSolomon<F> for NttEngine<F> {
let mut result = Vec::with_capacity(num_messages * codeword_length);
for (message, mask) in zip_strict(
messages,
chunks_exact_or_empty(masks, mask_length, num_messages),
chunks_exact_or_empty(masks.to_slice(), mask_length, num_messages),
) {
// FFT[a 0 0 0] = [a a a a], so just replicate input in coset dimension.
for _ in 0..num_cosets {
Expand Down Expand Up @@ -473,7 +490,7 @@ impl<F: Field> ReedSolomon<F> for NttEngine<F> {

// Transpose to row-major order with vectors stacked horizontally.
transpose(&mut result, num_messages, codeword_length);
result
ActiveBuffer::from_vec(result)
}
}

Expand Down
83 changes: 64 additions & 19 deletions src/algebra/ntt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,35 @@ pub use self::{
};
use crate::{
algebra::fields,
buffer::{ActiveBuffer, BufferOps, DefaultRs},
type_map::{self, TypeMap},
};

pub static NTT: LazyLock<TypeMap<NttFamily>> = LazyLock::new(|| {
let map = TypeMap::new();
map.insert(
Arc::new(NttEngine::<fields::Field64>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>
Arc::new(DefaultRs::<fields::Field64>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>
);
map.insert(
Arc::new(NttEngine::<fields::Field128>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>
Arc::new(DefaultRs::<fields::Field128>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>
);
map.insert(
Arc::new(NttEngine::<fields::Field192>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>
Arc::new(DefaultRs::<fields::Field192>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>
);
map.insert(
Arc::new(NttEngine::<fields::Field256>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>
Arc::new(DefaultRs::<fields::Field256>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>
);
map.insert(
Arc::new(NttEngine::<fields::Field64_2>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>,
Arc::new(DefaultRs::<fields::Field64_2>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>,
);
map.insert(
Arc::new(NttEngine::<fields::Field64_3>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>,
Arc::new(DefaultRs::<fields::Field64_3>::new_from_fftfield()) as Arc<dyn ReedSolomon<_>>,
);
map.insert(Arc::new(
NttEngine::<<fields::Field64_2 as Field>::BasePrimeField>::new_from_fftfield(),
DefaultRs::<<fields::Field64_2 as Field>::BasePrimeField>::new_from_fftfield(),
) as Arc<dyn ReedSolomon<_>>);
map.insert(Arc::new(
NttEngine::<<fields::Field64_3 as Field>::BasePrimeField>::new_from_fftfield(),
DefaultRs::<<fields::Field64_3 as Field>::BasePrimeField>::new_from_fftfield(),
) as Arc<dyn ReedSolomon<_>>);
map
});
Expand All @@ -61,6 +62,37 @@ impl type_map::Family for NttFamily {
type Dyn<F: 'static> = dyn ReedSolomon<F>;
}

/// Interleaved Reed-Solomon input buffers.
///
/// Chunking buffer-backed vectors into per-message views can require backend work. This descriptor
/// keeps the encoding input compact and lets each backend derive the chunked messages internally.
///
/// Each vector stores `interleaving_depth` consecutive messages of `message_length` field
/// elements.
#[derive(Clone, Copy)]
pub struct Messages<'a, F> {
pub(crate) vectors: &'a [&'a ActiveBuffer<F>],
pub(crate) message_length: usize,
pub(crate) interleaving_depth: usize,
}

impl<'a, F: Field> Messages<'a, F> {
pub fn new(
vectors: &'a [&'a ActiveBuffer<F>],
message_length: usize,
interleaving_depth: usize,
) -> Self {
assert!(vectors
.iter()
.all(|vector| vector.len() == message_length * interleaving_depth));
Self {
vectors,
message_length,
interleaving_depth,
}
}
}

/// Trait for a Reed-Solomon encoder implementation for a given field `F`.
pub trait ReedSolomon<F>: Debug + Send + Sync {
/// Returns the next supported order equal or larger than `size`.
Expand Down Expand Up @@ -88,15 +120,20 @@ pub trait ReedSolomon<F>: Debug + Send + Sync {

/// Compute a masked interleaved Reed-Solomon encoding.
///
/// `messages` are `num_messages` slices of `message_length` elements.
/// `masks` is a `num_messages` × `mask_length` matrix of blinding coefficients.
/// `messages` contains `num_vectors` buffers, each holding `interleaving_depth` messages of
/// `message_length` elements laid out contiguously. `masks` is a
/// `(num_vectors * interleaving_depth) × mask_length` matrix of blinding coefficients.
/// `codeword_length` must be an NTT-smooth number >= `message_length + mask_length`.
/// returns an `codeword_length × num_messages` matrix.
/// Returns a `codeword_length × (num_vectors * interleaving_depth)` matrix.
///
/// Each output value is the univariate polynomial evaluation in the evaluation point
/// corresponding with the index of a coefficient list formed by concatenating message and mask.
///
fn interleaved_encode(&self, messages: &[&[F]], masks: &[F], codeword_length: usize) -> Vec<F>;
fn interleaved_encode(
&self,
messages: Messages<'_, F>,
masks: &ActiveBuffer<F>,
codeword_length: usize,
) -> ActiveBuffer<F>;
}

assert_obj_safe!(ReedSolomon<crate::algebra::fields::Field256>);
Expand All @@ -118,10 +155,10 @@ pub fn evaluation_points<F: 'static>(
}

pub fn interleaved_rs_encode<F: 'static>(
messages: &[&[F]],
masks: &[F],
messages: Messages<'_, F>,
masks: &ActiveBuffer<F>,
codeword_length: usize,
) -> Vec<F> {
) -> ActiveBuffer<F> {
NTT.get::<F>()
.expect("Unsupported NTT field.")
.interleaved_encode(messages, masks, codeword_length)
Expand All @@ -145,6 +182,7 @@ mod tests {
use super::*;
use crate::{
algebra::{random_vector, univariate_evaluate},
buffer::BufferOps,
utils::{chunks_exact_or_empty, zip_strict},
};

Expand Down Expand Up @@ -188,10 +226,16 @@ mod tests {
.map(|_| random_vector(&mut rng, message_length))
.collect::<Vec<_>>();
let masks = random_vector(&mut rng, mask_length * num_messages);
let message_refs = messages.iter().map(|v| v.as_slice()).collect::<Vec<_>>();
let vectors = messages
.iter()
.map(|message| ActiveBuffer::from_slice(message))
.collect::<Vec<_>>();
let vector_refs = vectors.iter().collect::<Vec<_>>();
let mask_buffer = ActiveBuffer::from_slice(&masks);
let rs_messages = Messages::new(&vector_refs, message_length, 1);
let codeword = ntt.interleaved_encode(
&message_refs,
&masks,
rs_messages,
&mask_buffer,
codeword_length,
);

Expand All @@ -200,6 +244,7 @@ mod tests {

// Output values are polynomial evaluations in the evaluation points.
let mut evaluation_points = ntt.evaluation_points(message_length + mask_length, codeword_length, &sampled_indices);
let codeword = codeword.to_slice();
for (&index, &evaluation_point) in zip_strict(&sampled_indices, &evaluation_points) {
let evaluations = &codeword[index * num_messages.. (index + 1) * num_messages];
let masks = chunks_exact_or_empty(&masks, mask_length, num_messages);
Expand Down
15 changes: 9 additions & 6 deletions src/bin/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use whir::{
linear_form::{Evaluate, LinearForm, MultilinearExtension},
},
bits::Bits,
buffer::{ActiveBuffer, BufferOps},
cmdline_utils::{AvailableFields, AvailableHash},
hash::HASH_COUNTER,
parameters::ProtocolParameters,
Expand Down Expand Up @@ -162,12 +163,13 @@ where

HASH_COUNTER.reset();

let witness = params.commit(&mut prover_state, &[&vector]);
let vector_buffer = ActiveBuffer::from_slice(&vector);
let witness = params.commit(&mut prover_state, &[&vector_buffer]);

let _ = params.prove(
&mut prover_state,
vec![Cow::Borrowed(vector.as_slice())],
vec![Cow::Owned(witness)],
&[&vector_buffer],
vec![&witness],
vec![],
Cow::Owned(vec![]),
);
Expand Down Expand Up @@ -239,7 +241,8 @@ where
HASH_COUNTER.reset();
let whir_prover_time = Instant::now();

let witness = params.commit(&mut prover_state, &[&vector]);
let vector_buffer = ActiveBuffer::from_slice(&vector);
let witness = params.commit(&mut prover_state, &[&vector_buffer]);

let prove_linear_forms: Vec<Box<dyn LinearForm<M::Target>>> = points
.iter()
Expand All @@ -250,8 +253,8 @@ where

let _ = params.prove(
&mut prover_state,
vec![Cow::Borrowed(vector.as_slice())],
vec![Cow::Owned(witness)],
&[&vector_buffer],
vec![&witness],
prove_linear_forms,
Cow::Borrowed(evaluations.as_slice()),
);
Expand Down
13 changes: 8 additions & 5 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use whir::{
linear_form::{Covector, Evaluate, LinearForm, MultilinearExtension},
},
bits::Bits,
buffer::{ActiveBuffer, BufferOps},
cmdline_utils::{AvailableFields, AvailableHash},
hash::HASH_COUNTER,
parameters::ProtocolParameters,
Expand Down Expand Up @@ -149,9 +150,10 @@ where
}

let vector = (0..num_coeffs).map(M::Source::from).collect::<Vec<_>>();
let vector_buffer = ActiveBuffer::from_slice(&vector);

let whir_commit_time = Instant::now();
let witness = params.commit(&mut prover_state, &[&vector]);
let witness = params.commit(&mut prover_state, &[&vector_buffer]);
let whir_commit_time = whir_commit_time.elapsed();

// Allocate constraints
Expand Down Expand Up @@ -184,8 +186,8 @@ where
let whir_prove_time = Instant::now();
let _ = params.prove(
&mut prover_state,
vec![Cow::Borrowed(vector.as_slice())],
vec![Cow::Owned(witness)],
&[&vector_buffer],
vec![&witness],
prove_linear_forms,
Cow::Borrowed(evaluations.as_slice()),
);
Expand Down Expand Up @@ -315,13 +317,14 @@ where
}

let whir_commit_time = Instant::now();
let witness = params.commit(&mut prover_state, &[vector.as_slice()]);
let vector_buffer = ActiveBuffer::from_slice(&vector);
let witness = params.commit(&mut prover_state, &[&vector_buffer]);
let whir_commit_time = whir_commit_time.elapsed();

let whir_prove_time = Instant::now();
let _ = params.prove(
&mut prover_state,
vec![Cow::Borrowed(&vector)],
&[&vector_buffer],
witness,
prove_linear_forms,
Cow::Borrowed(&evaluations),
Expand Down
Loading
Loading