Skip to content

Commit 18f3b8f

Browse files
Oppendiegokingston
andauthored
perf(stark): fuse and hoist deep-composition reconstruction for both FRI points (#826)
* perf(stark): fuse and hoist deep-composition reconstruction for both FRI points reconstruct_deep_composition_poly_evaluation walked the OOD table and trace-term coefficients, and inverted denominators, independently for the regular and symmetric evaluation points, then recomputed the point-invariant OOD/gamma sums from scratch on every one of the ~80 FRI queries per proof. Rewriting coeff*(base-ood)*denom as denom*(coeff*base - coeff*ood) isolates the point-independent coeff*ood term (identical between the two points) from coeff*base, so both points can share the OOD walk and a single batch-inverse. The point-invariant ood_row_sum/z_pow/h_sum_zpow sums are now computed once per proof in compute_query_invariant_deep_terms instead of once per query. Multi-query recursion profile: 1,325,335,927 -> 916,824,543 cycles (-30.8%); step 3 (FRI) 635,418,644 -> 233,869,693 cycles (-63.2%). * fmt --------- Co-authored-by: Diego K <43053772+diegokingston@users.noreply.github.com>
1 parent 4c108a1 commit 18f3b8f

1 file changed

Lines changed: 187 additions & 81 deletions

File tree

crypto/stark/src/verifier.rs

Lines changed: 187 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ where
8484

8585
pub type DeepPolynomialEvaluations<F> = (Vec<FieldElement<F>>, Vec<FieldElement<F>>);
8686

87+
/// Deep-composition sums that are identical across all FRI queries of a
88+
/// single proof (see `compute_query_invariant_deep_terms`).
89+
pub struct QueryInvariantDeepTerms<FieldExtension>
90+
where
91+
FieldExtension: Send + Sync + IsField,
92+
{
93+
/// `ood_row_sum[row] = sum_col trace_term_coeffs[col][row] * ood(row, col)`.
94+
ood_row_sum: Vec<FieldElement<FieldExtension>>,
95+
/// Derived from `proof.composition_poly_parts_ood_evaluation().len()`.
96+
number_of_parts: usize,
97+
/// `challenges.z.pow(number_of_parts)`.
98+
z_pow: FieldElement<FieldExtension>,
99+
/// `sum_j composition_poly_parts_ood_evaluation[j] * challenges.gammas[j]`.
100+
h_sum_zpow: FieldElement<FieldExtension>,
101+
}
102+
87103
// The verifier reads proofs in place from their rkyv archive; archived field
88104
// elements are viewed as native ones, which is only valid on little-endian.
89105
#[cfg(not(target_endian = "little"))]
@@ -649,6 +665,60 @@ pub trait IsStarkVerifier<
649665
openings_ok & terminal_ok
650666
}
651667

