From e7b56939f85c9cf9e18683d5a4e77426657f9593 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 12:34:04 -0300 Subject: [PATCH 1/3] Share an AIR's bus interactions between its clones instead of deep-copying them. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An AirWithBuses held the interaction list twice — once in auxiliary_trace_build_data and again inside the LogUpLayout built from it — and Clone copied both, so every clone duplicated the same data twice over. The in-VM verifier clones one AIR per table per epoch, which turns that into real work: profiling the recursion guest put clone+drop of Vec at 9.2% of its cycles without computing anything. Both now sit behind an Arc, so a clone bumps two refcounts. This is the pattern the struct already uses one field up for the captured constraint IR, and for the same reason. Nothing else changes: every use of these two fields is a read, so Arc's Deref covers them, and no construction site had to move. Measured on the recursion guest verifying a real mainnet block (30 epochs), same input blob on both sides: 1,546,877,064 -> 1,485,030,675 cycles, -4.00%. The guest's keccak-permutation count is identical on both sides (2,009,340), so the verifier did the same work. --- crypto/stark/src/lookup.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index ceda5417a..186ef70d0 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -824,7 +824,12 @@ pub struct AirWithBuses< constraint_set: CS, /// The LogUp layout: the framework generates the LogUp (extension) /// constraints from this and appends them after the `constraint_set` ones. - logup: LogUpLayout, + /// Detras de `Arc` por el mismo motivo que `constraint_program`: clonar un + /// `AirWithBuses` copiaba las interacciones DOS veces (aca y en + /// `auxiliary_trace_build_data`), y el verificador en-VM clona un AIR por + /// tabla por epoca. Medido en el guest de recursion: clone+drop de + /// `Vec` eran el 9,2% de sus ciclos, sin hacer matematica. + logup: std::sync::Arc, /// Idx-ordered metadata for all transition constraints, DERIVED at /// construction: `constraint_set.meta()` (base prefix) followed by the /// LogUp emission's derived metadata (ext). @@ -838,7 +843,7 @@ pub struct AirWithBuses< /// program (16-25K nodes on the big tables) per epoch/shard instance. constraint_program: std::sync::OnceLock>>, - auxiliary_trace_build_data: AuxiliaryTraceBuildData, + auxiliary_trace_build_data: std::sync::Arc, boundary_constraint_builder: PhantomData<(B, PI)>, /// Commitment to precomputed columns (if this is a preprocessed table) preprocessed_commitment: Option, @@ -913,7 +918,10 @@ impl< // Base-field (table) constraints come from the constraint set; LogUp // (extension) constraints are appended by the framework from the layout. let num_interactions = auxiliary_trace_build_data.interactions.len(); - let logup = LogUpLayout::from_interactions(auxiliary_trace_build_data.interactions.clone()); + let logup = std::sync::Arc::new(LogUpLayout::from_interactions( + auxiliary_trace_build_data.interactions.clone(), + )); + let auxiliary_trace_build_data = std::sync::Arc::new(auxiliary_trace_build_data); let num_term_columns = logup.num_term_columns; // meta = constraint_set base-prefix meta + appended LogUp ext meta, From dcfe7cd106136b200ccd928d730be7c6b258caff Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 12:34:19 -0300 Subject: [PATCH 2/3] Compute the zerofier's 1/(z^N - 1) once per AIR instead of once per constraint. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit evaluate_zerofier computed that inverse itself, but the caller evaluates every constraint of an AIR at the same z and trace_length, so the value is identical for all of them — the loop was redoing one extension-field pow and one extension-field inv per constraint, hundreds of times per table, always with the same result. The constraint-dependent half (the end-exemptions product) is the only part that had to be inside. zerofier_base_inv now computes the shared half and the callers hoist it out of the loop. Both call sites had the identical loop, so both get it: the verifier's OOD transition evaluation and the prover's. Measured on the recursion guest verifying a real mainnet block (30 epochs), same input blob on both sides: 1,485,030,675 -> 1,311,599,244 cycles, -11.21%. Combined with the preceding commit that is -15.21% off main, or 235,277,820 fewer cycles — and since the outer trace carries one CPU row per guest cycle, the same number of rows off whatever has to prove that verifier. Identical keccak-permutation count on both sides (2,009,340). The win is specific to the in-VM verifier. Verifying the same 1.6 GB continuation proof natively shows no measurable change: three interleaved runs per side averaged 27.173s before and 27.378s after, a +0.75% difference against a within-arm spread of up to 2.08%. That is expected — in the VM every instruction costs one cycle and there is no SIMD, so a cubic-extension inversion (an addition chain of dozens of multiplications) dominates in a way it never does natively, where the 27 seconds go to deserializing 1.6 GB, Merkle paths and FRI. The prover side is not measured either and is expected neutral for the same reason: there the zerofier is evaluated once per proof, not per row. --- crypto/stark/src/constraints/zerofier.rs | 29 ++++++++++++++++++++---- crypto/stark/src/prover.rs | 7 ++++++ crypto/stark/src/verifier.rs | 7 ++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/crypto/stark/src/constraints/zerofier.rs b/crypto/stark/src/constraints/zerofier.rs index ba22098de..a91f739cd 100644 --- a/crypto/stark/src/constraints/zerofier.rs +++ b/crypto/stark/src/constraints/zerofier.rs @@ -115,13 +115,36 @@ pub fn zerofier_evaluations_on_extended_domain( .collect() } +/// `1/(z^N − 1)`, the half of the zerofier that does NOT depend on the +/// constraint. +/// +/// Hoisted out of [`evaluate_zerofier`] because the caller evaluates every +/// constraint of an AIR at the SAME `z` and `trace_length`: computing it inside +/// meant one extension-field `pow` and one extension-field `inv` **per +/// constraint** instead of one per AIR. Measured in the recursion guest, where +/// that loop is hot: `evaluate_zerofier` was 15.8% of the guest's cycles and +/// 88% of that was exactly this `inv` + `pow`. +pub fn zerofier_base_inv(z: &FieldElement, trace_length: usize) -> FieldElement +where + F: IsSubFieldOf, + E: IsField, +{ + (-FieldElement::::one() + z.pow(trace_length)) + .inv() + .unwrap() +} + /// Evaluation of the constraint's zerofier at some point `z`, which may be in /// a field extension. +/// +/// `base_inv` is [`zerofier_base_inv`] for this `z`/`trace_length` — the caller +/// computes it once and passes it for every constraint of the AIR. pub fn evaluate_zerofier( meta: &ConstraintMeta, z: &FieldElement, trace_primitive_root: &FieldElement, trace_length: usize, + base_inv: &FieldElement, ) -> FieldElement where F: IsSubFieldOf, @@ -134,9 +157,5 @@ where acc * -(root.clone() - z.clone()) }); - // 1/(z^N − 1), times the end-exemptions correction. - (-FieldElement::::one() + z.pow(trace_length)) - .inv() - .unwrap() - * &end_exemptions_eval + base_inv * &end_exemptions_eval } diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index faf512a72..0bfb11749 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -4186,12 +4186,19 @@ pub trait IsStarkProver< let mut denominators = vec![FieldElement::::zero(); air.num_transition_constraints()]; + // `1/(z^N − 1)` es el mismo para TODAS las constraints de este AIR: se + // calcula una vez acá en vez de una vez por constraint adentro del loop. + let zerofier_base_inv = crate::constraints::zerofier::zerofier_base_inv::( + z, + trace_length, + ); air.constraints_meta().iter().for_each(|m| { denominators[m.constraint_idx] = crate::constraints::zerofier::evaluate_zerofier( m, z, &domain.trace_primitive_root, trace_length, + &zerofier_base_inv, ); }); let transition_sum = transition_evals diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 44add9c21..30574bb77 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -397,12 +397,19 @@ pub trait IsStarkVerifier< let mut denominators = vec![FieldElement::::zero(); air.num_transition_constraints()]; + // `1/(z^N − 1)` es el mismo para TODAS las constraints de este AIR: se + // calcula una vez acá en vez de una vez por constraint adentro del loop. + let zerofier_base_inv = crate::constraints::zerofier::zerofier_base_inv::( + &challenges.z, + trace_length, + ); air.constraints_meta().iter().for_each(|m| { denominators[m.constraint_idx] = crate::constraints::zerofier::evaluate_zerofier( m, &challenges.z, &domain.trace_primitive_root, trace_length, + &zerofier_base_inv, ); }); From 9f2dcc7d4e0f49dcb5eebea77ba2c0c6fe7678a4 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 15:26:35 -0300 Subject: [PATCH 3/3] fix lint --- crypto/stark/src/lookup.rs | 10 +++++----- crypto/stark/src/prover.rs | 12 ++++++------ crypto/stark/src/verifier.rs | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 186ef70d0..55675a92d 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -824,11 +824,11 @@ pub struct AirWithBuses< constraint_set: CS, /// The LogUp layout: the framework generates the LogUp (extension) /// constraints from this and appends them after the `constraint_set` ones. - /// Detras de `Arc` por el mismo motivo que `constraint_program`: clonar un - /// `AirWithBuses` copiaba las interacciones DOS veces (aca y en - /// `auxiliary_trace_build_data`), y el verificador en-VM clona un AIR por - /// tabla por epoca. Medido en el guest de recursion: clone+drop de - /// `Vec` eran el 9,2% de sus ciclos, sin hacer matematica. + /// Behind `Arc` for the same reason as `constraint_program`: cloning an + /// `AirWithBuses` copied the interaction list TWICE (here and in + /// `auxiliary_trace_build_data`), and the in-VM verifier clones one AIR per + /// table per epoch. Measured on the recursion guest, clone+drop of + /// `Vec` was 9.2% of its cycles while computing nothing. logup: std::sync::Arc, /// Idx-ordered metadata for all transition constraints, DERIVED at /// construction: `constraint_set.meta()` (base prefix) followed by the diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 0bfb11749..8e428a1c8 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -4186,12 +4186,12 @@ pub trait IsStarkProver< let mut denominators = vec![FieldElement::::zero(); air.num_transition_constraints()]; - // `1/(z^N − 1)` es el mismo para TODAS las constraints de este AIR: se - // calcula una vez acá en vez de una vez por constraint adentro del loop. - let zerofier_base_inv = crate::constraints::zerofier::zerofier_base_inv::( - z, - trace_length, - ); + // `1/(z^N - 1)` is the same for EVERY constraint of this AIR, so it is + // computed once here instead of once per constraint inside the loop. + let zerofier_base_inv = crate::constraints::zerofier::zerofier_base_inv::< + Field, + FieldExtension, + >(z, trace_length); air.constraints_meta().iter().for_each(|m| { denominators[m.constraint_idx] = crate::constraints::zerofier::evaluate_zerofier( m, diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 30574bb77..e2ac71bee 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -397,12 +397,12 @@ pub trait IsStarkVerifier< let mut denominators = vec![FieldElement::::zero(); air.num_transition_constraints()]; - // `1/(z^N − 1)` es el mismo para TODAS las constraints de este AIR: se - // calcula una vez acá en vez de una vez por constraint adentro del loop. - let zerofier_base_inv = crate::constraints::zerofier::zerofier_base_inv::( - &challenges.z, - trace_length, - ); + // `1/(z^N - 1)` is the same for EVERY constraint of this AIR, so it is + // computed once here instead of once per constraint inside the loop. + let zerofier_base_inv = crate::constraints::zerofier::zerofier_base_inv::< + Field, + FieldExtension, + >(&challenges.z, trace_length); air.constraints_meta().iter().for_each(|m| { denominators[m.constraint_idx] = crate::constraints::zerofier::evaluate_zerofier( m,