Skip to content

Commit b788e45

Browse files
feat: add host configurable memory cell type to MemoryConfig (#1903)
Add trait `AddressSpaceHostLayout` with concrete implementation as enum `MemoryCellType`. Add `AddressSpaceHostConfig` struct and now `MemoryConfig` stores a vec of these, one per address space. Now all handling of conversions from raw bytes to `F` is done through this config. Comparison: https://github.com/axiom-crypto/openvm-reth-benchmark/actions/runs/16532679208 Memory finalize time seems to have increased significantly percentage-wise, but it is still on the order of milliseconds so I think negligible? --------- Co-authored-by: Golovanov399 <[email protected]>
1 parent e5e4dbc commit b788e45

File tree

25 files changed

+424
-261
lines changed

25 files changed

+424
-261
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ and this project follows a versioning principles documented in [VERSIONING.md](.
77

88
## [Unreleased]
99

10+
### Added
11+
- (Config) Added `addr_spaces` vector of `AddressSpaceHostConfig` to `MemoryConfig`.
12+
1013
### Changed
1114
- (Toolchain) Removed `step` from `Program` struct because `DEFAULT_PC_STEP = 4` is always used.
15+
- (Config) The `clk_max_bits` field in `MemoryConfig` has been renamed to `timestamp_max_bits`.
16+
- (Prover) Guest memory is stored on host with address space-specified memory layouts. In particular address space `1` through `3` are now represented in bytes instead of field elements.
1217

1318
## v1.3.0 (2025-07-15)
1419

crates/sdk/src/config/global.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ impl From<SdkVmConfigWithDefaultDeser> for SdkVmConfig {
412412
if config.native.is_none() && config.castf.is_none() {
413413
// There should be no need to write to native address space if Native extension and
414414
// CastF extension are not enabled.
415-
system.config.memory_config.addr_space_sizes[NATIVE_AS as usize] = 0;
415+
system.config.memory_config.addr_spaces[NATIVE_AS as usize].num_cells = 0;
416416
}
417417
Self {
418418
system,

crates/vm/src/arch/config.rs

Lines changed: 135 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ use std::{fs::File, io::Write, path::Path};
22

33
use derive_new::new;
44
use getset::{Setters, WithSetters};
5-
use openvm_instructions::NATIVE_AS;
5+
use openvm_instructions::{
6+
riscv::{RV32_IMM_AS, RV32_MEMORY_AS, RV32_REGISTER_AS},
7+
NATIVE_AS,
8+
};
69
use openvm_poseidon2_air::Poseidon2Config;
710
use openvm_stark_backend::{
811
config::{StarkGenericConfig, Val},
@@ -18,7 +21,9 @@ use crate::{
1821
Arena, ChipInventoryError, ExecutorInventory, ExecutorInventoryError,
1922
},
2023
system::{
21-
memory::{merkle::public_values::PUBLIC_VALUES_AS, num_memory_airs, CHUNK},
24+
memory::{
25+
merkle::public_values::PUBLIC_VALUES_AS, num_memory_airs, CHUNK, POINTER_MAX_BITS,
26+
},
2227
SystemChipComplex,
2328
},
2429
};
@@ -105,6 +110,10 @@ where
105110

106111
pub const OPENVM_DEFAULT_INIT_FILE_BASENAME: &str = "openvm_init";
107112
pub const OPENVM_DEFAULT_INIT_FILE_NAME: &str = "openvm_init.rs";
113+
/// The minimum block size is 4, but RISC-V `lb` only requires alignment of 1 and `lh` only requires
114+
/// alignment of 2 because the instructions are implemented by doing an access of block size 4.
115+
const DEFAULT_U8_BLOCK_SIZE: usize = 4;
116+
const DEFAULT_NATIVE_BLOCK_SIZE: usize = 1;
108117

109118
/// Trait for generating a init.rs file that contains a call to moduli_init!,
110119
/// complex_init!, sw_init! with the supported moduli and curves.
@@ -132,18 +141,36 @@ pub trait InitFileGenerator {
132141
}
133142
}
134143

144+
/// Each address space in guest memory may be configured with a different type `T` to represent a
145+
/// memory cell in the address space. On host, the address space will be mapped to linear host
146+
/// memory in bytes. The type `T` must be plain old data (POD) and be safely transmutable from a
147+
/// fixed size array of bytes. Moreover, each type `T` must be convertible to a field element `F`.
148+
///
149+
/// We currently implement this trait on the enum [MemoryCellType], which includes all cell types
150+
/// that we expect to be used in the VM context.
151+
pub trait AddressSpaceHostLayout {
152+
/// Size in bytes of the memory cell type.
153+
fn size(&self) -> usize;
154+
155+
/// # Safety
156+
/// - This function must only be called when `value` is guaranteed to be of size `self.size()`.
157+
/// - Alignment of `value` must be a multiple of the alignment of `F`.
158+
/// - The field type `F` must be plain old data.
159+
unsafe fn to_field<F: Field>(&self, value: &[u8]) -> F;
160+
}
161+
135162
#[derive(Debug, Serialize, Deserialize, Clone, new)]
136163
pub struct MemoryConfig {
137164
/// The maximum height of the address space. This means the trie has `addr_space_height` layers
138165
/// for searching the address space. The allowed address spaces are those in the range `[1,
139166
/// 1 + 2^addr_space_height)` where it starts from 1 to not allow address space 0 in memory.
140167
pub addr_space_height: usize,
141-
/// The number of cells in each address space. It is expected that the size of the list is
142-
/// `1 << addr_space_height + 1` and the first element is 0, which means no address space.
143-
pub addr_space_sizes: Vec<usize>,
168+
/// It is expected that the size of the list is `(1 << addr_space_height) + 1` and the first
169+
/// element is 0, which means no address space.
170+
pub addr_spaces: Vec<AddressSpaceHostConfig>,
144171
pub pointer_max_bits: usize,
145-
/// All timestamps must be in the range `[0, 2^clk_max_bits)`. Maximum allowed: 29.
146-
pub clk_max_bits: usize,
172+
/// All timestamps must be in the range `[0, 2^timestamp_max_bits)`. Maximum allowed: 29.
173+
pub timestamp_max_bits: usize,
147174
/// Limb size used by the range checker
148175
pub decomp: usize,
149176
/// Maximum N AccessAdapter AIR to support.
@@ -152,19 +179,46 @@ pub struct MemoryConfig {
152179

153180
impl Default for MemoryConfig {
154181
fn default() -> Self {
155-
let mut addr_space_sizes = vec![0; (1 << 3) + ADDR_SPACE_OFFSET as usize];
156-
addr_space_sizes[ADDR_SPACE_OFFSET as usize..=NATIVE_AS as usize].fill(1 << 29);
157-
addr_space_sizes[PUBLIC_VALUES_AS as usize] = DEFAULT_MAX_NUM_PUBLIC_VALUES;
158-
Self::new(3, addr_space_sizes, 29, 29, 17, 32)
182+
let mut addr_spaces =
183+
Self::empty_address_space_configs((1 << 3) + ADDR_SPACE_OFFSET as usize);
184+
const MAX_CELLS: usize = 1 << 29;
185+
addr_spaces[RV32_REGISTER_AS as usize].num_cells = 32 * size_of::<u32>();
186+
addr_spaces[RV32_MEMORY_AS as usize].num_cells = MAX_CELLS;
187+
addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = DEFAULT_MAX_NUM_PUBLIC_VALUES;
188+
addr_spaces[NATIVE_AS as usize].num_cells = MAX_CELLS;
189+
Self::new(3, addr_spaces, POINTER_MAX_BITS, 29, 17, 32)
159190
}
160191
}
161192

162193
impl MemoryConfig {
194+
pub fn empty_address_space_configs(num_addr_spaces: usize) -> Vec<AddressSpaceHostConfig> {
195+
// All except address spaces 0..4 default to native 32-bit field.
196+
// By default only address spaces 1..=4 have non-empty cell counts.
197+
let mut addr_spaces = vec![
198+
AddressSpaceHostConfig::new(
199+
0,
200+
DEFAULT_NATIVE_BLOCK_SIZE,
201+
MemoryCellType::native32()
202+
);
203+
num_addr_spaces
204+
];
205+
addr_spaces[RV32_IMM_AS as usize] = AddressSpaceHostConfig::new(0, 1, MemoryCellType::Null);
206+
addr_spaces[RV32_REGISTER_AS as usize] =
207+
AddressSpaceHostConfig::new(0, DEFAULT_U8_BLOCK_SIZE, MemoryCellType::U8);
208+
addr_spaces[RV32_MEMORY_AS as usize] =
209+
AddressSpaceHostConfig::new(0, DEFAULT_U8_BLOCK_SIZE, MemoryCellType::U8);
210+
addr_spaces[PUBLIC_VALUES_AS as usize] =
211+
AddressSpaceHostConfig::new(0, DEFAULT_U8_BLOCK_SIZE, MemoryCellType::U8);
212+
213+
addr_spaces
214+
}
215+
163216
/// Config for aggregation usage with only native address space.
164217
pub fn aggregation() -> Self {
165-
let mut addr_space_sizes = vec![0; (1 << 3) + ADDR_SPACE_OFFSET as usize];
166-
addr_space_sizes[NATIVE_AS as usize] = 1 << 29;
167-
Self::new(3, addr_space_sizes, 29, 29, 17, 8)
218+
let mut addr_spaces =
219+
Self::empty_address_space_configs((1 << 3) + ADDR_SPACE_OFFSET as usize);
220+
addr_spaces[NATIVE_AS as usize].num_cells = 1 << 29;
221+
Self::new(3, addr_spaces, POINTER_MAX_BITS, 29, 17, 8)
168222
}
169223
}
170224

@@ -209,10 +263,10 @@ impl SystemConfig {
209263
num_public_values: usize,
210264
) -> Self {
211265
assert!(
212-
memory_config.clk_max_bits <= 29,
266+
memory_config.timestamp_max_bits <= 29,
213267
"Timestamp max bits must be <= 29 for LessThan to work in 31-bit field"
214268
);
215-
memory_config.addr_space_sizes[PUBLIC_VALUES_AS as usize] = num_public_values;
269+
memory_config.addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = num_public_values;
216270
Self {
217271
max_constraint_degree,
218272
continuation_enabled: false,
@@ -243,7 +297,7 @@ impl SystemConfig {
243297

244298
pub fn with_public_values(mut self, num_public_values: usize) -> Self {
245299
self.num_public_values = num_public_values;
246-
self.memory_config.addr_space_sizes[PUBLIC_VALUES_AS as usize] = num_public_values;
300+
self.memory_config.addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = num_public_values;
247301
self
248302
}
249303

@@ -316,3 +370,67 @@ impl AsMut<SystemConfig> for SystemConfig {
316370

317371
// Default implementation uses no init file
318372
impl InitFileGenerator for SystemConfig {}
373+
374+
#[derive(Debug, Serialize, Deserialize, Clone, Copy, new)]
375+
pub struct AddressSpaceHostConfig {
376+
/// The number of cells in each address space.
377+
pub num_cells: usize,
378+
/// Minimum block size for memory accesses supported. This is a property of the address space
379+
/// that is determined by the ISA.
380+
///
381+
/// **Note**: Block size is in terms of memory cells.
382+
pub min_block_size: usize,
383+
pub layout: MemoryCellType,
384+
}
385+
386+
pub(crate) const MAX_CELL_BYTE_SIZE: usize = 8;
387+
388+
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
389+
pub enum MemoryCellType {
390+
Null,
391+
U8,
392+
U16,
393+
/// Represented in little-endian format.
394+
U32,
395+
/// `size` is the size in bytes of the native field type. This should not exceed 8.
396+
Native {
397+
size: u8,
398+
},
399+
}
400+
401+
impl MemoryCellType {
402+
pub fn native32() -> Self {
403+
Self::Native {
404+
size: size_of::<u32>() as u8,
405+
}
406+
}
407+
}
408+
409+
impl AddressSpaceHostLayout for MemoryCellType {
410+
fn size(&self) -> usize {
411+
match self {
412+
Self::Null => 1, // to avoid divide by zero
413+
Self::U8 => size_of::<u8>(),
414+
Self::U16 => size_of::<u16>(),
415+
Self::U32 => size_of::<u32>(),
416+
Self::Native { size } => *size as usize,
417+
}
418+
}
419+
420+
/// # Safety
421+
/// - This function must only be called when `value` is guaranteed to be of size `self.size()`.
422+
/// - Alignment of `value` must be a multiple of the alignment of `F`.
423+
/// - The field type `F` must be plain old data.
424+
///
425+
/// # Panics
426+
/// If the value is of integer type and overflows the field.
427+
unsafe fn to_field<F: Field>(&self, value: &[u8]) -> F {
428+
match self {
429+
Self::Null => unreachable!(),
430+
Self::U8 => F::from_canonical_u8(*value.get_unchecked(0)),
431+
Self::U16 => F::from_canonical_u16(core::ptr::read(value.as_ptr() as *const u16)),
432+
Self::U32 => F::from_canonical_u32(core::ptr::read(value.as_ptr() as *const u32)),
433+
Self::Native { .. } => core::ptr::read(value.as_ptr() as *const F),
434+
}
435+
}
436+
}

crates/vm/src/arch/execution_mode/metered/ctx.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ pub const DEFAULT_SEGMENT_CHECK_INSNS: u64 = 1000;
2525
#[derive(Clone, Debug, WithSetters)]
2626
pub struct MeteredCtx<const PAGE_BITS: usize = DEFAULT_PAGE_BITS> {
2727
pub trace_heights: Vec<u32>,
28-
// TODO[jpw]: should this be in Ctrl?
2928
pub is_trace_height_constant: Vec<bool>,
3029

3130
pub memory_ctx: MemoryCtx<PAGE_BITS>,

crates/vm/src/arch/extensions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ where
583583
}
584584

585585
pub fn timestamp_max_bits(&self) -> usize {
586-
self.airs.config().memory_config.clk_max_bits
586+
self.airs.config().memory_config.timestamp_max_bits
587587
}
588588
}
589589

crates/vm/src/arch/testing/memory/mod.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,10 @@ impl<F: PrimeField32> MemoryTester<F> {
3535
}
3636
}
3737

38-
// TODO: change interface by implementing GuestMemory trait after everything works
3938
pub fn read<const N: usize>(&mut self, addr_space: usize, ptr: usize) -> [F; N] {
4039
let memory = &mut self.memory;
4140
let t = memory.timestamp();
42-
// TODO: hack
41+
// TODO: this could be improved if we added a TracingMemory::get_f function
4342
let (t_prev, data) = if addr_space <= 3 {
4443
let (t_prev, data) = unsafe { memory.read::<u8, N, 4>(addr_space as u32, ptr as u32) };
4544
(t_prev, data.map(F::from_canonical_u8))
@@ -60,11 +59,10 @@ impl<F: PrimeField32> MemoryTester<F> {
6059
data
6160
}
6261

63-
// TODO: see read
6462
pub fn write<const N: usize>(&mut self, addr_space: usize, ptr: usize, data: [F; N]) {
6563
let memory = &mut self.memory;
6664
let t = memory.timestamp();
67-
// TODO: hack
65+
// TODO: this could be improved if we added a TracingMemory::write_f function
6866
let (t_prev, data_prev) = if addr_space <= 3 {
6967
let (t_prev, data_prev) = unsafe {
7068
memory.write::<u8, N, 4>(

crates/vm/src/arch/testing/mod.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use openvm_circuit_primitives::{
77
SharedVariableRangeCheckerChip, VariableRangeCheckerBus, VariableRangeCheckerChip,
88
},
99
};
10-
use openvm_instructions::{instruction::Instruction, NATIVE_AS};
10+
use openvm_instructions::{instruction::Instruction, riscv::RV32_REGISTER_AS, NATIVE_AS};
1111
use openvm_stark_backend::{
1212
config::{StarkGenericConfig, Val},
1313
engine::VerificationData,
@@ -272,7 +272,7 @@ impl<F: PrimeField32> VmChipTestBuilder<F> {
272272
}
273273

274274
pub fn address_bits(&self) -> usize {
275-
self.memory.controller.mem_config.pointer_max_bits
275+
self.memory.controller.memory_config().pointer_max_bits
276276
}
277277

278278
pub fn get_default_register(&mut self, increment: usize) -> usize {
@@ -336,7 +336,8 @@ impl VmChipTestBuilder<BabyBear> {
336336
impl<F: PrimeField32> VmChipTestBuilder<F> {
337337
pub fn default_persistent() -> Self {
338338
let mut mem_config = MemoryConfig::default();
339-
mem_config.addr_space_sizes[NATIVE_AS as usize] = 0;
339+
mem_config.addr_spaces[RV32_REGISTER_AS as usize].num_cells = 1 << 29;
340+
mem_config.addr_spaces[NATIVE_AS as usize].num_cells = 0;
340341
Self::persistent(mem_config)
341342
}
342343

@@ -411,7 +412,10 @@ impl<F: PrimeField32> VmChipTestBuilder<F> {
411412
impl<F: PrimeField32> Default for VmChipTestBuilder<F> {
412413
fn default() -> Self {
413414
let mut mem_config = MemoryConfig::default();
414-
mem_config.addr_space_sizes[NATIVE_AS as usize] = 0;
415+
// TODO[jpw]: this is because old tests use `gen_pointer` on address space 1; this can be
416+
// removed when tests are updated.
417+
mem_config.addr_spaces[RV32_REGISTER_AS as usize].num_cells = 1 << 29;
418+
mem_config.addr_spaces[NATIVE_AS as usize].num_cells = 0;
415419
Self::volatile(mem_config)
416420
}
417421
}
@@ -487,7 +491,7 @@ where
487491
}
488492
let mem_inventory = MemoryAirInventory::new(
489493
memory_controller.memory_bridge(),
490-
&memory_controller.mem_config,
494+
memory_controller.memory_config(),
491495
range_checker.bus(),
492496
is_persistent.then_some((
493497
PermutationCheckBus::new(MEMORY_MERKLE_BUS),

crates/vm/src/arch/vm.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,11 @@ where
237237
interactions: &[usize],
238238
) -> MeteredCtx {
239239
let system_config = self.config.as_ref();
240-
let num_addr_sp = 1 + (1 << system_config.memory_config.addr_space_height);
241-
let mut min_block_size = vec![1; num_addr_sp];
242-
// TMP: hardcoding for now
243-
// TODO[jpw]: move to mem_config
244-
min_block_size[1] = 4;
245-
min_block_size[2] = 4;
246-
min_block_size[3] = 4;
247-
let as_byte_alignment_bits = min_block_size
240+
let as_byte_alignment_bits = system_config
241+
.memory_config
242+
.addr_spaces
248243
.iter()
249-
.map(|&x| log2_strict_usize(x as usize) as u8)
244+
.map(|addr_sp| log2_strict_usize(addr_sp.min_block_size) as u8)
250245
.collect();
251246

252247
MeteredCtx::new(
@@ -543,7 +538,6 @@ where
543538
);
544539
let memory = TracingMemory::from_image(
545540
state.memory,
546-
&system_config.memory_config,
547541
system_config.initial_block_size(),
548542
access_adapter_arena_size_bound,
549543
);
@@ -1201,7 +1195,7 @@ pub(super) fn create_memory_image(
12011195
init_memory: SparseMemoryImage,
12021196
) -> GuestMemory {
12031197
GuestMemory::new(AddressMap::from_sparse(
1204-
memory_config.addr_space_sizes.clone(),
1198+
memory_config.addr_spaces.clone(),
12051199
init_memory,
12061200
))
12071201
}

0 commit comments

Comments
 (0)