Skip to content

feat: reuse compiled artifacts in rvr - #2833

Open
mansur20478 wants to merge 742 commits into
develop-v2.x.0from
feat/rvr-reuse-compiled-artifacts
Open

feat: reuse compiled artifacts in rvr#2833
mansur20478 wants to merge 742 commits into
develop-v2.x.0from
feat/rvr-reuse-compiled-artifacts

Conversation

@mansur20478

@mansur20478 mansur20478 commented Jun 1, 2026

Copy link
Copy Markdown

Part 2 of the original ticket split into two. Part 1: link

Part 2 consist of reusing persisted artifacts whenever possible instead of recompiling. The idea is to compile in rvr-mode till getting .c files, and instead of invoking make, check whether already existing .so are fit for reuse. Artifact fingerprints must match for .so file to be reused. During compilation

Artifact fingerprint is a SHA-256 fingerprint of:

  1. native_debug_info flag
  2. toolchain.compiler, toolchain.linker, toolchain.make, toolchain.host_os
  3. host_cpu_features: affects -march=native (unnecessary?)
  4. make_args: make variables except EXT_LIBS and EXT_SRCS
  5. name + content of every file under generated C project_dir in sorted order. The folder includes external static libs and external source files too.

New methods:
compile_pure_cached, compile_metered_cached, compile_metered_cost_cached that take cache_dir as an additional argument - a place on disk for storing artifacts. Invokable from sdk.

The following loading functions do not perform validation:
load_compiled_pure, load_compiled_metered, load_compiled_metered_cost methods don't have .so validation.

Modified build.rs of extensions to rebuild rvr FFI staticlibs on Cargo.lock and ffi-common source changes.

resolves INT-7843

stephenh-axiom-xyz and others added 30 commits May 18, 2026 11:49
Resolves INT-7138, INT-7158, INT-7164.

### INT-7138: Preserve `main_commit` on non-present `min_cached_idx`
rows

Moves the `main_commit` set to after `fill_present_row` and
`fill_non_present_row` so it won't be overwritten in the latter.

### INT-7158: Reject unsupported `max_cached` values in proof-shape CUDA

### INT-7164: Validate transcript CUDA proof count range

