Skip to content

feat: 2^32 memory address - #2850

Closed
GunaDD wants to merge 162 commits into
develop-v2.1.0-rv64from
feat/2-pow-32-memory-addresses
Closed

feat: 2^32 memory address #2850
GunaDD wants to merge 162 commits into
develop-v2.1.0-rv64from
feat/2-pow-32-memory-addresses

Conversation

@GunaDD

@GunaDD GunaDD commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

feat: support 2^32 byte memory addresses

Raises the RV64 memory address space to the full 2^32 bytes. Since RV64_MEMORY_AS
stores 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.pointer becomes pointer_limbs: [T; 2], and the bus payload is
[address_space, ptr_lo, ptr_hi, data..., timestamp].

Main changes

  • Memory system: MemoryAddress, memory bus, and offline checker switched to
    two-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.
  • Pointer conversion helpers (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.
  • Chip updates: every chip that accesses heap memory previously composed its
    pointer into a single field element; each now carries new witness columns and
    uses the shared helpers instead:
    • one carry column per base pointer for the byte→cell conversion (loadstore,
      hintstore, vec_heap family, keccak256 xorin, sha2);
    • one carry column per memory block for the base + block_offset addition in
      chips that do multi-block accesses (vec_heap family, keccak256, sha2);
    • register accesses take a cheap path: register pointers are tiny, so the high
      limb is constant zero and needs no carry or range check.
  • CUDA: GPU tracegen updated to mirror the CPU side for all of the above
    (column/record layouts kept identical).
  • Test utilities: gen_pointer now draws from the full 2^31 cell range. This
    also resolves jpw's TODO in the test config: tests used to call the general
    gen_pointer on the register address space, which forced the test MemoryConfig
    to artificially widen the register AS. Tests now use the new
    gen_register_pointer / gen_distinct_register_pointers, which draw from the
    real 32-slot register file (the distinct variant avoids collisions when a test
    writes several register operands), so the register-AS resizing workaround is
    removed.
  • SHA-512 state alignment UB (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, which
    is only guaranteed 4-byte alignment (align_of::<Sha2RecordHeader>() = 4, since
    the header is all u32 fields), while [u64; 8] requires 8-byte alignment — so the
    cast was undefined behavior on host memory. Fixed by copying through an aligned
    local [u64; 8] buffer instead of reinterpreting in place.

Closes INT-8080

@GunaDD GunaDD changed the title feat:2^32 memory address feat: 2^32 memory address Jun 6, 2026
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@GunaDD
GunaDD force-pushed the perf/merkle-tree-mem-opt branch 3 times, most recently from 52efe21 to 4c9cea5 Compare June 8, 2026 16:01
@GunaDD
GunaDD force-pushed the feat/2-pow-32-memory-addresses branch 2 times, most recently from e5623d6 to 50cec8e Compare June 8, 2026 19:59
@GunaDD
GunaDD marked this pull request as draft June 9, 2026 14:14
@GunaDD
GunaDD marked this pull request as ready for review June 9, 2026 14:14
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@GunaDD
GunaDD force-pushed the perf/merkle-tree-mem-opt branch from 814c620 to 0e6d6a6 Compare June 10, 2026 22:09
@GunaDD
GunaDD force-pushed the feat/2-pow-32-memory-addresses branch from 90b629c to 395639e Compare June 10, 2026 22:15
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@GunaDD
GunaDD force-pushed the feat/2-pow-32-memory-addresses branch from efee83a to f338acf Compare June 11, 2026 16:19
@github-actions

This comment has been minimized.

@GunaDD
GunaDD requested a review from shuklaayush June 11, 2026 17:16
Base automatically changed from perf/merkle-tree-mem-opt to develop-v2.1.0-rv64 June 18, 2026 20:28
@GunaDD
GunaDD force-pushed the feat/2-pow-32-memory-addresses branch from f338acf to e5f8e9c Compare June 18, 2026 20:45
@github-actions

This comment has been minimized.

@shuklaayush
shuklaayush force-pushed the develop-v2.1.0-rv64 branch 3 times, most recently from ddef172 to 5e8a1fc Compare June 19, 2026 22:15
shuklaayush and others added 2 commits June 20, 2026 00:17
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
mansur20478 and others added 23 commits June 26, 2026 12:46
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
GunaDD force-pushed the feat/2-pow-32-memory-addresses branch from f79ab88 to 4b07c41 Compare June 30, 2026 18:06
@github-actions

Copy link
Copy Markdown
Contributor
group app.proof_time_ms app.cycles leaf.proof_time_ms
fibonacci 3,341 4,000,051 388
keccak 20,564 14,365,133 3,055
sha2_bench 10,281 11,167,961 992
regex 2,607 4,090,656 354
ecrecover 1,957 112,210 281
pairing 2,109 592,827 297
kitchen_sink 5,609 1,979,971 867

Note: cells_used metrics omitted because CUDA tracegen does not expose unpadded trace heights.

Commit: ff75195

Benchmark Workflow

@GunaDD
GunaDD force-pushed the develop-v2.1.0-rv64 branch from 8351e0c to f211621 Compare July 13, 2026 16:47
@shuklaayush
shuklaayush changed the base branch from develop-v2.1.0-rv64 to develop-v2.1.0 July 14, 2026 13:06
@GunaDD
GunaDD changed the base branch from develop-v2.1.0 to develop-v2.1.0-rv64 August 4, 2026 18:39
@GunaDD GunaDD closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants