Skip to main content

sp1_hypercube/
chip.rs

1use std::{fmt::Display, hash::Hash, sync::Arc};
2use thousands::Separable;
3
4use slop_air::{Air, BaseAir, PairBuilder};
5use slop_algebra::{Field, PrimeField32};
6use slop_matrix::dense::RowMajorMatrix;
7use slop_uni_stark::{get_max_constraint_degree, get_symbolic_constraints, SymbolicAirBuilder};
8
9use crate::{
10    air::{MachineAir, SP1AirBuilder},
11    lookup::{Interaction, InteractionBuilder, InteractionKind},
12};
13
14use super::PROOF_MAX_NUM_PVS;
15
16/// The maximum constraint degree for a chip.
17pub const MAX_CONSTRAINT_DEGREE: usize = 3;
18
19/// An Air that encodes lookups based on interactions.
20pub struct Chip<F: Field, A> {
21    /// The underlying AIR of the chip for constraint evaluation.
22    pub air: Arc<A>,
23    /// The interactions that the chip sends.
24    pub sends: Arc<Vec<Interaction<F>>>,
25    /// The interactions that the chip receives.
26    pub receives: Arc<Vec<Interaction<F>>>,
27    /// The total number of constraints in the chip.
28    pub num_constraints: usize,
29}
30
31impl<F: Field, A: MachineAir<F>> std::fmt::Debug for Chip<F, A> {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("Chip")
34            .field("air", &self.air.name())
35            .field("sends", &self.sends.len())
36            .field("receives", &self.receives.len())
37            .field("num_constraints", &self.num_constraints)
38            .finish()
39    }
40}
41
42impl<F: Field, A> Clone for Chip<F, A> {
43    fn clone(&self) -> Self {
44        Self {
45            air: self.air.clone(),
46            sends: self.sends.clone(),
47            receives: self.receives.clone(),
48            num_constraints: self.num_constraints,
49        }
50    }
51}
52
53impl<F: Field, A> Chip<F, A> {
54    /// The send interactions of the chip.
55    #[must_use]
56    pub fn sends(&self) -> &[Interaction<F>] {
57        &self.sends
58    }
59
60    /// The receive interactions of the chip.
61    #[must_use]
62    pub fn receives(&self) -> &[Interaction<F>] {
63        &self.receives
64    }
65
66    /// Consumes the chip and returns the underlying air.
67    #[must_use]
68    pub fn into_inner(self) -> Option<A> {
69        Arc::into_inner(self.air)
70    }
71}
72
73impl<F: PrimeField32, A: MachineAir<F>> Chip<F, A> {
74    /// Returns whether the given chip is included in the execution record of the shard.
75    pub fn included(&self, shard: &A::Record) -> bool {
76        self.air.included(shard)
77    }
78}
79
80impl<F, A> Chip<F, A>
81where
82    F: Field,
83    A: BaseAir<F>,
84{
85    /// Records the interactions and constraint degree from the air and crates a new chip.
86    pub fn new(air: A) -> Self
87    where
88        A: MachineAir<F> + Air<InteractionBuilder<F>> + Air<SymbolicAirBuilder<F>>,
89    {
90        let mut builder = InteractionBuilder::new(air.preprocessed_width(), air.width());
91        air.eval(&mut builder);
92        let (sends, receives) = builder.interactions();
93
94        let nb_byte_sends = sends.iter().filter(|s| s.kind == InteractionKind::Byte).count();
95        let nb_byte_receives = receives.iter().filter(|r| r.kind == InteractionKind::Byte).count();
96        tracing::trace!(
97            "chip {} has {} byte interactions",
98            air.name(),
99            nb_byte_sends + nb_byte_receives
100        );
101
102        let mut max_constraint_degree =
103            get_max_constraint_degree(&air, air.preprocessed_width(), PROOF_MAX_NUM_PVS);
104
105        if !sends.is_empty() || !receives.is_empty() {
106            max_constraint_degree = std::cmp::max(max_constraint_degree, MAX_CONSTRAINT_DEGREE);
107        }
108        assert!(max_constraint_degree > 0);
109
110        assert!(max_constraint_degree <= MAX_CONSTRAINT_DEGREE);
111        // Count the number of constraints.
112        // TODO: unify this with the constraint degree calculation.
113        let num_constraints =
114            get_symbolic_constraints(&air, air.preprocessed_width(), PROOF_MAX_NUM_PVS).len();
115
116        let sends = Arc::new(sends);
117        let receives = Arc::new(receives);
118
119        let air = Arc::new(air);
120        Self { air, sends, receives, num_constraints }
121    }
122
123    /// Returns the number of interactions in the chip.
124    #[inline]
125    #[must_use]
126    pub fn num_interactions(&self) -> usize {
127        self.sends.len() + self.receives.len()
128    }
129
130    /// Returns the number of sent byte lookups in the chip.
131    #[inline]
132    #[must_use]
133    pub fn num_sent_byte_lookups(&self) -> usize {
134        self.sends.iter().filter(|i| i.kind == InteractionKind::Byte).count()
135    }
136
137    /// Returns the number of sends of the given kind.
138    #[inline]
139    #[must_use]
140    pub fn num_sends_by_kind(&self, kind: InteractionKind) -> usize {
141        self.sends.iter().filter(|i| i.kind == kind).count()
142    }
143
144    /// Returns the number of receives of the given kind.
145    #[inline]
146    #[must_use]
147    pub fn num_receives_by_kind(&self, kind: InteractionKind) -> usize {
148        self.receives.iter().filter(|i| i.kind == kind).count()
149    }
150
151    /// Returns the cost of a row in the chip.
152    #[inline]
153    #[must_use]
154    pub fn cost(&self) -> u64
155    where
156        A: MachineAir<F>,
157    {
158        let preprocessed_cols = self.preprocessed_width();
159        let main_cols = self.width();
160        (preprocessed_cols + main_cols) as u64
161    }
162}
163
164impl<F, A> BaseAir<F> for Chip<F, A>
165where
166    F: Field,
167    A: BaseAir<F> + Send,
168{
169    fn width(&self) -> usize {
170        self.air.width()
171    }
172
173    fn preprocessed_trace(&self) -> Option<RowMajorMatrix<F>> {
174        panic!("Chip should not use the `BaseAir` method, but the `MachineAir` method.")
175    }
176}
177
178impl<F, A> MachineAir<F> for Chip<F, A>
179where
180    F: Field,
181    A: MachineAir<F>,
182{
183    type Record = A::Record;
184
185    type Program = A::Program;
186
187    fn name(&self) -> &'static str {
188        self.air.name()
189    }
190
191    fn preprocessed_width(&self) -> usize {
192        <A as MachineAir<F>>::preprocessed_width(&self.air)
193    }
194
195    fn preprocessed_num_rows(&self, program: &Self::Program) -> Option<usize> {
196        <A as MachineAir<F>>::preprocessed_num_rows(&self.air, program)
197    }
198
199    fn preprocessed_num_rows_with_instrs_len(
200        &self,
201        program: &Self::Program,
202        instrs_len: usize,
203    ) -> Option<usize> {
204        <A as MachineAir<F>>::preprocessed_num_rows_with_instrs_len(&self.air, program, instrs_len)
205    }
206
207    fn generate_preprocessed_trace(&self, program: &A::Program) -> Option<RowMajorMatrix<F>> {
208        <A as MachineAir<F>>::generate_preprocessed_trace(&self.air, program)
209    }
210
211    fn num_rows(&self, input: &A::Record) -> Option<usize> {
212        <A as MachineAir<F>>::num_rows(&self.air, input)
213    }
214
215    fn generate_trace(&self, input: &A::Record, output: &mut A::Record) -> RowMajorMatrix<F> {
216        self.air.generate_trace(input, output)
217    }
218
219    fn generate_trace_into(
220        &self,
221        input: &A::Record,
222        output: &mut A::Record,
223        buffer: &mut [std::mem::MaybeUninit<F>],
224    ) {
225        self.air.generate_trace_into(input, output, buffer);
226    }
227
228    fn generate_dependencies(&self, input: &A::Record, output: &mut A::Record) {
229        self.air.generate_dependencies(input, output);
230    }
231
232    fn included(&self, shard: &Self::Record) -> bool {
233        self.air.included(shard)
234    }
235}
236
237// Implement AIR directly on Chip, evaluating both execution and permutation constraints.
238impl<F, A, AB> Air<AB> for Chip<F, A>
239where
240    F: Field,
241    A: Air<AB> + MachineAir<F>,
242    AB: SP1AirBuilder<F = F> + PairBuilder,
243{
244    fn eval(&self, builder: &mut AB) {
245        // Evaluate the execution trace constraints.
246        self.air.eval(builder);
247    }
248}
249
250impl<F, A> PartialEq for Chip<F, A>
251where
252    F: Field,
253    A: MachineAir<F>,
254{
255    #[inline]
256    fn eq(&self, other: &Self) -> bool {
257        self.air.name() == other.air.name()
258    }
259}
260
261impl<F: Field, A: MachineAir<F>> Eq for Chip<F, A> where F: Field + Eq {}
262
263impl<F, A> Hash for Chip<F, A>
264where
265    F: Field,
266    A: MachineAir<F>,
267{
268    #[inline]
269    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
270        self.air.name().hash(state);
271    }
272}
273
274impl<F: Field, A: MachineAir<F>> PartialOrd for Chip<F, A>
275where
276    F: Field,
277    A: MachineAir<F>,
278{
279    #[inline]
280    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
281        Some(self.cmp(other))
282    }
283}
284
285impl<F: Field, A: MachineAir<F>> Ord for Chip<F, A>
286where
287    F: Field,
288    A: MachineAir<F>,
289{
290    #[inline]
291    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
292        self.name().cmp(other.name())
293    }
294}
295
296/// Statistics about a chip.
297#[derive(Debug, Clone)]
298pub struct ChipStatistics<F> {
299    /// The name of the chip.
300    name: String,
301    /// The height of the chip.
302    height: usize,
303    /// The number of preprocessed columns.
304    preprocessed_cols: usize,
305    /// The number of main columns.
306    main_cols: usize,
307    _marker: std::marker::PhantomData<F>,
308}
309
310impl<F: Field> ChipStatistics<F> {
311    /// Creates a new chip statistics from a chip and height.
312    #[must_use]
313    pub fn new<A: MachineAir<F>>(chip: &Chip<F, A>, height: usize) -> Self {
314        let name = chip.name().to_string();
315        let preprocessed_cols = chip.preprocessed_width();
316        let main_cols = chip.width();
317        Self { name, height, preprocessed_cols, main_cols, _marker: std::marker::PhantomData }
318    }
319
320    /// Returns the total width of the chip.
321    #[must_use]
322    #[inline]
323    pub const fn total_width(&self) -> usize {
324        self.preprocessed_cols + self.main_cols
325    }
326
327    /// Returns the total number of cells in the chip.
328    #[must_use]
329    #[inline]
330    pub const fn total_number_of_cells(&self) -> usize {
331        self.total_width() * self.height
332    }
333
334    /// Returns the total memory size of the chip in bytes.
335    #[must_use]
336    #[inline]
337    pub const fn total_memory_size(&self) -> usize {
338        self.total_number_of_cells() * std::mem::size_of::<F>()
339    }
340}
341
342impl<F: Field> Display for ChipStatistics<F> {
343    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344        write!(
345            f,
346            "{:<15} | Prep Cols = {:<5} | Main Cols = {:<5} | Rows = {:<5} | Cells = {:<10}",
347            self.name,
348            self.preprocessed_cols.separate_with_underscores(),
349            self.main_cols.separate_with_underscores(),
350            self.height.separate_with_underscores(),
351            self.total_number_of_cells().separate_with_underscores()
352        )
353    }
354}