No fix required, but added a comment to explain `SWITCH_BLOCK` usage so
this doesn't get flagged as often. We may want to consider removing it
later as a maintenance item if it doesn't hurt performance.
Resolves INT-7136 and INT-7153.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This was done erroneously in the SHA2 refactor but the upstream feature
is called sha256 so we shouldn't change it.
…tracegen (#2670)

## Summary

Reduce CUDA stack usage, register pressure, and spills in `keccakf_op`
and `sha2_main` trace generation kernels, inspired by #2524.

### keccakf_op_tracegen
- Rewrote `keccakf_permutation` to operate in-place: eliminated the 200
B `temp[25]` scratch array, walk the rho/pi 24-element cycle with a
single saved temp, compute chi in-place with two temps per row, and use
a scalar `d` in theta. Stays `__forceinline__` so the state never has to
live in local memory.
- Moved the row-fill body into a `__noinline__` helper
`fill_keccakf_op_row` so the heavy working set (200 B keccak state
union, `MemoryAuxColsFactory`, `BitwiseOperationLookup`, the
50-iteration `mem_helper.fill` loop) lives in the helper's own frame
instead of the kernel's.

### sha2_main_tracegen
- Factored the body into a shared `__forceinline__` template
`sha2_main_row_body<V>` plus a thin `__noinline__` wrapper
`sha2_main_row_outlined<V>`. Uses `if constexpr (V::WORD_BITS > 32)` to
route SHA-512 through the outlined wrapper while keeping SHA-256 on the
inlined path (SHA-256 was already tight at 16 B stack / 255 regs, and
unconditional outlining would regress it).

### ptxas -v results (sm_89)

| Kernel | Before | After |
|---|---|---|
| `keccakf_op_tracegen` | 1656 B stack / 1268 spills / 255 regs | 16 B
kernel + 368 B helper / 168 spills / 24 regs |
| `sha2_main_tracegen<512>` | 1256 B stack / 1260 spills / 255 regs | 64
B kernel + 144 B helper / 144 spills / 24 regs |
| `sha2_main_tracegen<256>` | 16 B / 16 spills / 255 regs | unchanged |

[Reth benchmark
comparison](https://github.com/axiom-crypto/openvm-eth/actions/runs/24105029878)

Co-authored-by: axiom-agent <agent@axiom.xyz>
This resolves INT-7337 and INT-7340.
## Deduplicate Keccak round logic in CUDA code

Addresses
#2670 (comment)

### Problem

`keccakf_op.cuh` carried a second copy of the Keccak-f round logic that
already existed in `p3_keccakf.cuh`. Both were correct, but maintaining
two copies creates drift risk for future bug fixes or tuning.

### Changes

- **`p3_keccakf.cuh`**: Extracted a shared `__forceinline__` helper
`keccak256::keccakf_round_body()` that implements a single Keccak-f
round on a flat `uint64_t[25]` state. Reduced `apply_round_in_place()`
to a thin `__noinline__` wrapper that delegates to it.

- **`keccakf_op.cuh`**: Replaced the 50-line duplicate
`keccakf_permutation()` (and its local `rotl64()` helper) with a 5-line
loop over the shared round body.

- **`primitives/utils.cuh`**: Replaced the `ROTL64` macro with a proper
`__forceinline__` function `rotl64()` for type safety and
single-evaluation guarantees.

### Performance

No impact. All helpers are `__forceinline__`, so the compiler produces
identical code. The `__noinline__` / `__forceinline__` wrapper pattern
(same as `sha2_main.cu`) preserves the existing stack frame topology.
Set up [sccache](https://github.com/mozilla/sccache) with an S3 backend
across all CI workflows for distributed Rust compilation caching. This
caches compiled artifacts in S3 so that repeated builds across PRs and
branches avoid redundant compilation work.

- Add a reusable `setup-sccache` composite action
(`.github/actions/setup-sccache/`) that configures sccache with S3
credentials, auto-detects CUDA architecture for cache key partitioning,
and falls back gracefully to uncached builds if sccache fails to start
or S3 is unavailable.
- Integrate sccache into all 20+ CI workflows (extension tests, guest
library tests, CUDA tests, CLI, benchmarks, lints, SDK, recursion,
continuations, etc.).
- Replace `cargo install --force` with `cargo build` + PATH for local
tools (`cargo-openvm`, `openvm-prof`). `cargo install --force` always
rebuilds from scratch (no incremental compilation), while `cargo build`
leverages both sccache and incremental compilation from the `rust-cache`
target directory — making no-change rebuilds near-instant.
- Ensure all `runs-on` labels include `extras=s3-cache` for S3 access.

Compared against PR #2682 (same day, ~1hr earlier, no sccache), with
**98–100% sccache hit rates** across all workflows:

| Workflow | Before (s) | After (s) | Improvement |
|----------|-----------|-----------|-------------|
| CUDA SDK Tests | 724 | 397 | **-45%** |
| CUDA Continuations | 898 | 507 | **-44%** |
| Extension Tests CUDA (combined) | 662 | 404 | **-39%** |
| OpenVM CLI Tests | 738 | 505 | **-32%** |
| Guest Lib: verify-stark | 252 | 133 | **-47%** |
| Guest Lib: sha2 | 158 | 77 | **-51%** |
| Extension Tests: ecc (false) | 228 | 159 | **-30%** |
| Extension Tests: pairing (true) | 132 | 65 | **-51%** |

- [x] All CI workflows pass on this branch
- [x] sccache stats show high cache hit rates (98–100%)
- [x] Graceful fallback: workflows still work if sccache/S3 is
unavailable
- [x] `cargo openvm` commands work correctly via `cargo build` + PATH
instead of `cargo install`

---------

Co-authored-by: Claude <claude@axiom.xyz>
deletes the custom sccache action and uses runson
depends on #2685

resolves int-6953
Co-authored-by: Ayush Shukla <ayush@axiom.xyz>
@shuklaayush
shuklaayush force-pushed the develop-v2.1.0-rv64 branch from 316c914 to 0b8d705 Compare June 19, 2026 10:24
GunaDD and others added 13 commits June 19, 2026 09:11
Re-do of PR #2777 (base_alu part only), now on top of the u16 memory-bus
limbs change. Summary of the changes:
- Split base_alu chip into add_sub and xor_or_and chops.
- New xor_or_and chip is the old base_alu minus ADD/SUB.
- New add_sub chip handles the add and sub opcodes and store 2 bytes per
field element in its column.
- This allows us to remove the interactions needed to range check that
each individual field elements is bytes that was present in the previous
base_alu chip.
- Core width of the add_sub chip drops to 14 columns compared to the 29
columns of the base_alu chip.
- Rewrite tests.rs of add_sub chip for the new u16 columns layout.

Improves perf by 6% on the reth benchmark:
https://github.com/axiom-crypto/openvm-eth/actions/runs/27436476879

Closes INT-8102

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ayush Shukla <ayush@axiom.xyz>
@mansur20478
mansur20478 force-pushed the feat/rvr-reuse-compiled-artifacts branch from 5d2277b to b5d2f9d Compare June 19, 2026 18:45
@github-actions

This comment has been minimized.

@mansur20478

Copy link
Copy Markdown
Author

@claude provide Code Review for this PR

@claude

claude Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @mansur20478's task in 3m 18s —— View job


Code Review: PR #2833 — Reuse compiled artifacts in RVR

  • Gather context and read changed files
  • Review artifact_cache.rs (new fingerprinting module)
  • Review compile.rs changes (caching logic)
  • Review vm.rs changes (API surface)
  • Review SDK integration (lib.rs, compiled.rs, tests)
  • Review build.rs changes and cross-process test
  • Post review findings

Summary

This PR adds content-addressed caching for compiled RVR shared libraries (.so/.dylib). A SHA-256 fingerprint is computed from the generated C project files, toolchain identity, host CPU features, and make arguments. On cache hit the .so is loaded directly; on miss it's compiled and stored. The EXT_LIBS absolute-path issue from @gdmlcjs's review has been addressed. The race condition fix (write-to-temp-then-rename in save_artifact) is solid. The cross-process test with separate binaries validates fingerprint stability across processes.

The PR is already approved by @gdmlcjs. I have one actionable finding (the warn! on cache hit) and several observations for tracking. Previous review rounds have addressed the major issues well.


Findings

1. load_compiled_from_path warning fires on every cache hit (Medium)

crates/vm/src/arch/rvr/compile.rs:347-351load_compiled_from_path logs tracing::warn!("loading rvr artifact without compatibility validation"). On a cache hit (compile.rs:428), the code calls load_compiled_from_path(&cache_path), triggering this warning even though the artifact was just fingerprint-validated. In production where cache hits are the common case, this will spam warn-level logs.

Suggest extracting the unsafe { libloading::Library::new(...) } + fingerprint recovery into a crate-private load_lib(lib_path) helper without the warning, and calling it from both the cache-hit path and from load_compiled_from_path (which keeps the warning for the unvalidated public API):

fn load_lib(lib_path: &Path) -> Result<RvrCompiled, CompileError> {
    let lib = unsafe {
        libloading::Library::new(lib_path)
            .map_err(|e| CompileError::LibLoad(format!("{}: {}", lib_path.display(), e)))?
    };
    let fingerprint = lib_path.file_stem()
        .and_then(|s| s.to_str())
        .and_then(|s| s.strip_prefix("rvr-"))
        .map(str::to_owned);
    Ok(RvrCompiled { lib, lib_path: lib_path.to_path_buf(), artifact_dir: None, fingerprint })
}

pub fn load_compiled_from_path(lib_path: &Path) -> Result<RvrCompiled, CompileError> {
    tracing::warn!(/* ... */);
    load_lib(lib_path)
}

Then use load_lib(&cache_path) at line 428 instead of load_compiled_from_path.

Fix this →

2. host_cpu_features() returns empty string on aarch64 Linux (Medium — correctness for shared caches)

crates/vm/src/arch/rvr/artifact_cache.rs:74 — The function looks for a line starting with "flags", which is x86-only. On aarch64 Linux, the equivalent line starts with "Features" (capital F); on riscv64 it's "isa". This means on aarch64, host_cpu_features() falls back to empty string, and combined with toolchain.host_os (which carries no arch info), two different aarch64 machines (e.g. Graviton2 vs Graviton3) sharing a cache directory would compute the same fingerprint despite generating different -march=native binaries — leading to potential SIGILL at runtime.

The latest automated review (June 8) identified this same issue. Suggest at minimum:

  • Look for both "flags" (x86) and "Features" (arm64) in /proc/cpuinfo
  • Fold std::env::consts::ARCH into the hash unconditionally so the fallback path still differentiates architectures
#[cfg(target_os = "linux")]
{
    let cpuinfo = fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
    cpuinfo.lines()
        .find(|l| l.starts_with("flags") || l.starts_with("Features"))
        .map(|l| l.to_string())
        .unwrap_or_else(|| std::env::consts::ARCH.to_string())
}

Fix this →

3. Full setup_project runs on every call, even cache hits (Low — Performance, tracking item)

crates/vm/src/arch/rvr/compile.rs:394 — The entire C codegen pipeline (convert_vmexe_to_ir_with_debug, build_blocks, CProject::write_all, write_extension_staticlibs) runs before the fingerprint is computed and the cache is checked (lines 407–416). On a cache hit, all that work is discarded — only the make step is skipped.

This is inherent to the current design since the fingerprint needs the generated files to hash. Worth tracking as a follow-up: a two-phase fingerprint (hash VmExe bytes + config cheaply first, skip codegen on hit) could eliminate this overhead. Currently the caching benefit is limited to skipping the make compilation step.

4. TracerMode not explicitly in fingerprint — relies on C code divergence (Low)

crates/vm/src/arch/rvr/artifact_cache.rs:26-51TracerMode isn't directly hashed in compute_fingerprint. It's captured indirectly because different modes produce different C code. This works today (the cross-process test validates it), but explicitly hashing the mode discriminant would be more defensive against future changes where two modes might produce identical C but different runtime behavior:

h.update([opts.tracer_mode as u8]);
h.update(b"\0");

5. No domain separator after native_debug_info flag byte (Nit)

crates/vm/src/arch/rvr/artifact_cache.rs:33 — The native_debug_info flag is hashed as a single byte (0x00 or 0x01) without a trailing \0 separator before toolchain.compiler. Since the flag is fixed-length (1 byte) there's technically no ambiguity, but adding a \0 after it would make the format consistent with all other fields.

6. No compile_metered_segment_boundary_cached variant (Nit — Completeness)

crates/vm/src/arch/rvr/compile.rs:242-261compile_metered_segment_boundary and metered_segment_rvr_instance (vm.rs:510-534) don't have _cached counterparts. This may be intentional if segment-boundary mode isn't used in the caching workflow, but worth noting for completeness.


What looks good

  • Race condition fix (compile.rs:106-119): Write-to-temp-then-rename is correctly implemented. Cleanup on rename failure is handled. fs::rename is atomic on the same filesystem, guaranteed since both files are in cache_dir.
  • Error propagation in hash_dir_into (artifact_cache.rs:97): Uses .collect::<Result<Vec<_>, _>>()? — properly propagates IO errors from read_dir entries.
  • EXT_LIBS filtering (compile.rs:399-403): EXT_LIBS= and EXT_SRCS= are correctly filtered from the fingerprinted make args. The comment at line 396-398 accurately explains why. The staticlib contents are captured by hash_dir_into.
  • Non-cached functions delegate through cached with None (compile.rs:225-270): Clean refactor — compile() calls compile_cached(…, None), avoiding code duplication in compile_impl.
  • Fingerprint design (artifact_cache.rs:26-51): Covers all relevant inputs (files, toolchain, CPU features, make args, debug flag). Separator bytes between fields. Truncation to 128 bits is more than sufficient for content-addressing.
  • Deterministic directory hashing (artifact_cache.rs:96-114): Entries sorted by path, directories recursed with /\0 separator — correct and deterministic.
  • Validation moved early (compile.rs:374-390): Mode/suspension-policy validation happens before setup_project, failing fast before expensive codegen.
  • Cross-process test (rvr_cross_process_cache.rs): Excellent test design — spawns a separate binary to validate fingerprint stability across processes. The three assertions (artifact count, mtime stability, public value equality) cover the key invariants.
  • build.rs changes (all 6 extension build.rs files): Adding rerun-if-changed for Cargo.toml, Cargo.lock, and ffi-common/src ensures FFI staticlibs are rebuilt when shared dependencies change.
  • SDK API renames (compile_and_executeexecute, compilecompile_pure, executeexecute_compiled): Clearer naming that distinguishes the compile step from the execute step.

Overall Assessment

The implementation is well-structured and the fingerprinting approach is sound. All major prior review findings have been addressed. The two actionable items are finding #1 (spurious warn! on every cache hit — easy fix) and finding #2 (host_cpu_features() on aarch64 Linux — correctness issue for shared caches). The remaining findings are low-priority observations and design nits.


@mansur20478
mansur20478 marked this pull request as ready for review June 19, 2026 19:15
@github-actions

Copy link
Copy Markdown
Contributor
group app.proof_time_ms app.cycles leaf.proof_time_ms
fibonacci 1,023 4,000,051 392
keccak 16,473 14,365,133 3,061
sha2_bench 8,105 11,167,961 988
regex 1,223 4,090,656 356
ecrecover 434 112,210 288
pairing 600 592,827 296
kitchen_sink 3,869 1,979,971 854

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

Commit: 51a16b4

Benchmark Workflow

@shuklaayush
shuklaayush force-pushed the develop-v2.1.0-rv64 branch 2 times, most recently from 5e8a1fc to 46277f7 Compare June 19, 2026 22:17

@gdmlcjs gdmlcjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • In crates/rvr/rvr-openvm/c/Makefile, there are some configs that can be set with environment variables using ?=. They are not contained in the fingerprint so we should better add them in the fingerprint or change ?= to =.
  • There were some changes in the base branch: removed lto=false for macOS, changed method names from compile_pure to compile. Could you update those changes? A rebase might be helpful.
  • I'm not sure, but instead of having two versions of each function (with cache_dir: Option<&Path> and without) it might be simpler to have just one version (with cache_dir: Option<&Path>). Could you look into that?

@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:07
@mansur20478 mansur20478 removed their assignment Aug 14, 2026
@shuklaayush
shuklaayush changed the base branch from develop-v2.1.0 to develop-v2.x.0 August 25, 2026 15:27
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.