668+
/// Sums that depend only on `challenges` and proof-level OOD/gamma data —
669+
/// identical for every FRI query — computed once instead of once per
670+
/// query.
671+
fn compute_query_invariant_deep_terms(
672+
challenges: &Challenges<FieldExtension>,
673+
proof: StarkProofView<'_, Field, FieldExtension, PI>,
674+
) -> Option<QueryInvariantDeepTerms<FieldExtension>> {
675+
let trace_ood_evaluations = proof.trace_ood_evaluations();
676+
let ood_evaluations_table_height = trace_ood_evaluations.height();
677+
let ood_evaluations_table_width = trace_ood_evaluations.width();
678+
let ood_data = trace_ood_evaluations.row_major_data();
679+
let trace_term_coeffs = &challenges.trace_term_coeffs;
680+
681+
if trace_term_coeffs.is_empty()
682+
|| trace_term_coeffs.len() * trace_term_coeffs[0].len()
683+
!= ood_evaluations_table_height * ood_evaluations_table_width
684+
{
685+
return None;
686+
}
687+
688+
let mut ood_row_sum = Vec::with_capacity(ood_evaluations_table_height);
689+
for row_idx in 0..ood_evaluations_table_height {
690+
let ood_row = &ood_data[row_idx * ood_evaluations_table_width
691+
..(row_idx + 1) * ood_evaluations_table_width];
692+
let mut sum = FieldElement::<FieldExtension>::zero();
693+
for col_idx in 0..ood_evaluations_table_width {
694+
sum += &trace_term_coeffs[col_idx][row_idx] * &ood_row[col_idx];
695+
}
696+
ood_row_sum.push(sum);
697+
}
698+
699+
let composition_parts_ood = proof.composition_poly_parts_ood_evaluation();
700+
let number_of_parts = composition_parts_ood.len();
701+
let z_pow = challenges.z.pow(number_of_parts);
702+
703+
// A malformed proof/challenge set can advertise more composition
704+
// parts than sampled gammas; reject rather than silently truncate
705+
// the sum below.
706+
if challenges.gammas.len() < number_of_parts {
707+
return None;
708+
}
709+
let mut h_sum_zpow = FieldElement::<FieldExtension>::zero();
710+
for (h_i_zpower, gamma) in composition_parts_ood.iter().zip(challenges.gammas.iter()) {
711+
h_sum_zpow += h_i_zpower * gamma;
712+
}
713+
714+
Some(QueryInvariantDeepTerms {
715+
ood_row_sum,
716+
number_of_parts,
717+
z_pow,
718+
h_sum_zpow,
719+
})
720+
}
721+
652722
fn reconstruct_deep_composition_poly_evaluations_for_all_queries(
653723
challenges: &Challenges<FieldExtension>,
654724
domain: &VerifierDomain<Field>,
@@ -676,6 +746,8 @@ pub trait IsStarkVerifier<
676746
let primitive_root = &Field::get_primitive_root_of_unity(domain.root_order as u64)
677747
.expect("verifier domain root_order is a valid power of two");
678748

749+
let query_invariant_terms = Self::compute_query_invariant_deep_terms(challenges, proof)?;
750+
679751
for (i, iota) in challenges.iotas.iter().enumerate() {
680752
let opening = proof.deep_poly_opening(i);
681753

@@ -694,19 +766,6 @@ pub trait IsStarkVerifier<
694766
.map(|a| a.evaluations())
695767
.unwrap_or(&[]);
696768

697-
let evaluation_point = Self::query_challenge_to_evaluation_point(*iota, false, domain);
698-
deep_poly_evaluations.push(Self::reconstruct_deep_composition_poly_evaluation(
699-
proof,
700-
&evaluation_point,
701-
primitive_root,
702-
challenges,
703-
lde_precomputed,
704-
lde_main,
705-
lde_aux,
706-
opening.composition_poly().evaluations(),
707-
)?);
708-
709-
// Mirror for the symmetric query point.
710769
let lde_precomputed_sym: &[FieldElement<Field>] = opening
711770
.precomputed_trace_polys()
712771
.map(|p| p.evaluations_sym())
@@ -718,43 +777,62 @@ pub trait IsStarkVerifier<
718777
.map(|a| a.evaluations_sym())
719778
.unwrap_or(&[]);
720779

721-
let evaluation_point = Self::query_challenge_to_evaluation_point(*iota, true, domain);
722-
deep_poly_evaluations_sym.push(Self::reconstruct_deep_composition_poly_evaluation(
723-
proof,
724-
&evaluation_point,
725-
primitive_root,
726-
challenges,
727-
lde_precomputed_sym,
728-
lde_main_sym,
729-
lde_aux_sym,
730-
opening.composition_poly().evaluations_sym(),
731-
)?);
780+
let evaluation_point = Self::query_challenge_to_evaluation_point(*iota, false, domain);
781+
let evaluation_point_sym =
782+
Self::query_challenge_to_evaluation_point(*iota, true, domain);
783+
let (evaluation, evaluation_sym) =
784+
Self::reconstruct_deep_composition_poly_evaluation_pair(
785+
proof,
786+
&evaluation_point,
787+
&evaluation_point_sym,
788+
primitive_root,
789+
challenges,
790+
&query_invariant_terms,
791+
lde_precomputed,
792+
lde_main,
793+
lde_aux,
794+
opening.composition_poly().evaluations(),
795+
lde_precomputed_sym,
796+
lde_main_sym,
797+
lde_aux_sym,
798+
opening.composition_poly().evaluations_sym(),
799+
)?;
800+
deep_poly_evaluations.push(evaluation);
801+
deep_poly_evaluations_sym.push(evaluation_sym);
732802
}
733803
Some((deep_poly_evaluations, deep_poly_evaluations_sym))
734804
}
735805

