openvm_circuit/arch/execution_mode/metered/
memory_ctx.rs

1use abi_stable::std_types::RVec;
2use openvm_instructions::riscv::{RV32_NUM_REGISTERS, RV32_REGISTER_AS, RV32_REGISTER_NUM_LIMBS};
3
4use crate::{
5    arch::{SystemConfig, BOUNDARY_AIR_ID, MERKLE_AIR_ID},
6    system::memory::{dimensions::MemoryDimensions, CHUNK},
7};
8
9/// CHUNK granularity (merkle leaf size) for page fault tracking.
10/// Must match the CHUNK used to compute `MemoryDimensions::address_height`.
11const CHUNK_U32: u32 = CHUNK as u32;
12const CHUNK_BITS: u32 = CHUNK_U32.ilog2();
13
14/// Upper bound on number of memory pages accessed per instruction. Used for buffer allocation.
15pub const MAX_MEM_PAGE_OPS_PER_INSN: usize = 1 << 16;
16
17#[derive(Clone, Debug)]
18pub struct BitSet {
19    words: Box<[u64]>,
20}
21
22impl BitSet {
23    pub fn new(num_bits: usize) -> Self {
24        Self {
25            words: vec![0; num_bits.div_ceil(u64::BITS as usize)].into_boxed_slice(),
26        }
27    }
28
29    #[inline(always)]
30    pub fn insert(&mut self, index: usize) -> bool {
31        let word_index = index >> 6;
32        let bit_index = index & 63;
33        let mask = 1u64 << bit_index;
34
35        debug_assert!(word_index < self.words.len(), "BitSet index out of bounds");
36
37        // SAFETY: word_index is derived from a memory address that is bounds-checked
38        //         during memory access. The bitset is sized to accommodate all valid
39        //         memory addresses, so word_index is always within bounds.
40        let word = unsafe { self.words.get_unchecked_mut(word_index) };
41        let was_set = (*word & mask) != 0;
42        *word |= mask;
43        !was_set
44    }
45
46    /// Set all bits within [start, end) to 1, return the number of flipped bits.
47    /// Assumes start < end and end <= self.words.len() * 64.
48    #[inline(always)]
49    pub fn insert_range(&mut self, start: usize, end: usize) -> usize {
50        debug_assert!(start < end);
51        debug_assert!(end <= self.words.len() * 64, "BitSet range out of bounds");
52
53        let mut ret = 0;
54        let start_word_index = start >> 6;
55        let end_word_index = (end - 1) >> 6;
56        let start_bit = (start & 63) as u32;
57
58        if start_word_index == end_word_index {
59            let end_bit = ((end - 1) & 63) as u32 + 1;
60            let mask_bits = end_bit - start_bit;
61            let mask = (u64::MAX >> (64 - mask_bits)) << start_bit;
62            // SAFETY: Caller ensures start < end and end <= self.words.len() * 64,
63            // so start_word_index < self.words.len()
64            let word = unsafe { self.words.get_unchecked_mut(start_word_index) };
65            ret += mask_bits - (*word & mask).count_ones();
66            *word |= mask;
67        } else {
68            let end_bit = (end & 63) as u32;
69            let mask_bits = 64 - start_bit;
70            let mask = u64::MAX << start_bit;
71            // SAFETY: Caller ensures start < end and end <= self.words.len() * 64,
72            // so start_word_index < self.words.len()
73            let start_word = unsafe { self.words.get_unchecked_mut(start_word_index) };
74            ret += mask_bits - (*start_word & mask).count_ones();
75            *start_word |= mask;
76
77            let mask_bits = end_bit;
78            let mask = if end_bit == 0 {
79                0
80            } else {
81                u64::MAX >> (64 - end_bit)
82            };
83            // SAFETY: Caller ensures end <= self.words.len() * 64, so
84            // end_word_index < self.words.len()
85            let end_word = unsafe { self.words.get_unchecked_mut(end_word_index) };
86            ret += mask_bits - (*end_word & mask).count_ones();
87            *end_word |= mask;
88        }
89
90        if start_word_index + 1 < end_word_index {
91            for i in (start_word_index + 1)..end_word_index {
92                // SAFETY: Caller ensures proper start and end, so i is within bounds
93                // of self.words.len()
94                let word = unsafe { self.words.get_unchecked_mut(i) };
95                ret += word.count_zeros();
96                *word = u64::MAX;
97            }
98        }
99        ret as usize
100    }
101
102    #[inline(always)]
103    pub fn clear(&mut self) {
104        // SAFETY: words is valid for self.words.len() elements
105        unsafe {
106            std::ptr::write_bytes(self.words.as_mut_ptr(), 0, self.words.len());
107        }
108    }
109}
110
111#[derive(Clone, Debug)]
112pub struct MemoryCtx<const PAGE_BITS: usize> {
113    memory_dimensions: MemoryDimensions,
114    pub page_indices: BitSet,
115    pub addr_space_access_count: RVec<u32>,
116    pub page_indices_since_checkpoint: Box<[u32]>,
117    pub page_indices_since_checkpoint_len: usize,
118}
119
120impl<const PAGE_BITS: usize> MemoryCtx<PAGE_BITS> {
121    pub fn new(config: &SystemConfig, segment_check_insns: u64) -> Self {
122        let memory_dimensions = config.memory_config.memory_dimensions();
123        let merkle_height = memory_dimensions.overall_height();
124
125        let bitset_size = 1 << (merkle_height.saturating_sub(PAGE_BITS));
126        let addr_space_size = (1 << memory_dimensions.addr_space_height) + 1;
127        let checkpoint_capacity = Self::calculate_checkpoint_capacity(segment_check_insns);
128
129        Self {
130            memory_dimensions,
131            page_indices: BitSet::new(bitset_size),
132            addr_space_access_count: vec![0; addr_space_size].into(),
133            page_indices_since_checkpoint: vec![0; checkpoint_capacity].into_boxed_slice(),
134            page_indices_since_checkpoint_len: 0,
135        }
136    }
137
138    #[inline(always)]
139    fn calculate_checkpoint_capacity(segment_check_insns: u64) -> usize {
140        segment_check_insns as usize * MAX_MEM_PAGE_OPS_PER_INSN
141    }
142
143    #[inline(always)]
144    pub(crate) fn add_register_merkle_heights(&mut self) {
145        self.update_boundary_merkle_heights(
146            RV32_REGISTER_AS,
147            0,
148            (RV32_NUM_REGISTERS * RV32_REGISTER_NUM_LIMBS) as u32,
149        );
150    }
151
152    /// For each memory access, record the minimal necessary data to update heights of
153    /// memory-related chips. The actual height updates happen during segment checks. The
154    /// implementation is in `lazy_update_boundary_heights`.
155    #[inline(always)]
156    pub(crate) fn update_boundary_merkle_heights(
157        &mut self,
158        address_space: u32,
159        ptr: u32,
160        size: u32,
161    ) {
162        debug_assert!((address_space as usize) < self.addr_space_access_count.len());
163
164        let chunk_idx = ptr >> CHUNK_BITS;
165        let end_chunk_idx = (ptr + size - 1) >> CHUNK_BITS;
166        let num_blocks = end_chunk_idx - chunk_idx + 1;
167        let start_block_id = self
168            .memory_dimensions
169            .label_to_index((address_space, chunk_idx)) as u32;
170        let end_block_id = start_block_id + num_blocks;
171        let start_page_id = start_block_id >> PAGE_BITS;
172        let end_page_id = ((end_block_id - 1) >> PAGE_BITS) + 1;
173        assert!(
174            self.page_indices_since_checkpoint_len + (end_page_id - start_page_id) as usize
175                <= self.page_indices_since_checkpoint.len(),
176            "more than {MAX_MEM_PAGE_OPS_PER_INSN} memory pages accessed in a single instruction"
177        );
178
179        for page_id in start_page_id..end_page_id {
180            // Append page_id to page_indices_since_checkpoint
181            let len = self.page_indices_since_checkpoint_len;
182            debug_assert!(len < self.page_indices_since_checkpoint.len());
183            // SAFETY: len is within bounds, and we extend length by 1 after writing.
184            unsafe {
185                *self.page_indices_since_checkpoint.as_mut_ptr().add(len) = page_id;
186            }
187            self.page_indices_since_checkpoint_len = len + 1;
188
189            if self.page_indices.insert(page_id as usize) {
190                // SAFETY: address_space passed is usually a hardcoded constant or derived from an
191                // Instruction where it is bounds checked before passing
192                unsafe {
193                    *self
194                        .addr_space_access_count
195                        .get_unchecked_mut(address_space as usize) += 1;
196                }
197            }
198        }
199    }
200
201    /// Initialize state for a new segment
202    #[inline(always)]
203    pub(crate) fn initialize_segment(&mut self, trace_heights: &mut [u32]) {
204        // Clear page indices for the new segment
205        self.page_indices.clear();
206
207        // Reset trace heights for memory chips as 0
208        // SAFETY: BOUNDARY_AIR_ID and MERKLE_AIR_ID are compile-time constants within bounds
209        unsafe {
210            *trace_heights.get_unchecked_mut(BOUNDARY_AIR_ID) = 0;
211            *trace_heights.get_unchecked_mut(MERKLE_AIR_ID) = 0;
212        }
213        let poseidon2_idx = trace_heights.len() - 2;
214        // SAFETY: poseidon2_idx is trace_heights.len() - 2, guaranteed to be in bounds
215        unsafe {
216            *trace_heights.get_unchecked_mut(poseidon2_idx) = 0;
217        }
218
219        // Apply height updates for all pages accessed since last checkpoint, and
220        // initialize page_indices for the new segment.
221        let mut addr_space_access_count = vec![0; self.addr_space_access_count.len()];
222        let pages_len = self.page_indices_since_checkpoint_len;
223        for i in 0..pages_len {
224            // SAFETY: i is within 0..pages_len and pages_len is the slice length.
225            let page_id = unsafe { *self.page_indices_since_checkpoint.get_unchecked(i) } as usize;
226            if self.page_indices.insert(page_id) {
227                let (addr_space, _) = self
228                    .memory_dimensions
229                    .index_to_label((page_id as u64) << PAGE_BITS);
230                let addr_space_idx = addr_space as usize;
231                debug_assert!(addr_space_idx < addr_space_access_count.len());
232                // SAFETY: addr_space_idx is bounds checked in debug and derived from a valid page
233                // id.
234                unsafe {
235                    *addr_space_access_count.get_unchecked_mut(addr_space_idx) += 1;
236                }
237            }
238        }
239        self.apply_height_updates(trace_heights, &addr_space_access_count);
240
241        // Add merkle height contributions for all registers
242        self.add_register_merkle_heights();
243        self.lazy_update_boundary_heights(trace_heights);
244    }
245
246    /// Updates the checkpoint with current safe state
247    #[inline(always)]
248    pub(crate) fn update_checkpoint(&mut self) {
249        self.page_indices_since_checkpoint_len = 0;
250    }
251
252    /// Overestimates trace heights from page faults.
253    ///
254    /// Memory leaves (CHUNK-sized) form a sparse merkle tree of height `h`. Each segment
255    /// maintains an initial and final tree, so all counts are doubled.
256    ///
257    /// On each page fault, we conservatively assume all `2^PAGE_BITS` leaves in the page
258    /// are touched and no merkle nodes are shared across pages:
259    ///
260    /// ```text
261    ///        [root]              height h
262    ///        /    \
263    ///      ...    ...
264    ///      /        \
265    ///   [page]    [page]         (h - PAGE_BITS) nodes above each page
266    ///   / .. \
267    ///  L  ..  L                  2^PAGE_BITS leaves, (2^PAGE_BITS - 1) internal nodes
268    /// ```
269    ///
270    /// Per page fault:
271    /// - BOUNDARY_AIR: `2 * 2^PAGE_BITS` rows (one init + one final row per leaf)
272    /// - MERKLE_AIR:   `2 * nodes_per_page` rows
273    /// - Poseidon2:    `2 * 2^PAGE_BITS` (leaf compression) + `2 * nodes_per_page` (tree hashes)
274    #[inline(always)]
275    fn apply_height_updates(&self, trace_heights: &mut [u32], addr_space_access_count: &[u32]) {
276        let page_access_count: u32 = addr_space_access_count.iter().sum();
277
278        // Leaves touched: conservatively assume every leaf in each faulted page is touched.
279        let leaves = page_access_count << PAGE_BITS;
280        debug_assert!(trace_heights.len() >= 2);
281        let poseidon2_idx = trace_heights.len() - 2;
282
283        let merkle_height = self.memory_dimensions.overall_height();
284        let nodes_per_page = (((1 << PAGE_BITS) - 1) + (merkle_height - PAGE_BITS)) as u32;
285        // SAFETY: BOUNDARY_AIR_ID, MERKLE_AIR_ID, and poseidon2_idx are all within bounds
286        unsafe {
287            *trace_heights.get_unchecked_mut(BOUNDARY_AIR_ID) += leaves * 2;
288            // Poseidon2: 2 hashes per leaf (compression) + 2 per internal node (init + final tree)
289            *trace_heights.get_unchecked_mut(poseidon2_idx) +=
290                leaves * 2 + nodes_per_page * page_access_count * 2;
291            // Merkle AIR: 2 rows per internal node (init + final tree)
292            *trace_heights.get_unchecked_mut(MERKLE_AIR_ID) +=
293                nodes_per_page * page_access_count * 2;
294        }
295    }
296
297    /// Resolve all lazy updates of each memory access for poseidon2/merkle chips.
298    #[inline(always)]
299    pub(crate) fn lazy_update_boundary_heights(&mut self, trace_heights: &mut [u32]) {
300        self.apply_height_updates(trace_heights, &self.addr_space_access_count);
301        // SAFETY: Resetting array elements to 0 is always safe
302        unsafe {
303            std::ptr::write_bytes(
304                self.addr_space_access_count.as_mut_ptr(),
305                0,
306                self.addr_space_access_count.len(),
307            );
308        }
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_bitset_insert_range() {
318        // 513 bits
319        let mut bit_set = BitSet::new(8 * 64 + 1);
320        let num_flips = bit_set.insert_range(2, 29);
321        assert_eq!(num_flips, 27);
322        let num_flips = bit_set.insert_range(1, 31);
323        assert_eq!(num_flips, 3);
324
325        let num_flips = bit_set.insert_range(32, 65);
326        assert_eq!(num_flips, 33);
327        let num_flips = bit_set.insert_range(0, 66);
328        assert_eq!(num_flips, 3);
329        let num_flips = bit_set.insert_range(0, 66);
330        assert_eq!(num_flips, 0);
331
332        let num_flips = bit_set.insert_range(256, 320);
333        assert_eq!(num_flips, 64);
334        let num_flips = bit_set.insert_range(256, 377);
335        assert_eq!(num_flips, 57);
336        let num_flips = bit_set.insert_range(100, 513);
337        assert_eq!(num_flips, 413 - 121);
338    }
339}