openvm_circuit/system/memory/online.rs
1use std::{array::from_fn, fmt::Debug};
2
3use getset::Getters;
4use openvm_instructions::exe::SparseMemoryImage;
5use openvm_stark_backend::{
6 p3_field::{Field, PrimeField32},
7 p3_maybe_rayon::prelude::*,
8};
9use tracing::instrument;
10
11use crate::{
12 arch::{AddressSpaceHostConfig, AddressSpaceHostLayout, MemoryConfig, DEFAULT_BLOCK_SIZE},
13 system::{memory::TimestampedValues, TouchedMemory},
14};
15
16mod basic;
17#[cfg(any(unix, windows))]
18mod memmap;
19mod paged_vec;
20
21#[cfg(not(any(unix, windows)))]
22pub use basic::*;
23#[cfg(any(unix, windows))]
24pub use memmap::*;
25pub use paged_vec::PagedVec;
26
27#[cfg(all(any(unix, windows), not(feature = "basic-memory")))]
28pub type MemoryBackend = memmap::MmapMemory;
29#[cfg(any(not(any(unix, windows)), feature = "basic-memory"))]
30pub type MemoryBackend = basic::BasicMemory;
31
32pub const INITIAL_TIMESTAMP: u32 = 0;
33/// Default mmap page size. Change this if using THB.
34pub const PAGE_SIZE: usize = 4096;
35
36/// (address_space, pointer)
37pub type Address = (u32, u32);
38
39/// API for any memory implementation that allocates a contiguous region of memory.
40pub trait LinearMemory {
41 /// Create instance of `Self` with `size` bytes.
42 fn new(size: usize) -> Self;
43 /// Allocated size of the memory in bytes.
44 fn size(&self) -> usize;
45 /// Returns the entire memory as a raw byte slice.
46 fn as_slice(&self) -> &[u8];
47 /// Returns the entire memory as a raw byte slice.
48 fn as_mut_slice(&mut self) -> &mut [u8];
49 /// Fill the memory with zeros.
50 fn fill_zero(&mut self) {
51 self.as_mut_slice().fill(0);
52 }
53 /// Read `BLOCK` from `self` at `from` address without moving it.
54 ///
55 /// Panics or segfaults if `from..from + size_of::<BLOCK>()` is out of bounds.
56 ///
57 /// # Safety
58 /// - `BLOCK` should be "plain old data" (see [`Pod`](https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html)).
59 /// We do not add a trait bound due to Plonky3 types not implementing the trait.
60 /// - See [`core::ptr::read`] for similar considerations.
61 /// - Memory at `from` must be properly aligned for `BLOCK`. Use [`Self::read_unaligned`] if
62 /// alignment is not guaranteed.
63 unsafe fn read<BLOCK: Copy>(&self, from: usize) -> BLOCK;
64 /// Read `BLOCK` from `self` at `from` address without moving it.
65 /// Same as [`Self::read`] except that it does not require alignment.
66 ///
67 /// Panics or segfaults if `from..from + size_of::<BLOCK>()` is out of bounds.
68 ///
69 /// # Safety
70 /// - `BLOCK` should be "plain old data" (see [`Pod`](https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html)).
71 /// We do not add a trait bound due to Plonky3 types not implementing the trait.
72 /// - See [`core::ptr::read`] for similar considerations.
73 unsafe fn read_unaligned<BLOCK: Copy>(&self, from: usize) -> BLOCK;
74 /// Write `BLOCK` to `self` at `start` address without reading the old value. Does not drop
75 /// `values`. Semantically, `values` is moved into the location pointed to by `start`.
76 ///
77 /// Panics or segfaults if `start..start + size_of::<BLOCK>()` is out of bounds.
78 ///
79 /// # Safety
80 /// - See [`core::ptr::write`] for similar considerations.
81 /// - Memory at `start` must be properly aligned for `BLOCK`. Use [`Self::write_unaligned`] if
82 /// alignment is not guaranteed.
83 unsafe fn write<BLOCK: Copy>(&mut self, start: usize, values: BLOCK);
84 /// Write `BLOCK` to `self` at `start` address without reading the old value. Does not drop
85 /// `values`. Semantically, `values` is moved into the location pointed to by `start`.
86 /// Same as [`Self::write`] but without alignment requirement.
87 ///
88 /// Panics or segfaults if `start..start + size_of::<BLOCK>()` is out of bounds.
89 ///
90 /// # Safety
91 /// - See [`core::ptr::write`] for similar considerations.
92 unsafe fn write_unaligned<BLOCK: Copy>(&mut self, start: usize, values: BLOCK);
93 /// Swaps `values` with memory at `start..start + size_of::<BLOCK>()`.
94 ///
95 /// Panics or segfaults if `start..start + size_of::<BLOCK>()` is out of bounds.
96 ///
97 /// # Safety
98 /// - `BLOCK` should be "plain old data" (see [`Pod`](https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html)).
99 /// We do not add a trait bound due to Plonky3 types not implementing the trait.
100 /// - Memory at `start` must be properly aligned for `BLOCK`.
101 /// - The data in `values` should not overlap with memory in `self`.
102 unsafe fn swap<BLOCK: Copy>(&mut self, start: usize, values: &mut BLOCK);
103 /// Copies `data` into memory at `to` address.
104 ///
105 /// Panics or segfaults if `to..to + size_of_val(data)` is out of bounds.
106 ///
107 /// # Safety
108 /// - `T` should be "plain old data" (see [`Pod`](https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html)).
109 /// We do not add a trait bound due to Plonky3 types not implementing the trait.
110 /// - The underlying memory of `data` should not overlap with `self`.
111 /// - The starting pointer of `self` should be aligned to `T`.
112 /// - The memory pointer at `to` should be aligned to `T`.
113 unsafe fn copy_nonoverlapping<T: Copy>(&mut self, to: usize, data: &[T]);
114 /// Returns a slice `&[T]` for the memory region `start..start + len`.
115 ///
116 /// Panics or segfaults if `start..start + len * size_of::<T>()` is out of bounds.
117 ///
118 /// # Safety
119 /// - `T` should be "plain old data" (see [`Pod`](https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html)).
120 /// We do not add a trait bound due to Plonky3 types not implementing the trait.
121 /// - Memory at `start` must be properly aligned for `T`.
122 unsafe fn get_aligned_slice<T: Copy>(&self, start: usize, len: usize) -> &[T];
123}
124
125/// Map from address space to linear memory.
126/// The underlying memory is typeless, stored as raw bytes, but usage implicitly assumes that each
127/// address space has memory cells of a fixed type (e.g., `u8, F`). We do not use a typemap for
128/// performance reasons, and it is up to the user to enforce types. Needless to say, this is a very
129/// `unsafe` API.
130#[derive(Debug, Clone)]
131#[repr(C)]
132pub struct AddressMap<M: LinearMemory = MemoryBackend> {
133 /// Underlying memory data.
134 pub mem: Vec<M>,
135 /// Host configuration for each address space.
136 pub config: Vec<AddressSpaceHostConfig>,
137}
138
139impl Default for AddressMap {
140 fn default() -> Self {
141 Self::from_mem_config(&MemoryConfig::default())
142 }
143}
144
145impl<M: LinearMemory> AddressMap<M> {
146 pub fn new(config: Vec<AddressSpaceHostConfig>) -> Self {
147 assert_eq!(config[0].num_cells, 0, "Address space 0 must have 0 cells");
148 let mem = config
149 .iter()
150 .map(|config| M::new(config.num_cells.checked_mul(config.layout.size()).unwrap()))
151 .collect();
152 Self { mem, config }
153 }
154
155 pub fn from_mem_config(mem_config: &MemoryConfig) -> Self {
156 Self::new(mem_config.addr_spaces.clone())
157 }
158
159 #[inline(always)]
160 pub fn get_memory(&self) -> &Vec<M> {
161 &self.mem
162 }
163
164 #[inline(always)]
165 pub fn get_memory_mut(&mut self) -> &mut Vec<M> {
166 &mut self.mem
167 }
168
169 /// Fill each address space memory with zeros. Does not change the config.
170 pub fn fill_zero(&mut self) {
171 for mem in &mut self.mem {
172 mem.fill_zero();
173 }
174 }
175
176 /// # Safety
177 /// - Assumes `addr_space` is within the configured memory and not out of bounds
178 pub unsafe fn get_f<F: PrimeField32>(&self, addr_space: u32, ptr: u32) -> F {
179 let layout = &self.config.get_unchecked(addr_space as usize).layout;
180 let start = ptr as usize * layout.size();
181 let bytes = self.get_u8_slice(addr_space, start, layout.size());
182 layout.to_field(bytes)
183 }
184
185 /// # Safety
186 /// - `T` **must** be the correct type for a single memory cell for `addr_space`
187 /// - Assumes `addr_space` is within the configured memory and not out of bounds
188 pub unsafe fn get<T: Copy>(&self, (addr_space, ptr): Address) -> T {
189 debug_assert_eq!(
190 size_of::<T>(),
191 self.config[addr_space as usize].layout.size()
192 );
193 // SAFETY:
194 // - alignment is automatic since we multiply by `size_of::<T>()`
195 self.mem
196 .get_unchecked(addr_space as usize)
197 .read((ptr as usize) * size_of::<T>())
198 }
199
200 /// Panics or segfaults if `ptr..ptr + len` is out of bounds
201 ///
202 /// # Safety
203 /// - `T` **must** be the correct type for a single memory cell for `addr_space`
204 /// - Assumes `addr_space` is within the configured memory and not out of bounds
205 pub unsafe fn get_slice<T: Copy + Debug>(
206 &self,
207 (addr_space, ptr): Address,
208 len: usize,
209 ) -> &[T] {
210 debug_assert_eq!(
211 size_of::<T>(),
212 self.config[addr_space as usize].layout.size()
213 );
214 let start = (ptr as usize) * size_of::<T>();
215 let mem = self.mem.get_unchecked(addr_space as usize);
216 // SAFETY:
217 // - alignment is automatic since we multiply by `size_of::<T>()`
218 mem.get_aligned_slice(start, len)
219 }
220
221 /// Reads the slice at **byte** addresses `start..start + len` from address space `addr_space`
222 /// linear memory. Panics or segfaults if `start..start + len` is out of bounds
223 ///
224 /// # Safety
225 /// - Assumes `addr_space` is within the configured memory and not out of bounds
226 pub unsafe fn get_u8_slice(&self, addr_space: u32, start: usize, len: usize) -> &[u8] {
227 let mem = self.mem.get_unchecked(addr_space as usize);
228 mem.get_aligned_slice(start, len)
229 }
230
231 /// Copies `data` into the memory at `(addr_space, ptr)`.
232 ///
233 /// Panics or segfaults if `ptr + size_of_val(data)` is out of bounds.
234 ///
235 /// # Safety
236 /// - `T` **must** be the correct type for a single memory cell for `addr_space`
237 /// - The linear memory in `addr_space` is aligned to `T`.
238 pub unsafe fn copy_slice_nonoverlapping<T: Copy>(
239 &mut self,
240 (addr_space, ptr): Address,
241 data: &[T],
242 ) {
243 let start = (ptr as usize) * size_of::<T>();
244 // SAFETY:
245 // - Linear memory is aligned to `T` and `start` is multiple of `size_of::<T>()` so
246 // alignment is satisfied.
247 // - `data` and `self.mem` are non-overlapping
248 self.mem
249 .get_unchecked_mut(addr_space as usize)
250 .copy_nonoverlapping(start, data);
251 }
252
253 /// # Safety
254 /// - `T` **must** be the correct type for a single memory cell for `addr_space`
255 /// - Assumes `addr_space` is within the configured memory and not out of bounds
256 pub fn set_from_sparse(&mut self, sparse_map: &SparseMemoryImage) {
257 for (&(addr_space, index), &data_byte) in sparse_map.iter() {
258 // SAFETY:
259 // - safety assumptions in function doc comments
260 unsafe {
261 self.mem
262 .get_unchecked_mut(addr_space as usize)
263 .write_unaligned(index as usize, data_byte);
264 }
265 }
266 }
267}
268
269/// API for guest memory conforming to OpenVM ISA
270// @dev Note we don't make this a trait because phantom executors currently need a concrete type for
271// guest memory
272#[derive(Debug, Clone)]
273#[repr(C)]
274pub struct GuestMemory {
275 pub memory: AddressMap,
276}
277
278impl GuestMemory {
279 pub fn new(addr: AddressMap) -> Self {
280 Self { memory: addr }
281 }
282
283 /// Returns `[pointer:BLOCK_SIZE]_{address_space}`
284 ///
285 /// # Safety
286 /// The type `T` must be stack-allocated `repr(C)` or `repr(transparent)`,
287 /// and it must be the exact type used to represent a single memory cell in
288 /// address space `address_space`. For standard usage,
289 /// `T` is either `u8` or `F` where `F` is the base field of the ZK backend.
290 #[inline(always)]
291 pub unsafe fn read<T, const BLOCK_SIZE: usize>(
292 &self,
293 addr_space: u32,
294 ptr: u32,
295 ) -> [T; BLOCK_SIZE]
296 where
297 T: Copy + Debug,
298 {
299 self.debug_assert_cell_type::<T>(addr_space);
300 // SAFETY:
301 // - `T` should be "plain old data"
302 // - alignment for `[T; BLOCK_SIZE]` is automatic since we multiply by `size_of::<T>()`
303 self.memory
304 .get_memory()
305 .get_unchecked(addr_space as usize)
306 .read((ptr as usize) * size_of::<T>())
307 }
308
309 /// Writes `values` to `[pointer:BLOCK_SIZE]_{address_space}`
310 ///
311 /// # Safety
312 /// See [`GuestMemory::read`].
313 #[inline(always)]
314 pub unsafe fn write<T, const BLOCK_SIZE: usize>(
315 &mut self,
316 addr_space: u32,
317 ptr: u32,
318 values: [T; BLOCK_SIZE],
319 ) where
320 T: Copy + Debug,
321 {
322 self.debug_assert_cell_type::<T>(addr_space);
323 // SAFETY:
324 // - alignment for `[T; BLOCK_SIZE]` is automatic since we multiply by `size_of::<T>()`
325 self.memory
326 .get_memory_mut()
327 .get_unchecked_mut(addr_space as usize)
328 .write((ptr as usize) * size_of::<T>(), values);
329 }
330
331 /// Swaps `values` with `[pointer:BLOCK_SIZE]_{address_space}`.
332 ///
333 /// # Safety
334 /// See [`GuestMemory::read`] and [`LinearMemory::swap`].
335 #[inline(always)]
336 pub unsafe fn swap<T, const BLOCK_SIZE: usize>(
337 &mut self,
338 addr_space: u32,
339 ptr: u32,
340 values: &mut [T; BLOCK_SIZE],
341 ) where
342 T: Copy + Debug,
343 {
344 self.debug_assert_cell_type::<T>(addr_space);
345 // SAFETY:
346 // - alignment for `[T; BLOCK_SIZE]` is automatic since we multiply by `size_of::<T>()`
347 self.memory
348 .get_memory_mut()
349 .get_unchecked_mut(addr_space as usize)
350 .swap((ptr as usize) * size_of::<T>(), values);
351 }
352
353 #[inline(always)]
354 #[allow(clippy::missing_safety_doc)]
355 pub unsafe fn get_slice<T: Copy + Debug>(&self, addr_space: u32, ptr: u32, len: usize) -> &[T] {
356 self.memory.get_slice((addr_space, ptr), len)
357 }
358
359 #[inline(always)]
360 fn debug_assert_cell_type<T>(&self, addr_space: u32) {
361 debug_assert_eq!(
362 size_of::<T>(),
363 self.memory.config[addr_space as usize].layout.size()
364 );
365 }
366}
367
368/// Online memory that stores additional information for trace generation purposes.
369/// In particular, keeps track of timestamp.
370#[derive(Getters)]
371pub struct TracingMemory {
372 pub timestamp: u32,
373 /// The underlying data memory, with memory cells typed by address space: see [AddressMap].
374 #[getset(get = "pub")]
375 pub data: GuestMemory,
376 /// Maps `(addr_space, ptr / DEFAULT_BLOCK_SIZE)` to the latest access timestamp.
377 /// A value of 0 means the 4-cell touched-memory slot has never been accessed.
378 pub(super) meta: Vec<PagedVec<u32, PAGE_SIZE>>,
379}
380
381impl TracingMemory {
382 pub fn new(mem_config: &MemoryConfig) -> Self {
383 let image = GuestMemory::new(AddressMap::from_mem_config(mem_config));
384 Self::from_image(image)
385 }
386
387 /// Constructor from pre-existing memory image.
388 pub fn from_image(image: GuestMemory) -> Self {
389 let meta = image
390 .memory
391 .config
392 .iter()
393 .map(|config| PagedVec::new(config.num_cells.div_ceil(DEFAULT_BLOCK_SIZE)))
394 .collect();
395 Self {
396 data: image,
397 meta,
398 timestamp: INITIAL_TIMESTAMP + 1,
399 }
400 }
401
402 #[inline(always)]
403 fn assert_valid_access(&self, block_size: usize, addr_space: u32, ptr: u32) {
404 debug_assert_ne!(addr_space, 0);
405 debug_assert!(block_size.is_power_of_two());
406 debug_assert_eq!(
407 block_size, DEFAULT_BLOCK_SIZE,
408 "TracingMemory only supports {DEFAULT_BLOCK_SIZE}-cell accesses; got {block_size}"
409 );
410 assert_eq!(
411 ptr % block_size as u32,
412 0,
413 "pointer={ptr} not aligned to block_size {block_size}"
414 );
415 }
416
417 /// Returns the previous access timestamp and updates the metadata slot.
418 /// Block size is always `DEFAULT_BLOCK_SIZE`, so this is a single-slot read/write.
419 #[inline(always)]
420 fn prev_access_time(&mut self, address_space: usize, pointer: usize) -> u32 {
421 let idx = pointer / DEFAULT_BLOCK_SIZE;
422 // SAFETY: address_space is validated during instruction decoding
423 let meta_page = unsafe { self.meta.get_unchecked_mut(address_space) };
424 let prev = meta_page.get(idx);
425 meta_page.set(idx, self.timestamp);
426 prev
427 }
428
429 /// Atomic read operation which increments the timestamp by 1.
430 /// Returns `(t_prev, [pointer:BLOCK_SIZE]_{address_space})`.
431 ///
432 /// # Safety
433 /// - `T` must be `repr(C)` or `repr(transparent)` and match the cell type for `address_space`.
434 /// - `address_space` must be valid.
435 /// - `BLOCK_SIZE` is measured in memory cells and is tracked in fixed `DEFAULT_BLOCK_SIZE`
436 /// touched-memory slots.
437 #[inline(always)]
438 pub unsafe fn read<T, const BLOCK_SIZE: usize>(
439 &mut self,
440 address_space: u32,
441 pointer: u32,
442 ) -> (u32, [T; BLOCK_SIZE])
443 where
444 T: Copy + Debug,
445 {
446 self.assert_valid_access(BLOCK_SIZE, address_space, pointer);
447 let values = self.data.read(address_space, pointer);
448 let t_prev = self.prev_access_time(address_space as usize, pointer as usize);
449 self.timestamp += 1;
450
451 (t_prev, values)
452 }
453
454 /// Atomic write operation. Returns `(t_prev, values_prev)`.
455 ///
456 /// # Safety
457 /// - `T` must be `repr(C)` or `repr(transparent)` and match the cell type for `address_space`.
458 /// - `address_space` must be valid.
459 /// - `BLOCK_SIZE` is measured in memory cells and is tracked in fixed `DEFAULT_BLOCK_SIZE`
460 /// touched-memory slots.
461 #[inline(always)]
462 pub unsafe fn write<T, const BLOCK_SIZE: usize>(
463 &mut self,
464 address_space: u32,
465 pointer: u32,
466 values: [T; BLOCK_SIZE],
467 ) -> (u32, [T; BLOCK_SIZE])
468 where
469 T: Copy + Debug,
470 {
471 self.assert_valid_access(BLOCK_SIZE, address_space, pointer);
472 let values_prev = self.data.read(address_space, pointer);
473 let t_prev = self.prev_access_time(address_space as usize, pointer as usize);
474 self.data.write(address_space, pointer, values);
475 self.timestamp += 1;
476
477 (t_prev, values_prev)
478 }
479
480 pub fn increment_timestamp(&mut self) {
481 self.timestamp += 1;
482 }
483
484 pub fn increment_timestamp_by(&mut self, amount: u32) {
485 self.timestamp += amount;
486 }
487
488 pub fn timestamp(&self) -> u32 {
489 self.timestamp
490 }
491
492 /// Finalize the boundary and merkle chips.
493 #[instrument(name = "memory_finalize", skip_all)]
494 pub fn finalize<F: Field>(&mut self) -> TouchedMemory<F> {
495 self.touched_blocks_to_equipartition::<F>(self.touched_blocks())
496 }
497
498 /// Returns the list of all touched blocks (address, timestamp), sorted by address.
499 fn touched_blocks(&self) -> Vec<(Address, u32)> {
500 assert_eq!(self.meta.len(), self.data.memory.config.len());
501 let mut touched_blocks: Vec<_> = self
502 .meta
503 .par_iter()
504 .enumerate()
505 .flat_map_iter(|(addr_space, meta_page)| {
506 meta_page
507 .par_iter()
508 .filter_map(move |(idx, timestamp)| {
509 if timestamp > INITIAL_TIMESTAMP {
510 let ptr = idx as u32 * DEFAULT_BLOCK_SIZE as u32;
511 Some(((addr_space as u32, ptr), timestamp))
512 } else {
513 None
514 }
515 })
516 .collect::<Vec<_>>()
517 })
518 .collect();
519 // This sort may not be strictly necessary, but it makes the finalize path independent of
520 // Rayon ordering.
521 touched_blocks.sort_unstable_by_key(|(addr, _)| *addr);
522 touched_blocks
523 }
524
525 /// Returns the fixed 4-byte touched memory equipartition.
526 fn touched_blocks_to_equipartition<F: Field>(
527 &self,
528 touched_blocks: Vec<((u32, u32), u32)>,
529 ) -> TouchedMemory<F> {
530 debug_assert!(touched_blocks.is_sorted_by_key(|(addr, _)| addr));
531 touched_blocks
532 .into_par_iter()
533 .map(|((addr_space, ptr), timestamp)| {
534 let addr_space_config = &self.data.memory.config[addr_space as usize];
535 let cell_size = addr_space_config.layout.size();
536 let values = from_fn(|i| unsafe {
537 addr_space_config
538 .layout
539 .to_field(self.data.memory.get_u8_slice(
540 addr_space,
541 (ptr as usize + i) * cell_size,
542 cell_size,
543 ))
544 });
545 ((addr_space, ptr), TimestampedValues { timestamp, values })
546 })
547 .collect()
548 }
549}