1#![allow(rustdoc::private_intra_doc_links)]
2use std::{
17 collections::HashMap,
18 sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
19};
20
21#[cfg(feature = "halo2-gpu")]
22use halo2_base::halo2_proofs::cuda::utils::HALO2_GPU_CTX;
23use halo2_base::halo2_proofs::{
24 arithmetic::Field as _, halo2curves::bn256::Fr, plonk::AdviceColumns,
25};
26#[cfg(feature = "halo2-gpu")]
27use openvm_cuda_common::d_buffer::DeviceBuffer;
28use openvm_stark_sdk::{
29 config::baby_bear_bn254_poseidon2::BabyBearBn254Poseidon2Config as RootConfig,
30 openvm_stark_backend::{
31 p3_field::{BasedVectorSpace, PrimeField64},
32 proof::Proof,
33 },
34 p3_baby_bear::BabyBear,
35};
36use serde::{Deserialize, Serialize};
37
38use crate::{
39 chip_traits::{ChipBase, PopulateInputs},
40 circuit::StaticVerifierCircuit,
41 field::baby_bear::{
42 BabyBearExt4, BabyBearWire, ReducedBabyBearExt4Wire, ReducedBabyBearWire, BABYBEAR_MAX_BITS,
43 },
44 tracegen::{
45 ir_builder::{GraphCell, Halo2IRBuilder, Halo2Opcode},
46 opcode_impl::{interpret_op, UNMATERIALIZED},
47 },
48};
49
50#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
53struct GraphCoreInst {
54 opcode: Halo2Opcode,
55 args: (u32, u32),
57 const_inds: (u32, u32),
59 ctx_offset: u32,
61 ctx_len: u32,
62 lookup_offset: u32,
64 lookups_len: u32,
65 dep_list: (u32, u32),
67}
68
69#[derive(Copy, Clone, Serialize, Deserialize)]
73struct ReleaseEntry {
74 inst: u32,
76 advice_end: u32,
78 lookup_end: u32,
80}
81
82#[derive(Clone, Copy)]
84struct TapePtr(*mut Fr);
85#[allow(unsafe_code)]
86unsafe impl Send for TapePtr {}
87#[allow(unsafe_code)]
88unsafe impl Sync for TapePtr {}
89
90#[derive(Serialize, Deserialize, Clone)]
97pub struct GraphProgram {
98 advice_cells: usize,
99 lookup_cells: usize,
100 lookup_bits: usize,
101 consts: Vec<Fr>,
103 operand_offsets: Vec<u32>,
105 operand_bits: Vec<u16>,
107 const_inds: Vec<u32>,
109 insts: Vec<GraphCoreInst>,
111 input_insts: Vec<GraphCoreInst>,
113 release_order: Vec<ReleaseEntry>,
118 dep_inds: Vec<u32>,
120 pv_offsets: Vec<usize>,
123}
124
125pub struct GraphExecutorState {
133 tape: Vec<Fr>,
135 input_cursor: usize,
136 flags: Vec<AtomicU8>,
139 phase: u8,
142}
143
144pub struct GraphExecutor<'a> {
148 pub program: &'a GraphProgram,
149 pub state: &'a mut GraphExecutorState,
150}
151
152impl GraphProgram {
153 pub fn new(
157 circuit: &StaticVerifierCircuit,
158 lookup_bits: usize,
159 representative_proof: &Proof<RootConfig>,
160 ) -> Self {
161 let mut ir = Halo2IRBuilder::new(lookup_bits);
162 let pvs_wire = circuit.populate_pvs(&mut ir, representative_proof);
163 let pv_offsets = pvs_wire
164 .to_vec()
165 .iter()
166 .map(|cell| match cell {
167 GraphCell::Cell(_, offset, _) => *offset,
168 GraphCell::Const(_) => unreachable!("public value must be an advice cell"),
169 })
170 .collect();
171 let mut program = Self::lower(ir);
172 program.pv_offsets = pv_offsets;
173 program
174 }
175
176 fn lower(ir: Halo2IRBuilder) -> Self {
178 let advice_cells = ir.total_ctx_len();
179 let lookup_cells = ir.total_lookups_len();
180 let lookup_bits = ir.lookup_bits();
181 let const_base = advice_cells + lookup_cells;
182
183 let mut consts: Vec<Fr> = Vec::new();
184 let mut const_map: HashMap<[u8; 32], u32> = HashMap::new();
185 let mut operand_offsets: Vec<u32> = Vec::new();
186 let mut operand_bits: Vec<u16> = Vec::new();
187 let mut const_inds: Vec<u32> = Vec::new();
188 let mut input_insts: Vec<GraphCoreInst> = Vec::new();
189 let mut compute: Vec<(u32, GraphCoreInst)> = Vec::with_capacity(ir.nodes.len());
190
191 for (node, meta) in ir.nodes.iter().zip(&ir.node_meta) {
192 let arg_lo = operand_offsets.len() as u32;
193 for (cell, &arg_offset) in node.operands.iter().zip(&meta.arg_offsets) {
194 let offset = match cell {
195 GraphCell::Cell(..) => {
196 assert_ne!(
197 arg_offset, UNMATERIALIZED,
198 "cell operand must be materialized"
199 );
200 arg_offset
201 }
202 GraphCell::Const(value) => {
203 let idx = *const_map.entry(value.to_bytes()).or_insert_with(|| {
204 consts.push(*value);
205 (consts.len() - 1) as u32
206 });
207 const_base + idx as usize
208 }
209 };
210 operand_offsets.push(u32::try_from(offset).expect("tape offset exceeds u32"));
211 operand_bits.push(cell.bits() as u16);
212 }
213 let ci_lo = const_inds.len() as u32;
214 const_inds.extend_from_slice(&meta.constant_skip_inds);
215 let inst = GraphCoreInst {
216 opcode: node.opcode,
217 args: (arg_lo, operand_offsets.len() as u32),
218 const_inds: (ci_lo, const_inds.len() as u32),
219 ctx_offset: meta.ctx_offset as u32,
220 ctx_len: meta.ctx_len as u32,
221 lookup_offset: meta.lookup_offset as u32,
222 lookups_len: meta.lookups_len as u32,
223 dep_list: (0, 0),
226 };
227 match node.opcode {
228 Halo2Opcode::LoadWitness => input_insts.push(inst),
229 _ => compute.push((meta.level, inst)),
230 }
231 }
232
233 let mut order: Vec<u32> = (0..compute.len() as u32).collect();
243 order.sort_by_key(|&i| compute[i as usize].0);
244 let mut insts: Vec<GraphCoreInst> = order.iter().map(|&i| compute[i as usize].1).collect();
245 let mut emission_to_sorted = vec![0u32; compute.len()];
246 for (sorted, &emission) in order.iter().enumerate() {
247 emission_to_sorted[emission as usize] = sorted as u32;
248 }
249
250 let mut release_order: Vec<ReleaseEntry> = Vec::with_capacity(insts.len());
258 let (mut prev_a, mut prev_l) = (0u32, 0u32);
259 for (i, (_, inst)) in compute.iter().enumerate() {
260 if inst.ctx_len == 0 && inst.lookups_len == 0 {
261 continue;
262 }
263 debug_assert!(inst.ctx_offset >= prev_a && inst.lookup_offset >= prev_l);
264 prev_a = inst.ctx_offset + inst.ctx_len;
265 prev_l = inst.lookup_offset + inst.lookups_len;
266 release_order.push(ReleaseEntry {
267 inst: emission_to_sorted[i],
268 advice_end: prev_a,
269 lookup_end: prev_l,
270 });
271 }
272 drop(compute);
273
274 let mut cell_to_compute_inst: Vec<i32> = vec![-1; advice_cells];
281 for (idx, inst) in insts.iter().enumerate() {
282 let ctx_lo = inst.ctx_offset as usize;
283 let ctx_hi = ctx_lo + inst.ctx_len as usize;
284 cell_to_compute_inst[ctx_lo..ctx_hi].fill(idx as i32);
285 }
286 let mut dep_inds: Vec<u32> = Vec::new();
287 let mut local_deps: Vec<u32> = Vec::new();
288 for inst in &mut insts {
289 local_deps.clear();
290 let (arg_lo, arg_hi) = inst.args;
291 for &arg_offset in &operand_offsets[arg_lo as usize..arg_hi as usize] {
292 let offset = arg_offset as usize;
293 if offset < advice_cells {
294 let parent = cell_to_compute_inst[offset];
295 if parent >= 0 {
296 let parent = parent as u32;
297 if !local_deps.contains(&parent) {
298 local_deps.push(parent);
299 }
300 }
301 }
302 }
303 let dep_lo = dep_inds.len() as u32;
304 dep_inds.extend_from_slice(&local_deps);
305 let dep_hi = dep_inds.len() as u32;
306 inst.dep_list = (dep_lo, dep_hi);
307 }
308 drop(cell_to_compute_inst);
309
310 GraphProgram {
311 advice_cells,
312 lookup_cells,
313 lookup_bits,
314 consts,
315 operand_offsets,
316 operand_bits,
317 const_inds,
318 insts,
319 input_insts,
320 release_order,
321 dep_inds,
322 pv_offsets: Vec::new(),
323 }
324 }
325
326 pub fn advice_cells(&self) -> usize {
328 self.advice_cells
329 }
330
331 pub fn lookup_cells(&self) -> usize {
333 self.lookup_cells
334 }
335
336 pub fn pv_offsets(&self) -> &[usize] {
338 &self.pv_offsets
339 }
340}
341
342impl GraphExecutorState {
343 pub fn new(program: &GraphProgram) -> Self {
346 let const_base = program.advice_cells + program.lookup_cells;
347 let mut tape = vec![Fr::ZERO; const_base + program.consts.len()];
348 tape[const_base..].copy_from_slice(&program.consts);
349 let flags: Vec<AtomicU8> = (0..program.insts.len()).map(|_| AtomicU8::new(0)).collect();
350 Self {
351 tape,
352 input_cursor: 0,
353 flags,
354 phase: 0,
355 }
356 }
357
358 pub fn reset(&mut self) {
361 self.input_cursor = 0;
362 }
363
364 pub fn advice(&self, program: &GraphProgram) -> &[Fr] {
367 &self.tape[..program.advice_cells]
368 }
369
370 pub fn lookups(&self, program: &GraphProgram) -> &[Fr] {
372 &self.tape[program.advice_cells..program.advice_cells + program.lookup_cells]
373 }
374}
375
376impl<'a> GraphExecutor<'a> {
377 pub fn new(program: &'a GraphProgram, state: &'a mut GraphExecutorState) -> Self {
378 assert_eq!(
379 state.tape.len(),
380 program.advice_cells + program.lookup_cells + program.consts.len(),
381 "state was allocated against a different program"
382 );
383 assert_eq!(
384 state.flags.len(),
385 program.insts.len(),
386 "state was allocated against a different program"
387 );
388 Self { program, state }
389 }
390
391 pub fn program(&self) -> &GraphProgram {
392 self.program
393 }
394
395 pub fn advice(&self) -> &[Fr] {
397 self.state.advice(self.program)
398 }
399
400 pub fn lookups(&self) -> &[Fr] {
402 self.state.lookups(self.program)
403 }
404
405 fn populate_input(&mut self, expected: Halo2Opcode, value: Fr) -> usize {
407 debug_assert!(matches!(expected, Halo2Opcode::LoadWitness));
408 let inst = *self
409 .program
410 .input_insts
411 .get(self.state.input_cursor)
412 .expect("more input loads than recorded input instructions");
413 debug_assert_eq!(
414 inst.opcode, expected,
415 "input load {} kind mismatch",
416 self.state.input_cursor
417 );
418 self.state.input_cursor += 1;
419 let advice = &mut self.state.tape[..self.program.advice_cells];
420 advice[inst.ctx_offset as usize] = value;
421 debug_assert!(inst.ctx_len == 1);
422 debug_assert!(inst.lookups_len == 0);
423 inst.ctx_offset as usize
424 }
425
426 #[allow(unsafe_code)]
441 pub fn run<F>(&mut self, num_threads: usize, mut on_delta: F)
442 where
443 F: FnMut(usize, &[Fr], usize, &[Fr]),
444 {
445 assert!(num_threads > 0);
446 assert_eq!(
447 self.state.input_cursor,
448 self.program.input_insts.len(),
449 "all proof inputs must be populated before run"
450 );
451
452 self.state.phase = self.state.phase.wrapping_add(1);
455 if self.state.phase == 0 {
456 self.state.phase = 1;
457 }
458 let phase = self.state.phase;
459
460 let mut tape = std::mem::take(&mut self.state.tape);
463 let tape_ptr = TapePtr(tape.as_mut_ptr());
464 let program: &GraphProgram = self.program;
465 let flags: &[AtomicU8] = &self.state.flags;
466 let advice_cells = program.advice_cells;
467 let lookup_cells = program.lookup_cells;
468 let n_insts = program.insts.len();
469 let claim_cursor = AtomicUsize::new(0);
471
472 let poisoned = AtomicBool::new(false);
475 struct PoisonOnPanic<'a>(&'a AtomicBool);
476 impl Drop for PoisonOnPanic<'_> {
477 fn drop(&mut self) {
478 if std::thread::panicking() {
479 self.0.store(true, Ordering::Relaxed);
480 }
481 }
482 }
483
484 std::thread::scope(|s| {
485 for _ in 0..num_threads {
486 let claim_cursor = &claim_cursor;
487 let poisoned = &poisoned;
488 s.spawn(move || {
489 let _poison = PoisonOnPanic(poisoned);
490 let mut args: Vec<Fr> = Vec::new();
491 let mut bits: Vec<u16> = Vec::new();
492 loop {
493 let idx = claim_cursor.fetch_add(1, Ordering::Relaxed);
494 if idx >= n_insts {
495 break;
496 }
497 let inst = &program.insts[idx];
498 let (dep_lo, dep_hi) = inst.dep_list;
500 for d in dep_lo as usize..dep_hi as usize {
501 let parent = program.dep_inds[d] as usize;
502 while flags[parent].load(Ordering::Acquire) != phase {
503 assert!(
504 !poisoned.load(Ordering::Relaxed),
505 "graph executor worker panicked"
506 );
507 std::hint::spin_loop();
508 }
509 }
510 program.eval_inst(inst, tape_ptr, &mut args, &mut bits);
511 flags[idx].store(phase, Ordering::Release);
513 }
514 });
515 }
516
517 {
519 const MAX_SPIN_TRIES: usize = 32;
521 const MIN_FLUSH_CELLS: u32 = 8 * 1024;
524 const MAX_FLUSH_CELLS: u32 = 1 << 20;
527 const READY_SENTINEL: u32 = u32::MAX;
529
530 #[derive(Clone, Copy)]
532 struct Range {
533 a_start: u32,
534 a_end: u32,
535 l_start: u32,
536 l_end: u32,
537 }
538 impl Range {
539 fn empty_at(a: u32, l: u32) -> Self {
541 Range {
542 a_start: a,
543 a_end: a,
544 l_start: l,
545 l_end: l,
546 }
547 }
548 fn cells(&self) -> u32 {
549 (self.a_end - self.a_start) + (self.l_end - self.l_start)
550 }
551 }
552 type Entry = (u32, Range);
554
555 let try_wait = |inst: u32| {
556 if inst == READY_SENTINEL {
557 return true;
558 }
559 let flag = &flags[inst as usize];
560 for _ in 0..MAX_SPIN_TRIES {
561 if flag.load(Ordering::Acquire) == phase {
562 return true;
563 }
564 std::hint::spin_loop();
565 }
566 false
567 };
568 let mut flush_or_defer = |r: Range, defer_to: Option<&mut Vec<Entry>>| {
578 if r.cells() == 0 {
579 return;
580 }
581 if let Some(defer) = defer_to {
582 if r.cells() < MIN_FLUSH_CELLS {
583 defer.push((READY_SENTINEL, r));
584 return;
585 }
586 }
587 let advice_delta: &[Fr] = unsafe {
588 std::slice::from_raw_parts(
589 (tape_ptr.0 as *const Fr).add(r.a_start as usize),
590 (r.a_end - r.a_start) as usize,
591 )
592 };
593 let lookup_delta: &[Fr] = unsafe {
594 std::slice::from_raw_parts(
595 (tape_ptr.0 as *const Fr).add(advice_cells + r.l_start as usize),
596 (r.l_end - r.l_start) as usize,
597 )
598 };
599 on_delta(
600 r.a_start as usize,
601 advice_delta,
602 r.l_start as usize,
603 lookup_delta,
604 );
605 };
606
607 let mut failed: Vec<Entry> = Vec::new();
612 let mut pend = Range::empty_at(0, 0);
613 for &ReleaseEntry {
614 inst,
615 advice_end,
616 lookup_end,
617 } in &program.release_order
618 {
619 if try_wait(inst) {
620 pend.a_end = advice_end;
621 pend.l_end = lookup_end;
622 if pend.a_end - pend.a_start >= MAX_FLUSH_CELLS {
623 flush_or_defer(pend, None);
624 pend = Range::empty_at(advice_end, lookup_end);
625 }
626 } else {
627 flush_or_defer(pend, Some(&mut failed));
628 failed.push((
629 inst,
630 Range {
631 a_start: pend.a_end,
632 a_end: advice_end,
633 l_start: pend.l_end,
634 l_end: lookup_end,
635 },
636 ));
637 pend = Range::empty_at(advice_end, lookup_end);
638 }
639 }
640 pend.a_end = advice_cells as u32;
642 pend.l_end = lookup_cells as u32;
643 if failed.is_empty() {
644 flush_or_defer(pend, None);
645 } else {
646 flush_or_defer(pend, Some(&mut failed));
647 }
648
649 while !failed.is_empty() {
654 assert!(
655 !poisoned.load(Ordering::Relaxed),
656 "graph executor worker panicked"
657 );
658 let allow_defer = failed.iter().any(|&(idx, _)| idx != READY_SENTINEL);
659 let mut still: Vec<Entry> = Vec::new();
660 let mut pend = Range::empty_at(0, 0);
661 for &(idx, r) in &failed {
662 if try_wait(idx) {
663 if pend.a_end == r.a_start && pend.l_end == r.l_start {
664 pend.a_end = r.a_end;
665 pend.l_end = r.l_end;
666 } else {
667 flush_or_defer(
668 pend,
669 if allow_defer { Some(&mut still) } else { None },
670 );
671 pend = r;
672 }
673 if pend.a_end - pend.a_start >= MAX_FLUSH_CELLS {
674 flush_or_defer(pend, None);
675 pend = Range::empty_at(pend.a_end, pend.l_end);
676 }
677 } else {
678 flush_or_defer(pend, Some(&mut still));
679 still.push((idx, r));
680 pend = Range::empty_at(r.a_end, r.l_end);
681 }
682 }
683 if allow_defer && !still.is_empty() {
684 flush_or_defer(pend, Some(&mut still));
685 } else {
686 flush_or_defer(pend, None);
687 }
688 failed = still;
689 }
690 }
691 });
692
693 self.state.tape = tape;
694 }
695}
696
697impl GraphProgram {
698 #[allow(unsafe_code)]
699 fn eval_inst(
700 &self,
701 inst: &GraphCoreInst,
702 tape: TapePtr,
703 args: &mut Vec<Fr>,
704 bits: &mut Vec<u16>,
705 ) {
706 args.clear();
707 bits.clear();
708 let (lo, hi) = inst.args;
709 for i in lo as usize..hi as usize {
710 let offset = self.operand_offsets[i] as usize;
711 args.push(unsafe { *tape.0.add(offset) });
714 bits.push(self.operand_bits[i]);
715 }
716 let ctx = unsafe {
719 std::slice::from_raw_parts_mut(
720 tape.0.add(inst.ctx_offset as usize),
721 inst.ctx_len as usize,
722 )
723 };
724 let lookups = unsafe {
725 std::slice::from_raw_parts_mut(
726 tape.0.add(self.advice_cells + inst.lookup_offset as usize),
727 inst.lookups_len as usize,
728 )
729 };
730 let (ci_lo, ci_hi) = inst.const_inds;
731 interpret_op(
732 &inst.opcode,
733 args,
734 bits,
735 ctx,
736 lookups,
737 self.lookup_bits,
738 &self.const_inds[ci_lo as usize..ci_hi as usize],
739 );
740 }
741}
742
743impl<'a> ChipBase for GraphExecutor<'a> {
744 type F = usize;
746}
747
748impl<'a> PopulateInputs for GraphExecutor<'a> {
749 fn load_witness(&mut self, value: Fr) -> usize {
750 self.populate_input(Halo2Opcode::LoadWitness, value)
751 }
752
753 fn bb_load_reduced_witness(&mut self, value: BabyBear) -> ReducedBabyBearWire<usize> {
754 let offset =
755 self.populate_input(Halo2Opcode::LoadWitness, Fr::from(value.as_canonical_u64()));
756 ReducedBabyBearWire::assume_reduced(BabyBearWire {
757 value: offset,
758 max_bits: BABYBEAR_MAX_BITS,
759 })
760 }
761
762 fn ext_load_reduced_witness(&mut self, value: BabyBearExt4) -> ReducedBabyBearExt4Wire<usize> {
763 let coeffs = value.as_basis_coefficients_slice();
764 ReducedBabyBearExt4Wire::assume_reduced(core::array::from_fn(|i| {
765 self.bb_load_reduced_witness(coeffs[i])
766 }))
767 }
768}
769
770pub struct FusedColumnBuilder {
783 n: usize,
785 num_advice_columns: usize,
786 break_points: Vec<usize>,
788 col_starts: Vec<usize>,
792 lookup_col_indices: Vec<usize>,
794
795 columns: AdviceColumns<Fr>,
797}
798
799impl FusedColumnBuilder {
800 pub fn new(
801 n: usize,
802 num_advice_columns: usize,
803 break_points: Vec<usize>,
804 lookup_col_indices: Vec<usize>,
805 ) -> Self {
806 let mut col_starts = Vec::with_capacity(break_points.len() + 1);
807 col_starts.push(0usize);
808 for &bp in &break_points {
809 col_starts.push(col_starts.last().unwrap() + bp);
810 }
811 Self {
812 n,
813 num_advice_columns,
814 break_points,
815 col_starts,
816 lookup_col_indices,
817 columns: AdviceColumns::<Fr>::new(),
818 }
819 }
820
821 fn ensure_allocated(&mut self) {
822 if !self.columns.is_empty() {
823 return;
824 }
825 self.columns.reserve_exact(self.num_advice_columns);
826 for _ in 0..self.num_advice_columns {
827 #[cfg(feature = "halo2-gpu")]
828 {
829 let buf: DeviceBuffer<Fr> =
830 DeviceBuffer::<Fr>::with_capacity_on(self.n, &HALO2_GPU_CTX);
831 buf.fill_zero_on(&HALO2_GPU_CTX)
832 .expect("zero-fill advice column");
833 self.columns.push(buf);
834 }
835 #[cfg(not(feature = "halo2-gpu"))]
836 self.columns.push(vec![Fr::ZERO; self.n]);
837 }
838 }
839
840 pub fn append(
844 &mut self,
845 advice_offset: usize,
846 advice_delta: &[Fr],
847 lookup_offset: usize,
848 lookup_delta: &[Fr],
849 ) {
850 self.ensure_allocated();
851
852 if !advice_delta.is_empty() {
859 let c = self.col_starts.partition_point(|&s| s <= advice_offset) - 1;
860 let (mut col, mut row) = if c > 0 && advice_offset == self.col_starts[c] {
861 (c - 1, self.break_points[c - 1])
862 } else {
863 (c, advice_offset - self.col_starts[c])
864 };
865 let mut delta_pos = 0usize;
866 while delta_pos < advice_delta.len() {
867 let cur_break_point = self.break_points.get(col).copied();
868 let rows_until_break = match cur_break_point {
869 Some(bp) => {
870 debug_assert!(bp >= row);
871 bp - row + 1
872 }
873 None => usize::MAX,
874 };
875 let delta_remaining = advice_delta.len() - delta_pos;
876 let take = rows_until_break.min(delta_remaining);
877 let src = &advice_delta[delta_pos..delta_pos + take];
878 #[cfg(feature = "halo2-gpu")]
879 self.columns[col]
880 .mut_slice(row..row + take)
881 .copy_from_host(src, &HALO2_GPU_CTX)
882 .expect("H2D advice gate segment");
883 #[cfg(not(feature = "halo2-gpu"))]
884 self.columns[col][row..row + take].copy_from_slice(src);
885 delta_pos += take;
886 if cur_break_point.is_some() && take == rows_until_break {
887 col += 1;
888 row = 0;
889 delta_pos -= 1; } else {
891 row += take;
892 }
893 }
894 }
895
896 let l = self.lookup_col_indices.len();
904 #[cfg(feature = "halo2-gpu")]
905 if !lookup_delta.is_empty() {
906 for col in 0..l {
907 let first_pos = (col + l - lookup_offset % l) % l;
909 if first_pos >= lookup_delta.len() {
910 continue;
911 }
912 let num_values = (lookup_delta.len() - first_pos).div_ceil(l);
913 let first_row = (lookup_offset + first_pos) / l;
914 let host_buf: Vec<Fr> = (0..num_values)
915 .map(|i| lookup_delta[first_pos + i * l])
916 .collect();
917 self.columns[self.lookup_col_indices[col]]
918 .mut_slice(first_row..first_row + num_values)
919 .copy_from_host(&host_buf, &HALO2_GPU_CTX)
920 .expect("H2D lookup column gather");
921 }
922 }
923 #[cfg(not(feature = "halo2-gpu"))]
924 for (i, v) in lookup_delta.iter().enumerate() {
925 let global = lookup_offset + i;
926 let col = self.lookup_col_indices[global % l];
927 let row = global / l;
928 self.columns[col][row] = *v;
929 }
930 }
931
932 pub fn take_columns(&mut self) -> AdviceColumns<Fr> {
934 assert!(
935 !self.columns.is_empty(),
936 "take_columns: no data was ever appended",
937 );
938 std::mem::take(&mut self.columns)
939 }
940
941 #[cfg(all(feature = "halo2-gpu", test))]
944 pub fn snapshot_columns_to_host(&self) -> Vec<Vec<Fr>> {
945 use openvm_cuda_common::copy::MemCopyD2H;
946 self.columns
947 .iter()
948 .map(|d| d.to_host_on(&HALO2_GPU_CTX).expect("D2H advice column"))
949 .collect()
950 }
951}
952
953#[cfg(test)]
954mod tests {
955 use std::sync::Arc;
956 #[cfg(feature = "halo2-gpu")]
957 use std::time::{Duration, Instant};
958
959 use halo2_base::gates::{
960 circuit::{builder::BaseCircuitBuilder, CircuitBuilderStage},
961 RangeChip,
962 };
963 use openvm_stark_sdk::{
964 config::baby_bear_bn254_poseidon2::{
965 BabyBearBn254Poseidon2Config as RootConfig, BabyBearBn254Poseidon2CpuEngine,
966 },
967 openvm_stark_backend::{
968 proof::Proof,
969 test_utils::{test_system_params_small, InteractionsFixture11, TestFixture},
970 StarkEngine,
971 },
972 };
973 #[cfg(feature = "halo2-gpu")]
974 use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng};
975
976 use super::*;
977 use crate::{
978 backend::Halo2Backend,
979 stages::{full_pipeline::load_proof_wire, proof_shape::log_heights_per_air_from_proof},
980 StaticVerifierCircuit,
981 };
982 #[cfg(feature = "halo2-gpu")]
983 use crate::{
984 test_fixtures::{fixture_circuit_and_proof, FIXTURE_K},
985 Halo2Params, StaticVerifierProvingKey, StaticVerifierShape,
986 };
987
988 const K: usize = 22;
989 const LOOKUP_BITS: usize = K - 1;
990
991 fn lookup_tape(range: &RangeChip<Fr>) -> Vec<Fr> {
993 let map = range.lookup_manager()[0].cells_to_lookup.lock().unwrap();
994 assert!(map.len() <= 1, "expected a single context tag");
995 map.values()
996 .flat_map(|cells| cells.iter().map(|c| c[0].value.evaluate()))
997 .collect()
998 }
999
1000 fn build_and_run(
1001 circuit: &StaticVerifierCircuit,
1002 proof: &Proof<RootConfig>,
1003 log_heights_per_air: &[usize],
1004 num_threads: usize,
1005 ) -> (GraphProgram, GraphExecutorState) {
1006 let mut ir = Halo2IRBuilder::new(LOOKUP_BITS);
1007 circuit.populate_verify_stark_constraints(&mut ir, proof);
1008 let program = GraphProgram::lower(ir);
1009 let mut state = GraphExecutorState::new(&program);
1010 {
1011 let mut executor = GraphExecutor::new(&program, &mut state);
1012 load_proof_wire(&mut executor, proof, log_heights_per_air);
1013
1014 let mut shadow_advice = vec![Fr::ZERO; executor.advice().len()];
1017 let mut covered_advice = vec![false; executor.advice().len()];
1018 let mut shadow_lookups = vec![Fr::ZERO; executor.lookups().len()];
1019 let mut covered_lookups = vec![false; executor.lookups().len()];
1020 executor.run(num_threads, |a_off, advice, l_off, lookups| {
1021 shadow_advice[a_off..a_off + advice.len()].copy_from_slice(advice);
1022 covered_advice[a_off..a_off + advice.len()].fill(true);
1023 shadow_lookups[l_off..l_off + lookups.len()].copy_from_slice(lookups);
1024 covered_lookups[l_off..l_off + lookups.len()].fill(true);
1025 });
1026 assert!(covered_advice.iter().all(|&c| c), "uncovered advice cells");
1027 assert!(covered_lookups.iter().all(|&c| c), "uncovered lookup cells");
1028 assert_eq!(shadow_advice, executor.advice(), "advice deltas");
1029 assert_eq!(shadow_lookups, executor.lookups(), "lookup deltas");
1030 }
1031 (program, state)
1032 }
1033
1034 #[test]
1035 fn graph_executor_matches_halo2_backend() {
1036 let engine: BabyBearBn254Poseidon2CpuEngine =
1037 BabyBearBn254Poseidon2CpuEngine::new(test_system_params_small(2, 8, 3));
1038 let (vk, proof) = InteractionsFixture11.keygen_and_prove(&engine);
1039 let log_heights_per_air = log_heights_per_air_from_proof(&proof);
1040 let circuit = StaticVerifierCircuit::try_new(vk, Default::default(), &log_heights_per_air)
1041 .expect("static circuit params");
1042
1043 let mut builder = BaseCircuitBuilder::from_stage(CircuitBuilderStage::Mock)
1045 .use_k(K)
1046 .use_lookup_bits(LOOKUP_BITS);
1047 let range = Arc::new(builder.range_chip());
1048 let ctx = builder.main(0);
1049 let mut backend = Halo2Backend::new(range.clone(), ctx);
1050 circuit.populate_verify_stark_constraints(&mut backend, &proof);
1051 let real_advice: Vec<Fr> = backend
1052 .ctx_mut()
1053 .advice
1054 .iter()
1055 .map(|a| a.evaluate())
1056 .collect();
1057 let real_lookups = lookup_tape(&range);
1058
1059 let (program, state) = build_and_run(&circuit, &proof, &log_heights_per_air, 4);
1060 assert_eq!(
1061 state.advice(&program).len(),
1062 real_advice.len(),
1063 "advice len"
1064 );
1065 assert_eq!(state.advice(&program), &real_advice[..], "advice tape");
1066 assert_eq!(state.lookups(&program), &real_lookups[..], "range tape");
1067
1068 let (seq_program, seq_state) = build_and_run(&circuit, &proof, &log_heights_per_air, 1);
1070 assert_eq!(seq_state.advice(&seq_program), state.advice(&program));
1071 assert_eq!(seq_state.lookups(&seq_program), state.lookups(&program));
1072 }
1073
1074 #[cfg(feature = "halo2-gpu")]
1076 fn timed_run(
1077 executor: &mut GraphExecutor<'_>,
1078 builder: &mut FusedColumnBuilder,
1079 num_threads: usize,
1080 ) -> Duration {
1081 let start = Instant::now();
1082 executor.run(
1083 num_threads,
1084 |advice_offset, advice, lookup_offset, lookups| {
1085 builder.append(advice_offset, advice, lookup_offset, lookups)
1086 },
1087 );
1088 start.elapsed()
1089 }
1090
1091 #[cfg(feature = "halo2-gpu")]
1094 fn keygen_fixture_static_verifier() -> (StaticVerifierProvingKey, Proof<RootConfig>) {
1095 let (circuit, proof) = fixture_circuit_and_proof();
1096 let shape = StaticVerifierShape {
1097 k: FIXTURE_K,
1098 lookup_bits: FIXTURE_K - 1,
1099 minimum_rows: 20,
1100 instance_columns: 1,
1101 };
1102
1103 let start = Instant::now();
1104 let params = Halo2Params::setup(FIXTURE_K as u32, ChaCha20Rng::seed_from_u64(42));
1105 println!("SRS setup (k={FIXTURE_K}): {:?}", start.elapsed());
1106
1107 let start = Instant::now();
1108 let pk = StaticVerifierProvingKey::keygen(¶ms, shape, circuit, &proof);
1109 println!("static verifier keygen: {:?}", start.elapsed());
1110 (pk, proof)
1111 }
1112
1113 #[test]
1114 #[cfg(feature = "halo2-gpu")]
1115 #[ignore = "requires CUDA GPU; slow (fixture STARK prove + halo2 keygen)"]
1116 fn graph_executor_root_proof() {
1117 use halo2_base::{
1118 gates::circuit::MaybeRangeConfig,
1119 halo2_proofs::{halo2curves::bn256::G1Affine, plonk::create_constraint_system},
1120 };
1121
1122 let (pk, proof) = keygen_fixture_static_verifier();
1123 let metadata = &pk.pinning.metadata;
1124 let log_heights_per_air = log_heights_per_air_from_proof(&proof);
1125
1126 let n = 1usize << metadata.config_params.k;
1129 let (cs, config) = create_constraint_system::<G1Affine, BaseCircuitBuilder<Fr>>(
1130 metadata.config_params.clone(),
1131 );
1132 let num_advice_columns = cs.num_advice_columns();
1133 let MaybeRangeConfig::WithRange(range_config) = &config.base else {
1134 panic!("static verifier requires lookup advice columns");
1135 };
1136 let lookup_col_indices: Vec<usize> = range_config.lookup_advice[0]
1137 .iter()
1138 .map(|c| c.index())
1139 .collect();
1140 let break_points = metadata.break_points[0].clone();
1141 let fused_builder = || {
1142 FusedColumnBuilder::new(
1143 n,
1144 num_advice_columns,
1145 break_points.clone(),
1146 lookup_col_indices.clone(),
1147 )
1148 };
1149
1150 let start = Instant::now();
1151 let mut ir = Halo2IRBuilder::new(pk.shape.lookup_bits);
1152 pk.circuit.populate_pvs(&mut ir, &proof);
1153 println!("IR build: {:?}", start.elapsed());
1154
1155 let start = Instant::now();
1156 let program = GraphProgram::lower(ir);
1157 let mut state = GraphExecutorState::new(&program);
1158 println!(
1159 "lowering: {:?} ({} insts)",
1160 start.elapsed(),
1161 program.insts.len()
1162 );
1163
1164 let start = Instant::now();
1165 {
1166 let mut executor = GraphExecutor::new(&program, &mut state);
1167 load_proof_wire(&mut executor, &proof, &log_heights_per_air);
1168 }
1169 println!("input population: {:?}", start.elapsed());
1170
1171 let mut builder = fused_builder();
1172 let total = timed_run(
1173 &mut GraphExecutor::new(&program, &mut state),
1174 &mut builder,
1175 1,
1176 );
1177 let reference_columns = builder.snapshot_columns_to_host();
1178 drop(builder.take_columns());
1179 println!("run + fused H2D (1 thread): {total:?}");
1180 let reference_advice = state.advice(&program).to_vec();
1181 let reference_lookups = state.lookups(&program).to_vec();
1182
1183 for num_threads in [4, 8, 12] {
1187 let mut builder = fused_builder();
1188 let total = timed_run(
1189 &mut GraphExecutor::new(&program, &mut state),
1190 &mut builder,
1191 num_threads,
1192 );
1193 let columns = builder.snapshot_columns_to_host();
1194 drop(builder.take_columns());
1195 println!("run + fused H2D ({num_threads} threads): {total:?}");
1196 assert_eq!(state.advice(&program), &reference_advice[..]);
1197 assert_eq!(state.lookups(&program), &reference_lookups[..]);
1198 assert_eq!(columns.len(), reference_columns.len());
1199 for (i, (col, reference)) in columns.iter().zip(&reference_columns).enumerate() {
1200 assert!(
1201 col == reference,
1202 "device column {i} mismatch vs 1-thread reference ({num_threads} threads)"
1203 );
1204 }
1205 }
1206 }
1207
1208 #[test]
1221 #[cfg(all(feature = "evm-prove", feature = "halo2-gpu"))]
1222 #[ignore = "requires CUDA GPU; slow (fixture STARK prove + halo2 keygen)"]
1223 fn graph_executor_prove_wrapped_pipeline() {
1224 let (pk, proof) = keygen_fixture_static_verifier();
1225 let mut state = GraphExecutorState::new(&pk.graph_program);
1226
1227 let start = Instant::now();
1230 let (warmup_advice, _) = pk.generate_witness(&proof, 1, &mut state);
1231 println!("pipeline warm-up (1 thread): {:?}", start.elapsed());
1232 drop(warmup_advice);
1233
1234 for num_threads in [4, 8, 12] {
1235 for iter in 0..3 {
1236 let start = Instant::now();
1237 let (gpu_advice, _instances) = pk.generate_witness(&proof, num_threads, &mut state);
1238 println!(
1239 "pipeline (threads={num_threads}, iter={iter}): {:?}",
1240 start.elapsed()
1241 );
1242 drop(gpu_advice);
1243 }
1244 }
1245 }
1246}