806+
/// Reconstructs the deep composition polynomial evaluation at a query's
807+
/// point and its symmetric counterpart together. Rewriting the per-element
808+
/// trace term `coeff*(base-ood)*denom` as `denom*(coeff*base - coeff*ood)`
809+
/// isolates `coeff*ood` (identical for both points, hoisted into
810+
/// `query_invariant_terms`) from `coeff*base` (per-point), so both points
811+
/// share the OOD walk and a single batch-inverse for their denominators.
736812
#[allow(clippy::too_many_arguments)]
737-
fn reconstruct_deep_composition_poly_evaluation<'b>(
813+
fn reconstruct_deep_composition_poly_evaluation_pair<'b>(
738814
proof: StarkProofView<'_, Field, FieldExtension, PI>,
739815
evaluation_point: &FieldElement<Field>,
816+
evaluation_point_sym: &FieldElement<Field>,
740817
primitive_root: &FieldElement<Field>,
741818
challenges: &Challenges<FieldExtension>,
819+
query_invariant_terms: &QueryInvariantDeepTerms<FieldExtension>,
742820
lde_trace_precomputed_evaluations: &'b [FieldElement<Field>],
743821
lde_trace_main_evaluations: &'b [FieldElement<Field>],
744822
lde_trace_aux_evaluations: &[FieldElement<FieldExtension>],
745823
lde_composition_poly_parts_evaluation: &[FieldElement<FieldExtension>],
746-
) -> Option<FieldElement<FieldExtension>> {
747-
let trace_ood_evaluations = proof.trace_ood_evaluations();
748-
let ood_evaluations_table_height = trace_ood_evaluations.height();
749-
let ood_evaluations_table_width = trace_ood_evaluations.width();
750-
// Hot loop below: resolve the OOD data to one flat slice once instead
751-
// of re-deriving a row slice per element.
752-
let ood_data = trace_ood_evaluations.row_major_data();
824+
lde_trace_precomputed_evaluations_sym: &'b [FieldElement<Field>],
825+
lde_trace_main_evaluations_sym: &'b [FieldElement<Field>],
826+
lde_trace_aux_evaluations_sym: &[FieldElement<FieldExtension>],
827+
lde_composition_poly_parts_evaluation_sym: &[FieldElement<FieldExtension>],
828+
) -> Option<(FieldElement<FieldExtension>, FieldElement<FieldExtension>)> {
829+
let ood_evaluations_table_height = query_invariant_terms.ood_row_sum.len();
830+
let ood_evaluations_table_width = proof.trace_ood_evaluations().width();
753831
let trace_term_coeffs = &challenges.trace_term_coeffs;
754832

755833
// Base columns are supplied as two slices (precomputed ‖ main) that the
756-
// prover concatenated in this order; `num_base` is their combined width
757-
// and `base_at` indexes into them as if concatenated, without allocating.
834+
// prover concatenated in this order; `num_base`/`base_at` index into
835+
// them as if concatenated, without allocating.
758836
let num_precomputed = lde_trace_precomputed_evaluations.len();
759837
let num_base = num_precomputed + lde_trace_main_evaluations.len();
760838
let base_at = move |col: usize| -> &'b FieldElement<Field> {
@@ -764,68 +842,96 @@ pub trait IsStarkVerifier<
764842
&lde_trace_main_evaluations[col - num_precomputed]
765843
}
766844
};
845+
let num_precomputed_sym = lde_trace_precomputed_evaluations_sym.len();
846+
let num_base_sym = num_precomputed_sym + lde_trace_main_evaluations_sym.len();
847+
let base_at_sym = move |col: usize| -> &'b FieldElement<Field> {
848+
if col < num_precomputed_sym {
849+
&lde_trace_precomputed_evaluations_sym[col]
850+
} else {
851+
&lde_trace_main_evaluations_sym[col - num_precomputed_sym]
852+
}
853+
};
767854

