feat: 2^32 memory address - #2850
Closed
GunaDD wants to merge 162 commits into
Closed
Conversation
Contributor
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
GunaDD
force-pushed
the
perf/merkle-tree-mem-opt
branch
3 times, most recently
from
June 8, 2026 16:01
52efe21 to
4c9cea5
Compare
GunaDD
force-pushed
the
feat/2-pow-32-memory-addresses
branch
2 times, most recently
from
June 8, 2026 19:59
e5623d6 to
50cec8e
Compare
GunaDD
marked this pull request as draft
June 9, 2026 14:14
GunaDD
marked this pull request as ready for review
June 9, 2026 14:14
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
GunaDD
force-pushed
the
perf/merkle-tree-mem-opt
branch
from
June 10, 2026 22:09
814c620 to
0e6d6a6
Compare
GunaDD
force-pushed
the
feat/2-pow-32-memory-addresses
branch
from
June 10, 2026 22:15
90b629c to
395639e
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
GunaDD
force-pushed
the
feat/2-pow-32-memory-addresses
branch
from
June 11, 2026 16:19
efee83a to
f338acf
Compare
This comment has been minimized.
This comment has been minimized.
GunaDD
force-pushed
the
feat/2-pow-32-memory-addresses
branch
from
June 18, 2026 20:45
f338acf to
e5f8e9c
Compare
This comment has been minimized.
This comment has been minimized.
shuklaayush
force-pushed
the
develop-v2.1.0-rv64
branch
3 times, most recently
from
June 19, 2026 22:15
ddef172 to
5e8a1fc
Compare
makes the basic framework for using rvr extensions in openvm - adds an rvr feature flag - defines `RvrExtensionCtx` struct to provide mappings between opcode/executor/air indices - defines `VmRvrExtension` trait that extensions can implement to be registered - updated macro so that rvr `ExtensionRegistry` can be auto-generated in `SdkVmConfig` closes INT-7474, INT-7475, INT-7479
update rvr to have 64bit PC. This PR consist of multiple parts: 1) Updating Intermediate Representations to have 64bit PC instead of 32bit 2) Updating Lifter to use 64bit PC 3) Updating Emitter to use 64bit PC for its function arguments, update PC related constants to 64bit. 4) Adding boundary checks during compile-time for statically computable PCs (`JAL` and branching instructions) resolves INT-8319
## Summary - validate statically known CFG targets before block construction - reject targets that exceed the implemented PC bounds - reject in-bounds targets that do not correspond to a lifted instruction - rename the PC bounds helper to make the distinction from program membership explicit ## Testing - `cargo check --profile fast -p rvr-openvm-lift -p rvr-openvm` - `cargo nextest run --cargo-profile=fast -p rvr-openvm-lift -p rvr-openvm`
## Summary
Resolves INT-8554.
This PR tightens metered memory-height accounting for the memory
boundary, memory Merkle, and Poseidon2 AIRs.
Metering records which leaves inside each 64-leaf memory page were
touched, then counts the Merkle work from those masks at checkpoint and
segment boundaries. RVR and interpreted metering use the same Rust
accounting path.
## Before
The estimate was page-oriented. If a few leaves inside a page were
touched, the estimate charged page-level work from page-level state.
```text
one memory page = 64 Merkle leaves
leaf index: 0 1 2 3 4 5 ... 63
accesses: . x . x . . ... .
page-oriented estimate:
[page subtree]
/ \
... ... page was touched,
/ \ / \ estimate used page-level state
L0 L1 L2 L3
x x
```
Shared upper-tree ancestors across nearby pages were also charged along
each page path.
```text
page A -> P -> G -> root
page B -> P -> G -> root
^ ^ ^
shared ancestors
```
That made the estimate safe, but it could overshoot the rows needed by
`MemoryMerkleAir` and the related Poseidon2 hashes.
## Now
Each memory page is exactly 64 Merkle leaves. The page-local leaf
occupancy is stored in one `u64` mask.
```text
full memory tree
[root]
/ \
... ... ancestors above pages are shared
/ \
[page] [page]
/ ... \ / ... \
L0 ... L63 L0 ... L63 each page stores one u64 leaf mask
```
For each access, metering records a page-table index and a per-page leaf
mask:
```text
field type meaning
page_id u32 index into page_masks
leaf_mask u64 one occupancy bit per leaf in that 64-leaf page
```
`page_masks[page_id]` stores the accumulated `leaf_mask` for that page.
```text
page_masks: Box<[u64]>
page_id ----------+
v
page_masks[page_id] = accumulated u64 occupancy mask for that page
```
Across pages, upper-tree ancestors are tracked with a separate bitset of
ancestor node ids.
```text
new estimate for two nearby pages
root counted once in upper_nodes bitset
|
G counted once in upper_nodes bitset
|
P counted once in upper_nodes bitset
/ \
page A page B page-local work counted from each u64 mask
```
## Counting
For leaves, the update is bit operations on one `u64`:
```text
old page mask: 00001000
incoming access: 00101000
new page mask: 00101000 = old | incoming
newly touched bits: 00100000 = incoming & !old
new leaves: popcnt(newly touched bits)
```
For internal nodes inside a 64-leaf page, the code uses two page-local
paths.
Single-leaf path:
```text
leaf = trailing_zeros(added_mask)
```
For that leaf, the code checks the aligned groups that map to the
page-local ancestor levels:
```text
2-leaf group -> level 1 ancestor
4-leaf group -> level 2 ancestor
8-leaf group -> level 3 ancestor
16-leaf group -> level 4 ancestor
32-leaf group -> level 5 ancestor
64-leaf page -> page root
```
For a group size `G`, the group containing `leaf` is checked with:
```text
group_start = leaf & !(G - 1)
group_mask = ((1 << G) - 1) << group_start
is_new_node = (old_mask & group_mask) == 0
```
The first touched leaf in an aligned group creates that group's ancestor
node. Later leaves in the same group reuse the counted ancestor. The
first touched leaf in the page creates the page root.
Multi-leaf path:
```text
old_mask = previous page occupancy
added_mask = newly touched leaves
for each of the 6 page-local levels:
old_mask = OR-reduce old_mask into parent groups
added_mask = OR-reduce added_mask into parent groups
new_nodes += popcnt(added_mask & !old_mask)
```
The OR-reduction step is a fixed shift/mask operation:
```text
children: 0 1 1 0 0 0 1 1
next level: 1 1 0 1
mask = (mask | (mask >> shift)) & representative_bit_mask
```
This counts parent groups that have at least one newly added leaf and
had no old leaf under that group. A 64-leaf page has six levels up to
the page root, so the multi-leaf path is a fixed six-level sequence with
`count_ones()` at each level.
The resulting height updates are:
```text
Boundary rows = 2 * new_leaves
Merkle rows = 2 * new_merkle_nodes
Poseidon2 = 2 * new_leaves + 2 * new_merkle_nodes
```
The Poseidon2 count remains a safe upper bound because trace generation
can deduplicate equal hash inputs by value.
## RVR
The RVR tracer records the same page/leaf-mask information in generated
C and applies it through the same `MemoryCtx` logic.
```text
generated C tracer
|
v
page/leaf-mask buffers
|
v
Rust MemoryCtx accounting
|
v
same height updates as interpreted metering
```
The normal memory fast path coalesces consecutive accesses to the same
page before flushing to Rust. That keeps the common path cheap while
keeping RVR and interpreted metering on the same estimate.
## Performance
This PR also removes avoidable overhead from metered execution:
- the interpreter countdown fast path uses a lightweight decrement
before the full segmentation check
- repeated same-page memory accesses are coalesced before checkpoint
replay
- single-leaf updates use a small aligned-group check
- multi-leaf updates use a fixed six-level `added_mask` OR-reduction
- call sites use the real height-update operation directly
## Testing
- `cargo +nightly fmt --all -- --check`
- `RUSTC_WRAPPER= cargo test -p openvm-circuit
arch::execution_mode::metered --lib`
- `RUSTC_WRAPPER= cargo test -p openvm-circuit --features rvr
rvr_metered_ctx_parts_roundtrip_preserves_execution_state --lib`
- `RUSTC_WRAPPER= cargo check --profile fast -p openvm-circuit
--features rvr`
- `RUSTC_WRAPPER= cargo check --profile fast -p rvr-openvm`
- `git diff --check`
- [reth benchmark
comparison](https://github.com/axiom-crypto/openvm-eth/actions/runs/28176871048)
…eferral chip changes and fixing the gen_pointer test function to use 2^31 max_memory
…er of cells in the register AS since now we have a separate gen_distinct_register_pointers
GunaDD
force-pushed
the
feat/2-pow-32-memory-addresses
branch
from
June 30, 2026 18:06
f79ab88 to
4b07c41
Compare
Contributor
Note: cells_used metrics omitted because CUDA tracegen does not expose unpadded trace heights. Commit: ff75195 |
GunaDD
force-pushed
the
develop-v2.1.0-rv64
branch
from
July 13, 2026 16:47
8351e0c to
f211621
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: support 2^32 byte memory addresses
Raises the RV64 memory address space to the full 2^32 bytes. Since
RV64_MEMORY_ASstores u16 cells, that means cell pointers are now up to 31 bits wide
(
POINTER_MAX_BITS: 28 → 31).Why pointers become two limbs
A 31-bit pointer no longer fits safely in a single BabyBear field element, so every
pointer on the memory bus is now sent as two little-endian 16-bit limbs.
MemoryAddress.pointerbecomespointer_limbs: [T; 2], and the bus payload is[address_space, ptr_lo, ptr_hi, data..., timestamp].Main changes
MemoryAddress, memory bus, and offline checker switched totwo-limb pointers. The persistent boundary AIR decomposes the merkle leaf label
into range-checked limbs so the leaf pointer can be emitted without composing the
full 31-bit value.
extensions/riscv/circuit/src/adapters/mod.rs):shared AIR + tracegen helpers that convert RV64 byte pointers (read from
registers) into cell-pointer limbs using a carry witness plus range checks, and
add per-block offsets with carries. Register-AS pointers always fit in the low
limb, so register accesses skip the extra columns and range checks.
pointer into a single field element; each now carries new witness columns and
uses the shared helpers instead:
hintstore, vec_heap family, keccak256 xorin, sha2);
base + block_offsetaddition inchips that do multi-block accesses (vec_heap family, keccak256, sha2);
limb is constant zero and needs no carry or range check.
(column/record layouts kept identical).
gen_pointernow draws from the full 2^31 cell range. Thisalso resolves jpw's TODO in the test config: tests used to call the general
gen_pointeron the register address space, which forced the testMemoryConfigto artificially widen the register AS. Tests now use the new
gen_register_pointer/gen_distinct_register_pointers, which draw from thereal 32-slot register file (the distinct variant avoids collisions when a test
writes several register operands), so the register-AS resizing workaround is
removed.
extensions/sha2/circuit/src/sha2_chips/config.rs):the incremental hasher cast the record's state byte slice in place to
&mut [u64; 8]. The state lives inside the trace-generation record buffer, whichis only guaranteed 4-byte alignment (
align_of::<Sha2RecordHeader>() = 4, sincethe header is all u32 fields), while
[u64; 8]requires 8-byte alignment — so thecast was undefined behavior on host memory. Fixed by copying through an aligned
local
[u64; 8]buffer instead of reinterpreting in place.Closes INT-8080