@@ -6,12 +6,47 @@ use crate::{
66 structs:: ComposedConstrainSystem ,
77} ;
88use ceno_gpu:: {
9- estimate_build_tower_witness_memory , estimate_prove_tower_memory, estimate_sumcheck_memory,
9+ estimate_build_tower_memory , estimate_prove_tower_memory, estimate_sumcheck_memory,
1010} ;
1111use ff_ext:: ExtensionField ;
1212use gkr_iop:: gpu:: { BB31Base , GpuBackend , gpu_prover:: BB31Ext } ;
1313use mpcs:: PolynomialCommitmentScheme ;
1414
15+ const ESTIMATION_TOLERANCE_BYTES : usize = 1024 * 1024 ; // max estimation error: 1 MB
16+ const ESTIMATION_SAFETY_MARGIN_BYTES : usize = 5 * 1024 * 1024 ; // reserved headroom: 5 MB, 1MB for each sub-stage
17+
18+ /// Validate that the estimated GPU memory matches actual usage within tolerance.
19+ /// - Under-estimate (actual > estimated): diff must be <= `ESTIMATION_TOLERANCE_BYTES`
20+ /// - Over-estimate (estimated > actual): diff must be <= `ESTIMATION_SAFETY_MARGIN_BYTES`
21+ pub fn check_mem_estimation ( label : & str , estimated_bytes : usize , actual_bytes : usize ) {
22+ const ONE_MB : usize = 1024 * 1024 ;
23+ let diff = estimated_bytes as isize - actual_bytes as isize ;
24+ let to_mb = |b : usize | b as f64 / ONE_MB as f64 ;
25+ let diff_mb = diff as f64 / ONE_MB as f64 ;
26+ tracing:: info!( "[memcheck] {label}: estimated={:.2}MB, actual={:.2}MB, diff={:.2}MB" , to_mb( estimated_bytes) , to_mb( actual_bytes) , diff_mb) ;
27+ if diff < 0 {
28+ // Under-estimate: actual exceeds estimated
29+ assert ! (
30+ ( -diff) as usize <= ESTIMATION_TOLERANCE_BYTES ,
31+ "[memcheck] {label}: under-estimate! estimated={:.2}MB, actual={:.2}MB, diff={:.2}MB, tolerance={:.2}MB" ,
32+ to_mb( estimated_bytes) ,
33+ to_mb( actual_bytes) ,
34+ diff_mb,
35+ to_mb( ESTIMATION_TOLERANCE_BYTES ) ,
36+ ) ;
37+ } else {
38+ // Over-estimate: estimated exceeds actual
39+ assert ! (
40+ diff as usize <= ESTIMATION_SAFETY_MARGIN_BYTES ,
41+ "[memcheck] {label}: over-estimate! estimated={:.2}MB, actual={:.2}MB, diff={:.2}MB, margin={:.2}MB" ,
42+ to_mb( estimated_bytes) ,
43+ to_mb( actual_bytes) ,
44+ diff_mb,
45+ to_mb( ESTIMATION_SAFETY_MARGIN_BYTES ) ,
46+ ) ;
47+ }
48+ }
49+
1550/// Pre-estimate GPU memory usage for a chip proof before actual execution.
1651/// Used by the concurrent proving scheduler to reserve VRAM from the GPU memory pool,
1752/// ensuring multiple chip proofs can run in parallel without OOM.
@@ -20,50 +55,21 @@ pub fn estimate_chip_proof_memory<E: ExtensionField, PCS: PolynomialCommitmentSc
2055 input : & ProofInput < ' _ , GpuBackend < E , PCS > > ,
2156 circuit_name : & str ,
2257) -> u64 {
23- // chip parameters
24- let cs = & composed_cs. zkvm_v1_css ;
25- let num_prod_towers = composed_cs. num_reads ( ) + composed_cs. num_writes ( ) ;
26- let num_logup_towers = if composed_cs. is_with_lk_table ( ) {
27- cs. lk_table_expressions . len ( )
28- } else {
29- cs. lk_expressions . len ( )
30- } ;
31- // num_vars is log2_num_instances (+ rotation) - 1 (tower reduces by 1 layer for fanin=2)
32- let num_vars = input
33- . log2_num_instances ( )
34- . saturating_add ( composed_cs. rotation_vars ( ) . unwrap_or ( 0 ) )
35- . saturating_sub ( 1 ) ;
36- let elem_size = std:: mem:: size_of :: < BB31Ext > ( ) ;
37- let has_logup_numerator = composed_cs. is_with_lk_table ( ) ;
58+ let num_var_with_rotation =
59+ input. log2_num_instances ( ) + composed_cs. rotation_vars ( ) . unwrap_or ( 0 ) ;
3860
3961 // Part 1: trace (base usage: witness & structural mles)
4062 let trace_est = estimate_trace_bytes ( composed_cs, input) ;
4163
4264 // Part 2: main witness (base usage)
43- let main_witness_bytes = estimate_main_witness_bytes ( composed_cs, input ) ;
65+ let main_witness_bytes = estimate_main_witness_bytes ( composed_cs, num_var_with_rotation ) ;
4466
4567 // Part 3: ecc quark (temporary usage)
46- let ecc_quark_temporary_bytes = estimate_ecc_quark_bytes ( composed_cs, input) ;
68+ let n = num_var_with_rotation. saturating_sub ( 1 ) ;
69+ let ecc_quark_temporary_bytes = estimate_ecc_quark_bytes_from_num_vars ( n) ;
4770
4871 // Part 4: build & prove tower (temporary usage)
49- let build_est = estimate_build_tower_witness_memory (
50- num_prod_towers,
51- num_logup_towers,
52- num_vars,
53- num_vars,
54- elem_size,
55- has_logup_numerator,
56- ) ;
57- let prove_est = estimate_prove_tower_memory (
58- num_prod_towers,
59- num_logup_towers,
60- num_vars,
61- num_vars,
62- NUM_FANIN ,
63- elem_size,
64- true ,
65- ) ;
66- let tower_temporary_bytes = build_est. total_bytes + prove_est. total_bytes ;
72+ let tower_temporary_bytes = estimate_tower_bytes ( composed_cs, input) ;
6773
6874 // Part 5: main constraints (temporary usage)
6975 let main_constraints_temporary_bytes = estimate_main_constraints_bytes ( composed_cs, input) ;
@@ -76,7 +82,7 @@ pub fn estimate_chip_proof_memory<E: ExtensionField, PCS: PolynomialCommitmentSc
7682 . max ( ecc_quark_temporary_bytes)
7783 . max ( main_constraints_temporary_bytes) ;
7884
79- let total_usage_bytes = trace_est. trace_resident_bytes + main_witness_bytes + stage_peak_usage_bytes;
85+ let total_usage_bytes = trace_est. trace_resident_bytes + main_witness_bytes + stage_peak_usage_bytes + ESTIMATION_SAFETY_MARGIN_BYTES ;
8086
8187 let to_mb = |bytes : usize | bytes as f64 / ( 1024.0 * 1024.0 ) ;
8288 // Resident memory (always occupied during chip proof)
@@ -97,7 +103,7 @@ pub fn estimate_chip_proof_memory<E: ExtensionField, PCS: PolynomialCommitmentSc
97103 ) ;
98104 // Total peak = resident + max(stage temporaries)
99105 tracing:: info!(
100- "[mem estimate][{}] total_usage={:.2}MB (resident={:.2}MB + stage_peak ={:.2}MB)" ,
106+ "[mem estimate][{}] total_usage={:.2}MB (resident={:.2}MB + temporary ={:.2}MB)" ,
101107 circuit_name,
102108 to_mb( total_usage_bytes) ,
103109 to_mb( trace_est. trace_resident_bytes + main_witness_bytes) ,
@@ -107,50 +113,42 @@ pub fn estimate_chip_proof_memory<E: ExtensionField, PCS: PolynomialCommitmentSc
107113 total_usage_bytes as u64
108114}
109115
110- struct TraceEstimate {
116+ pub ( crate ) struct TraceEstimate {
111117 /// Persistent resident bytes (witness polys + structural MLEs)
112- trace_resident_bytes : usize ,
118+ pub ( crate ) trace_resident_bytes : usize ,
113119 /// Temporary peak during get_trace extraction (freed after)
114- trace_temporary_bytes : usize ,
120+ pub ( crate ) trace_temporary_bytes : usize ,
115121}
116122
117- fn estimate_trace_bytes < E : ExtensionField , PCS : PolynomialCommitmentScheme < E > > (
123+ /// Estimate GPU memory for structural MLEs (fixed circuit wiring polynomials).
124+ pub ( crate ) fn estimate_structural_mle_bytes ( num_structural_witin : usize , num_vars : usize ) -> usize {
125+ let base_elem_size = std:: mem:: size_of :: < BB31Base > ( ) ;
126+ let mle_len = 1usize << num_vars;
127+ num_structural_witin * mle_len * base_elem_size
128+ }
129+
130+ pub ( crate ) fn estimate_trace_bytes < E : ExtensionField , PCS : PolynomialCommitmentScheme < E > > (
118131 composed_cs : & ComposedConstrainSystem < E > ,
119132 input : & ProofInput < ' _ , GpuBackend < E , PCS > > ,
120133) -> TraceEstimate {
121134 let cs = & composed_cs. zkvm_v1_css ;
122- let cache_level =
123- std:: env:: var ( "CENO_GPU_CACHE_LEVEL" ) . unwrap_or_else ( |_| "full" . to_string ( ) ) ;
124- let has_trace_cached = matches ! ( cache_level. as_str( ) , "2" | "full" | "1" | "trace" ) ;
125-
126135 let num_var_with_rotation =
127136 input. log2_num_instances ( ) + composed_cs. rotation_vars ( ) . unwrap_or ( 0 ) ;
128- let base_elem_size = std:: mem:: size_of :: < BB31Base > ( ) ;
129- let mle_len = 1usize << num_var_with_rotation;
130- let structural_mle_bytes = cs. num_structural_witin as usize * mle_len * base_elem_size;
131-
132- // Memory cost depends on GPU cache level set during batch_commit:
133- // "full"/"trace" (default): trace cached on GPU, get_trace creates views → no additional usage
134- // "none": trace not cached, get_trace allocates temp_buffer(2x) + poly copies
135- let ( witness_mle_bytes, trace_temporary_bytes) = if has_trace_cached {
136- ( 0usize , 0usize )
137- } else {
138- let poly_bytes = cs. num_witin as usize * mle_len * base_elem_size;
139- let temp_bytes = 2 * poly_bytes;
140- ( poly_bytes, temp_bytes)
141- } ;
142137
143- let trace_resident_bytes = witness_mle_bytes + structural_mle_bytes;
138+ let structural_mle_bytes =
139+ estimate_structural_mle_bytes ( cs. num_structural_witin as usize , num_var_with_rotation) ;
140+ let ( witness_mle_bytes, trace_temporary_bytes) =
141+ estimate_trace_extraction_bytes ( cs. num_witin as usize , num_var_with_rotation) ;
144142
145143 TraceEstimate {
146- trace_resident_bytes,
144+ trace_resident_bytes : witness_mle_bytes + structural_mle_bytes ,
147145 trace_temporary_bytes,
148146 }
149147}
150148
151- fn estimate_main_witness_bytes < E : ExtensionField , PCS : PolynomialCommitmentScheme < E > > (
149+ pub fn estimate_main_witness_bytes < E : ExtensionField > (
152150 composed_cs : & ComposedConstrainSystem < E > ,
153- input : & ProofInput < ' _ , GpuBackend < E , PCS > > ,
151+ num_var_with_rotation : usize ,
154152) -> usize {
155153 let cs = & composed_cs. zkvm_v1_css ;
156154 let num_reads = cs. r_expressions . len ( ) + cs. r_table_expressions . len ( ) ;
@@ -163,46 +161,13 @@ fn estimate_main_witness_bytes<E: ExtensionField, PCS: PolynomialCommitmentSchem
163161 } ;
164162 let num_records = num_reads + num_writes + num_lk_num + num_lk_den;
165163
166- let num_var_with_rotation = input. log2_num_instances ( )
167- + composed_cs. rotation_vars ( ) . unwrap_or ( 0 ) ;
168164 let elem_size = std:: mem:: size_of :: < BB31Ext > ( ) ;
169165 let record_len = 1usize << num_var_with_rotation;
170166 num_records * record_len * elem_size
171167}
172168
173- fn estimate_ecc_quark_bytes < E : ExtensionField , PCS : PolynomialCommitmentScheme < E > > (
174- composed_cs : & ComposedConstrainSystem < E > ,
175- input : & ProofInput < ' _ , GpuBackend < E , PCS > > ,
176- ) -> usize {
177- let cs = & composed_cs. zkvm_v1_css ;
178- if cs. ec_final_sum . is_empty ( ) {
179- return 0 ;
180- }
181169
182- let n = input. log2_num_instances ( ) . saturating_sub ( 1 ) ;
183- let elem_size = std:: mem:: size_of :: < BB31Ext > ( ) ;
184- let full_len = 1usize << n;
185- let half_len = 1usize << n. saturating_sub ( 1 ) ;
186-
187- // selector MLEs: sel_add, sel_bypass, sel_export
188- let selector_bytes = 3usize * full_len * elem_size;
189- // split batches: x0/x1/y0/y1 (SEPTIC_EXTENSION_DEGREE each)
190- let split_bytes = 4usize * SEPTIC_EXTENSION_DEGREE * half_len * elem_size;
191- // half batches: x3/y3/s (SEPTIC_EXTENSION_DEGREE each)
192- let half_bytes = 3usize * SEPTIC_EXTENSION_DEGREE * half_len * elem_size;
193- // final_sum extraction uses another half-batch for xp/yp
194- let final_sum_bytes = 2usize * SEPTIC_EXTENSION_DEGREE * half_len * elem_size;
195-
196- let base_bytes = selector_bytes + split_bytes + half_bytes + final_sum_bytes;
197-
198- let mle_count = 3usize + SEPTIC_EXTENSION_DEGREE * 7 ;
199- let mle_num_vars_list = vec ! [ n; mle_count] ;
200- let sumcheck_est = estimate_sumcheck_memory ( n, 4 , & mle_num_vars_list, elem_size, true ) ;
201-
202- base_bytes + sumcheck_est. total_bytes
203- }
204-
205- fn estimate_main_constraints_bytes < E : ExtensionField , PCS : PolynomialCommitmentScheme < E > > (
170+ pub ( crate ) fn estimate_main_constraints_bytes < E : ExtensionField , PCS : PolynomialCommitmentScheme < E > > (
206171 composed_cs : & ComposedConstrainSystem < E > ,
207172 input : & ProofInput < ' _ , GpuBackend < E , PCS > > ,
208173) -> usize {
@@ -234,30 +199,35 @@ fn estimate_main_constraints_bytes<E: ExtensionField, PCS: PolynomialCommitmentS
234199 . layers
235200 . iter ( )
236201 . map ( |layer| {
202+ // +1 because the GPU sumcheck monomial terms include eq/selector multiplication,
203+ // which adds one degree on top of the raw constraint expressions.
204+ // (see ZerocheckLayer verifier: max_degree = self.max_expr_degree + 1)
205+ let main_sumcheck_degree = ( layer. max_expr_degree + 1 ) . max ( 1 ) ;
206+
237207 let total_mles =
238208 layer. n_witin + layer. n_structural_witin + layer. n_fixed + layer. n_instance ;
239209 let main_mle_num_vars_list = vec ! [ num_var_with_rotation; total_mles] ;
240210 let main_est = estimate_sumcheck_memory (
241211 num_var_with_rotation,
242- layer . max_expr_degree . max ( 1 ) ,
212+ main_sumcheck_degree ,
243213 & main_mle_num_vars_list,
244214 elem_size,
245- true ,
246215 ) ;
247216
248217 let rotation_exprs_len = layer. rotation_exprs . 1 . len ( ) ;
249218 let rotation_est = if rotation_exprs_len > 0 {
219+ // Rotation sumcheck degree = 2 (raw rotation degree 1 + selector),
220+ // but we use main_sumcheck_degree as a safe upper bound.
250221 let rotation_mles = rotation_exprs_len * 2 + 1 ;
251222 let rotation_mle_num_vars_list = vec ! [ num_var_with_rotation; rotation_mles] ;
252223 estimate_sumcheck_memory (
253224 num_var_with_rotation,
254- layer . max_expr_degree . max ( 1 ) ,
225+ main_sumcheck_degree ,
255226 & rotation_mle_num_vars_list,
256227 elem_size,
257- true ,
258228 )
259229 } else {
260- estimate_sumcheck_memory ( 0 , 1 , & [ ] , elem_size, true )
230+ estimate_sumcheck_memory ( 0 , 1 , & [ ] , elem_size)
261231 } ;
262232
263233 ( main_est. total_bytes , rotation_est. total_bytes )
@@ -269,3 +239,88 @@ fn estimate_main_constraints_bytes<E: ExtensionField, PCS: PolynomialCommitmentS
269239 let sumcheck_bytes = main_sumcheck_bytes. max ( rotation_sumcheck_bytes) ;
270240 eqs_bytes + sumcheck_bytes
271241}
242+
243+ /// Estimate temporary GPU memory for the tower proving stage (build + prove).
244+ /// Used by prove_tower_relation to validate against actual mem_tracker measurements.
245+ pub ( crate ) fn estimate_tower_bytes < E : ExtensionField , PCS : PolynomialCommitmentScheme < E > > (
246+ composed_cs : & ComposedConstrainSystem < E > ,
247+ input : & ProofInput < ' _ , GpuBackend < E , PCS > > ,
248+ ) -> usize {
249+ let cs = & composed_cs. zkvm_v1_css ;
250+ let num_prod_towers = composed_cs. num_reads ( ) + composed_cs. num_writes ( ) ;
251+ let num_logup_towers = if composed_cs. is_with_lk_table ( ) {
252+ cs. lk_table_expressions . len ( )
253+ } else {
254+ cs. lk_expressions . len ( )
255+ } ;
256+ let num_vars = input
257+ . log2_num_instances ( )
258+ . saturating_add ( composed_cs. rotation_vars ( ) . unwrap_or ( 0 ) )
259+ . saturating_sub ( 1 ) ;
260+ let elem_size = std:: mem:: size_of :: < BB31Ext > ( ) ;
261+ let has_logup_numerator = composed_cs. is_with_lk_table ( ) ;
262+
263+ let build_est = estimate_build_tower_memory (
264+ num_prod_towers,
265+ num_logup_towers,
266+ num_vars,
267+ num_vars,
268+ elem_size,
269+ has_logup_numerator,
270+ ) ;
271+ let prove_est = estimate_prove_tower_memory (
272+ num_prod_towers,
273+ num_logup_towers,
274+ num_vars,
275+ num_vars,
276+ NUM_FANIN ,
277+ elem_size,
278+ ) ;
279+
280+ build_est. total_bytes + prove_est. total_bytes
281+ }
282+
283+ /// Estimate GPU memory for trace extraction (get_trace).
284+ /// Returns `(resident_witness_bytes, temporary_bytes)`:
285+ /// - `resident`: poly copies that remain as witness MLEs after extraction
286+ /// - `temporary`: temp_buffer allocation (2x), freed after extraction
287+ /// Returns `(0, 0)` when trace is cached (default), because get_trace creates views without allocation.
288+ pub ( crate ) fn estimate_trace_extraction_bytes ( num_witin : usize , num_vars : usize ) -> ( usize , usize ) {
289+ let cache_level =
290+ std:: env:: var ( "CENO_GPU_CACHE_LEVEL" ) . unwrap_or_else ( |_| "full" . to_string ( ) ) ;
291+ let has_trace_cached = matches ! ( cache_level. as_str( ) , "2" | "full" | "1" | "trace" ) ;
292+
293+ if has_trace_cached {
294+ ( 0 , 0 )
295+ } else {
296+ let base_elem_size = std:: mem:: size_of :: < BB31Base > ( ) ;
297+ let mle_len = 1usize << num_vars;
298+ let poly_bytes = num_witin * mle_len * base_elem_size;
299+ // get_trace allocates poly copies (resident) + temp_buffer (2x, freed after)
300+ ( poly_bytes, 2 * poly_bytes)
301+ }
302+ }
303+
304+ /// Estimate GPU memory for ecc quark proving, using only the number of variables.
305+ /// This is the variant callable from prove_ec_sum_quark where composed_cs is not available.
306+ pub ( crate ) fn estimate_ecc_quark_bytes_from_num_vars ( n : usize ) -> usize {
307+ let elem_size = std:: mem:: size_of :: < BB31Ext > ( ) ;
308+ let base_elem_size = std:: mem:: size_of :: < BB31Base > ( ) ;
309+ let full_len = 1usize << n;
310+
311+ // selector MLEs: sel_add, sel_bypass, sel_export (Ext field, uploaded via mle_host_to_gpu)
312+ let selector_bytes = 3usize * full_len * elem_size;
313+ // split batches via mle_filter_even_odd_batch: x0/x1/y0/y1 (Base field, new GPU allocations)
314+ // Input MLEs have n+1 vars, split produces MLEs with n vars (full_len elements)
315+ let split_bytes = 4usize * SEPTIC_EXTENSION_DEGREE * full_len * base_elem_size;
316+ // half batches (x3/y3/s) and final_sum (xp/yp) use batch_mles_take_half
317+ // which creates views via as_view_chunk — no new GPU allocation
318+
319+ let base_bytes = selector_bytes + split_bytes;
320+
321+ let mle_count = 3usize + SEPTIC_EXTENSION_DEGREE * 7 ;
322+ let mle_num_vars_list = vec ! [ n; mle_count] ;
323+ let sumcheck_est = estimate_sumcheck_memory ( n, 4 , & mle_num_vars_list, elem_size) ;
324+
325+ base_bytes + sumcheck_est. total_bytes
326+ }
0 commit comments