768-
// Runtime guard: a malformed proof may supply opening evaluations whose
769-
// column count does not match the OOD table width, or whose composition
770-
// poly parts count does not match the proof's `composition_poly_parts_ood_evaluation`.
771-
// Without these checks the indexing below would panic in release builds.
772-
if num_base + lde_trace_aux_evaluations.len() != ood_evaluations_table_width {
855+
// Runtime guards: a malformed proof may supply opening evaluations
856+
// whose column count does not match the OOD table width, or whose
857+
// regular/symmetric base-column split disagree. Without these checks
858+
// the indexing below would panic in release builds.
859+
if num_base != num_base_sym {
773860
return None;
774861
}
775-
if trace_term_coeffs.is_empty()
776-
|| trace_term_coeffs.len() * trace_term_coeffs[0].len()
777-
!= ood_evaluations_table_height * ood_evaluations_table_width
862+
if num_base + lde_trace_aux_evaluations.len() != ood_evaluations_table_width
863+
|| num_base + lde_trace_aux_evaluations_sym.len() != ood_evaluations_table_width
778864
{
779865
return None;
780866
}
781867

782-
let mut denoms_trace = Vec::with_capacity(ood_evaluations_table_height);
868+
// Build both denominator sets (regular, then symmetric) and invert
869+
// them together in a single batch.
870+
let mut denoms = Vec::with_capacity(2 * ood_evaluations_table_height);
871+
let mut current_z = challenges.z.clone();
872+
for _ in 0..ood_evaluations_table_height {
873+
denoms.push(evaluation_point - &current_z);
874+
current_z = primitive_root * &current_z;
875+
}
783876
let mut current_z = challenges.z.clone();
784877
for _ in 0..ood_evaluations_table_height {
785-
denoms_trace.push(evaluation_point - &current_z);
878+
denoms.push(evaluation_point_sym - &current_z);
786879
current_z = primitive_root * &current_z;
787880
}
788881
// A malformed proof can land an OOD evaluation point on the LDE coset, reject.
789-
FieldElement::inplace_batch_inverse(&mut denoms_trace).ok()?;
790-
791-
let trace_term = (0..ood_evaluations_table_width)
792-
.zip(&challenges.trace_term_coeffs)
793-
.fold(FieldElement::zero(), |trace_terms, (col_idx, coeff_row)| {
794-
let trace_i = (0..ood_evaluations_table_height).zip(coeff_row).fold(
795-
FieldElement::zero(),
796-
|trace_t, (row_idx, coeff)| {
797-
let ood_val = &ood_data[row_idx * ood_evaluations_table_width + col_idx];
798-
// Stay in base when we can: F: IsSubFieldOf<E> gives F - E -> E.
799-
let diff: FieldElement<FieldExtension> = if col_idx < num_base {
800-
base_at(col_idx) - ood_val
801-
} else {
802-
&lde_trace_aux_evaluations[col_idx - num_base] - ood_val
803-
};
804-
let poly_evaluation = diff * &denoms_trace[row_idx];
805-
trace_t + &poly_evaluation * coeff
806-
},
807-
);
808-
trace_terms + trace_i
809-
});
882+
FieldElement::inplace_batch_inverse(&mut denoms).ok()?;
883+
let (denoms_trace, denoms_trace_sym) = denoms.split_at(ood_evaluations_table_height);
884+
885+
let mut trace_term = FieldElement::<FieldExtension>::zero();
886+
let mut trace_term_sym = FieldElement::<FieldExtension>::zero();
887+
for row_idx in 0..ood_evaluations_table_height {
888+
let ood_row_sum = &query_invariant_terms.ood_row_sum[row_idx];
889+
let mut base_row_sum = FieldElement::<FieldExtension>::zero();
890+
let mut base_row_sum_sym = FieldElement::<FieldExtension>::zero();
891+
for (col_idx, coeff_col) in trace_term_coeffs.iter().enumerate() {
892+
let coeff = &coeff_col[row_idx];
893+
if col_idx < num_base {
894+
// F: IsSubFieldOf<E> gives the cheap asymmetric F * E -> E product.
895+
base_row_sum += base_at(col_idx) * coeff;
896+
base_row_sum_sym += base_at_sym(col_idx) * coeff;
897+
} else {
898+
let aux_idx = col_idx - num_base;
899+
base_row_sum += coeff * &lde_trace_aux_evaluations[aux_idx];
900+
base_row_sum_sym += coeff * &lde_trace_aux_evaluations_sym[aux_idx];
901+
}
902+
}
903+
trace_term += &denoms_trace[row_idx] * &(&base_row_sum - ood_row_sum);
904+
trace_term_sym += &denoms_trace_sym[row_idx] * &(&base_row_sum_sym - ood_row_sum);
905+
}
810906

811-
let composition_parts_ood = proof.composition_poly_parts_ood_evaluation();
812-
let number_of_parts = lde_composition_poly_parts_evaluation.len();
813-
let z_pow = &challenges.z.pow(number_of_parts);
814-
815-
// A malformed proof can make evaluation_point == z^N, reject.
816-
let denom_composition = (evaluation_point - z_pow).inv().ok()?;
817-
let mut h_terms = FieldElement::zero();
818-
for (j, h_i_upsilon) in lde_composition_poly_parts_evaluation.iter().enumerate() {
819-
// Bounds-check via `.get(j)?`: a malformed opening may have more
820-
// parts than the proof header advertises.
821-
let h_i_zpower = composition_parts_ood.get(j)?;
822-
let gamma = challenges.gammas.get(j)?;
823-
let h_i_term = (h_i_upsilon - h_i_zpower) * gamma;
824-
h_terms += h_i_term;
907+
let number_of_parts = query_invariant_terms.number_of_parts;
908+
// Also rejects a per-query opening length that disagrees with the
909+
// proof-level `number_of_parts`, not just a regular/symmetric mismatch.
910+
if lde_composition_poly_parts_evaluation.len() != number_of_parts
911+
|| lde_composition_poly_parts_evaluation_sym.len() != number_of_parts
912+
{
913+
return None;
914+
}
915+
let z_pow = &query_invariant_terms.z_pow;
916+
917+
// A malformed proof can make evaluation_point == z_pow, reject.
918+
let mut denom_composition_pair = [evaluation_point - z_pow, evaluation_point_sym - z_pow];
919+
FieldElement::inplace_batch_inverse(&mut denom_composition_pair).ok()?;
920+
let [denom_composition, denom_composition_sym] = denom_composition_pair;
921+
922+
let mut h_sum = FieldElement::<FieldExtension>::zero();
923+
let mut h_sum_sym = FieldElement::<FieldExtension>::zero();
924+
for j in 0..number_of_parts {
925+
let h_i_upsilon = &lde_composition_poly_parts_evaluation[j];
926+
let h_i_upsilon_sym = &lde_composition_poly_parts_evaluation_sym[j];
927+
let gamma = &challenges.gammas[j];
928+
h_sum += h_i_upsilon * gamma;
929+
h_sum_sym += h_i_upsilon_sym * gamma;
825930
}
826-
h_terms *= denom_composition;
931+
let h_terms = (&h_sum - &query_invariant_terms.h_sum_zpow) * denom_composition;
932+
let h_terms_sym = (&h_sum_sym - &query_invariant_terms.h_sum_zpow) * denom_composition_sym;
827933

828-
Some(trace_term + h_terms)
934+
Some((trace_term + h_terms, trace_term_sym + h_terms_sym))
829935
}
830936

831937
/// Verifies one or more STARK proofs with their corresponding AIRs.

0 commit comments

Comments
 (0)