Skip to content

Commit 73fd202

Browse files
committed
dev mode: check_mem_estimation
1 parent 1d72c61 commit 73fd202

4 files changed

Lines changed: 286 additions & 122 deletions

File tree

ceno_zkvm/src/scheme/gpu/memory.rs

Lines changed: 158 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,47 @@ use crate::{
66
structs::ComposedConstrainSystem,
77
};
88
use 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
};
1111
use ff_ext::ExtensionField;
1212
use gkr_iop::gpu::{BB31Base, GpuBackend, gpu_prover::BB31Ext};
1313
use 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

Comments
 (0)