From 45152db05345cce3fe89428a1e052b7daf37b5d0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Tue, 19 May 2026 15:29:23 -0300 Subject: [PATCH] opt: monomorphic dispatch for LogUp constraints in prover hot loop Store concrete copies of LookupBatchedTermConstraint and LookupAccumulatedConstraint alongside the boxed versions. Override compute_transition_prover on AirWithBuses to call the concrete types directly, enabling the compiler to inline fingerprint computation and eliminate vtable overhead for the expensive extension-field constraints. Base (domain) constraints still use dyn dispatch since they are heterogeneous user-defined types. --- crypto/stark/src/lookup.rs | 65 +++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index 17ba7c5ec..277b38cc9 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -814,6 +814,12 @@ pub struct AirWithBuses< /// Maximum number of bus elements across all interactions. /// Used to compute the correct number of alpha powers. max_bus_elements: usize, + /// Concrete LogUp term constraints for monomorphic prover dispatch. + /// These mirror the boxed versions in `transition_constraints` but enable + /// the compiler to inline fingerprint computation in the hot loop. + logup_term_constraints_direct: Vec, + /// Concrete LogUp accumulated constraint for monomorphic prover dispatch. + logup_accumulated_direct: Option, } impl< @@ -852,7 +858,9 @@ impl< let absorbed = auxiliary_trace_build_data.interactions[num_interactions - absorbed_count..].to_vec(); - // Create batched term constraints for committed pairs only + // Create batched term constraints for committed pairs only. + // Store concrete copies for monomorphic prover dispatch alongside boxed versions. + let mut logup_term_constraints_direct = Vec::with_capacity(num_committed_pairs); for pair_idx in 0..num_committed_pairs { let constraint = LookupBatchedTermConstraint::new( auxiliary_trace_build_data.interactions[pair_idx * 2].clone(), @@ -860,20 +868,25 @@ impl< pair_idx, transition_constraints.len(), ); + logup_term_constraints_direct.push(constraint.clone()); transition_constraints.push(Box::new(constraint)); } let num_term_columns = num_committed_pairs; - // Add the accumulated constraint with absorbed interactions - if num_interactions > 0 { + // Add the accumulated constraint with absorbed interactions. + let logup_accumulated_direct = if num_interactions > 0 { let accumulated_constraint = LookupAccumulatedConstraint::new( transition_constraints.len(), num_term_columns, absorbed, ); + let direct = accumulated_constraint.clone(); transition_constraints.push(Box::new(accumulated_constraint)); - } + Some(direct) + } else { + None + }; // Layout: num_committed_pairs term columns + 1 accumulated = ⌈N/2⌉ let num_aux_columns = if num_interactions > 0 { @@ -911,6 +924,8 @@ impl< num_precomputed_cols: None, name: None, max_bus_elements, + logup_term_constraints_direct, + logup_accumulated_direct, } } @@ -1009,6 +1024,46 @@ where self.num_base_constraints } + /// Prover-optimized constraint evaluation with monomorphic LogUp dispatch. + /// + /// Base (domain) constraints use the standard vtable path — they're heterogeneous + /// user-defined types that can't be devirtualized. + /// + /// LogUp constraints (`LookupBatchedTermConstraint`, `LookupAccumulatedConstraint`) + /// are dispatched through concrete types, enabling the compiler to inline + /// fingerprint computation and eliminate indirect call overhead in the hot loop. + fn compute_transition_prover( + &self, + evaluation_context: &TransitionEvaluationContext, + base_evals: &mut [FieldElement], + ext_evals: &mut [FieldElement], + ) { + // Zero base-field buffers + for e in base_evals.iter_mut() { + *e = FieldElement::zero(); + } + let num_base = base_evals.len(); + // Zero extension slots (LogUp constraints write here) + for e in ext_evals[num_base..].iter_mut() { + *e = FieldElement::zero(); + } + + // Phase 1: Base (domain) constraints — dyn dispatch (heterogeneous types) + for c in &self.transition_constraints[..num_base] { + c.evaluate_prover(evaluation_context, base_evals, ext_evals); + } + + // Phase 2: LogUp term constraints — monomorphic dispatch (concrete type) + for c in &self.logup_term_constraints_direct { + c.evaluate_verifier(evaluation_context, ext_evals); + } + + // Phase 3: LogUp accumulated constraint — monomorphic dispatch (concrete type) + if let Some(c) = &self.logup_accumulated_direct { + c.evaluate_verifier(evaluation_context, ext_evals); + } + } + fn transition_constraints( &self, ) -> &Vec>> { @@ -1832,6 +1887,7 @@ fn compute_fingerprint_from_step, B: IsField>( /// Clearing denominators: `c * fp_a * fp_b - sign_a * m_a * fp_b - sign_b * m_b * fp_a = 0` /// /// Degree 3: c (aux) × fp_a (linear in main) × fp_b (linear in main). +#[derive(Clone)] struct LookupBatchedTermConstraint { interaction_a: BusInteraction, interaction_b: BusInteraction, @@ -1957,6 +2013,7 @@ where /// /// For 2 absorbed interactions: /// `(acc_next - acc_curr - Σ terms + L/N) · f₁·f₂ - sign₁·m₁·f₂ - sign₂·m₂·f₁ = 0` (degree 3) +#[derive(Clone)] struct LookupAccumulatedConstraint { constraint_idx: usize, /// Number of committed term columns (excludes absorbed interactions)