Skip to content

Unify memcpy, memmove, memset and commit on one accelerator - #984

Merged
MauroToscano merged 81 commits into
mainfrom
exp/memmove-literal
Oct 23, 2026
Merged

MauroToscano merged 81 commits into
mainfrom
exp/memmove-literal

Conversation

@nicole-graus

@nicole-graus nicole-graus commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

main has no accelerator for memcpy, memmove or memset, and proves the write syscall with a dedicated COMMIT chip. This adds the three memory operations and folds write into the same chip, following the merged COPY spec.

Description

One COPY table (41 columns, 33 bus interactions, 39 constraints, degree 3) serves memcpy/memmove, memset and write. The COMMIT chip is deleted.

Which operation a row runs is decoded, never chosen by the caller: the ECALL number is a linear function of is_set and is_write, so the three accepted numbers are exactly 64, -30 and -31 and nothing else; both flags ride inside the chain tuple, so a sequence cannot change operation half way through.

  • memset is a propagating copy. The stub seeds eight bytes and calls with dst = src + 8; the chip inverts the read/write timestamps so each row observes the previous row's write. The gap is pinned in-circuit: without it a dst == src call would leave the copied value unconstrained.
  • write reads its destination from x254, the running commit index, writes dst + count back, checks the file descriptor is 1 and returns count in x10.
  • A chain ends on the row that moves the last bytes; there is no terminal row. end is pinned by restrict_end against the borrow out of count - step.
  • The width is a free per-row choice upward, and pinned exactly downward: a wide row with fewer than eight bytes left borrows, and the borrow demands count == 0.

ECALL renumbering: memset moves to -31 to match the spec, and hint to -32. x254 becomes a two-word register (addresses 508-509), so the register file grows by one word address. Every guest ELF must be rebuilt: in-tree artifacts are gitignored and rebuild automatically, but build caches and the bench box will not.

jotabulacios and others added 30 commits July 29, 2026 11:10
Routes the guest's strong `memset` symbol through a bounded DMA ecall, the
same shape as the memcpy stub #874 added, and proves each chunk with a new
20-column DMA_SET table.

memset is cheaper than memcpy rather than a copy of it: there is no source to
read, so a row emits one MEMW write and no read (half the memory traffic per
byte), and every byte written is the same constant, so one `fill` column
replaces memcpy's eight value lanes. `fill_wide` is `fill` on eight-byte rows
and zero on one-byte tail rows, which lets one write tuple serve both widths.
`fill <= 255` is proven on the first row; the executor rejects wider values and
the guest stub masks a1, mirroring how the byte-count bound is handled.

Measured on real mainnet block 25368371 (50,781,394 cycles baseline):
  #874 memcpy alone   41,642,609  -17.99%
  + memset (this)     40,338,153  -20.57%
mem* routines fall from 24.41% to 4.84% of guest cycles.

No existing AIR changes: CPU stays at 38 columns and the new table only adds
senders to existing buses.
The DMA memcpy ecall already snapshots its entire source range before writing
(all reads at T+1, all writes at T+2), so one chunk has memmove semantics for
free. Chunking is what breaks it: copying [0,256) -> [4,260) clobbers source
bytes a later forward chunk still needs.

So the memmove stub walks chunks from the END backwards exactly when the
destination starts inside the source range (src < dst < src+n); every chunk
then reads bytes no earlier chunk has written. Disjoint regions, and dst below
src, keep forward chunking.

This costs one guest symbol and nothing else — no table, no syscall, no
constraint. Measured on real mainnet block 25368371:
  memcpy + memset      40,338,153
  + memmove (this)     39,867,443   -0.93%
Cumulative vs the 50,781,394 baseline: -21.49%.

The guest test covers both overlap directions at offsets either side of the
256-byte chunk boundary, plus exact aliasing.
Resolve the accelerator() conflict: the base gained DMA cycle counting
(DmaMemcpy => Some(Accelerator::Dma)) while this branch added DmaMemset and
classified both as None. Keep the counting semantics and extend them:
DmaMemcpy | DmaMemset => Some(Accelerator::Dma).

Two exhaustiveness follow-ups the merged tree needs to compile and pass:
- SyscallNumbers::raw() gets the DmaMemset arm (DMA_MEMSET_SYSCALL_NUMBER).
- The CLI's EXPECTED_ACCELERATORS gets a DmaMemset row, required by
  accelerator_of_mirrors_prover_classification's one-row-per-syscall check.
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Benchmark Results for unmodified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
base binary_search 38.7 ± 0.6 37.6 39.5 1.00
head binary_search 39.7 ± 0.9 38.1 41.3 1.03 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base bitwise_ops 39.0 ± 0.6 38.0 39.9 1.00
head bitwise_ops 39.6 ± 1.1 38.5 41.4 1.02 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base fibonacci_26 40.9 ± 0.6 40.0 41.8 1.00
head fibonacci_26 41.3 ± 0.5 40.7 42.0 1.01 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base matrix_multiply 41.3 ± 0.9 39.8 42.6 1.01 ± 0.02
head matrix_multiply 40.8 ± 0.4 40.2 41.5 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base modular_exp 38.9 ± 0.5 38.3 39.8 1.00
head modular_exp 41.6 ± 4.7 38.5 52.6 1.07 ± 0.12
Command Mean [ms] Min [ms] Max [ms] Relative
base quicksort 41.8 ± 0.5 40.4 42.2 1.00
head quicksort 42.0 ± 0.7 40.8 42.9 1.01 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base sieve 42.0 ± 0.5 41.2 42.9 1.00
head sieve 43.1 ± 0.8 41.7 43.8 1.03 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base sum_array 49.4 ± 0.6 48.5 50.3 1.00
head sum_array 49.9 ± 0.6 48.6 50.8 1.01 ± 0.02

@nicole-graus

Copy link
Copy Markdown
Collaborator Author

/bench

@nicole-graus

Copy link
Copy Markdown
Collaborator Author

/bench-verify

@github-actions

Copy link
Copy Markdown

⏳ Benchmark started on the bench server. Two verifier arms (monolithic + continuations over an ethrex 20-tx block), then the recursion-guest cycle comparison, which adds guest builds on top — longer on a cold runner. The bench server is occupied until it finishes.

)

* Correct comments and test data left stale by the COPY unification

- Name COPY (not COMMIT) among the ECALL receivers and as the Commit-bus sender.
- Count five accelerators, not six, and rename the test that says six.
- logs.rs: the row width follows from the count alone; it no longer reads src % 8 / dst % 8.
- CLI accel_counts_sizes_dma_calls: derive rows with copy_trace_rows instead of the
  old terminal-row schedule.
- Drop the doubled #[cfg(test)], the DMA/DMA_SET history line, the stale drift-test
  doc, the allocator and dma_memset_min comments.

* Call COPY's Commit-syscall rows commit rows in the comments
@MauroToscano
MauroToscano added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 861d517 Oct 23, 2026
21 checks passed
@MauroToscano
MauroToscano deleted the exp/memmove-literal branch October 23, 2026 17:30
MauroToscano added a commit that referenced this pull request Oct 23, 2026
Resolve the two conflicts with #984 (COPY replaces COMMIT):
- trace_builder.rs BITWISE collectors: keep COPY's collector from main and this
  branch's KECCAK_RND padding count (keccak_rnd::padding_are_bytes).
- skip_empty_tables_tests.rs: COMMIT left the optional-table list (17 tables);
  keep this branch's ARE_BYTES exemption imports.
MauroToscano added a commit that referenced this pull request Oct 23, 2026
Brings in main's #984 (COPY replaces COMMIT) through #1016's branch.
Conflicts resolved by keeping both sides:
- TableCounts / statement / VmAirs / Traces: drop commit, keep is_wh and copy.
- BusId: IS_WH = 32 next to COPY's CopyNext = 33. 32 was one of the two IDs
  main's comment listed as unused; 29 and 34 stay unused.
- RECURSION_INPUT_VERSION 5 with a v5 note: main's v4 already means the
  layout without commit, so is_wh needs its own number.
- Tests: counts 20 / 18 / 6, the accelerator test renamed to
  a_run_without_accelerators_omits_all_five_and_is_wh, COMMIT's omission
  tests dropped as on main, and main's committed-output test kept with this
  branch's "dropping ..." message style.
MauroToscano added a commit that referenced this pull request Oct 23, 2026
Brings in #984 (COPY replaces COMMIT, x254 becomes two words) and #1016.
Conflicts:
- lib.rs: keep the pinned REGISTER commitment and add main's COPY AIR.
- register.rs: keep the single OFFSET/INIT/FINI/IS_PC commitment; x254 is at
  508-509 now.
- trace_builder.rs: drop the COMMIT aux count; REGISTER takes final_pc_ts.
- constraint-program tests: CPU with ts_base 0 plus main's COPY check.

Changes the merge needs without a textual conflict:
- cpu_start: COPY is a fixed table ahead of REGISTER and COMMIT is gone, so
  the first CPU sub-proof is at 5 + HALT + the five accelerators. The
  air_refs debug check and cpu_proof_start_matches_air_refs_layout (now 32
  masks) follow.
- collect_epoch_from_cpu_ops returns Result: main made collect_ops_from_cpu
  fallible.
- multiplicity_forgery_poc: VmAirs::new takes the CPU heights and the
  REGISTER commitment; the prover commits to its own trace.
MauroToscano added a commit that referenced this pull request Oct 25, 2026
Brings in main's copy accelerator (#984), table multiplicity fix (#1016),
IS_WH table (#1022), and final-PC pin with preprocessed CPU timestamp
(#1020, #1025), adapted to the block:

- COPY is a counted table in COMMIT's place, cut into tables of 2^20 rows
  in the block; its bus is 36 and IS_WH's 37.
- The multiplicity fix's per-row lookups are counted per chunk in the
  finish, the streamed walk and the windowed builder.
- IS_WH is built at the finish, at most one table, counted last.
- Each CPU table has its own AIR (its timestamp base) and a preprocessed
  TIMESTAMP; REGISTER's FINI is the halted file. The block builds its AIRs
  from the stated CPU heights (each at most 2^21 rows, at most 2^30 - 1 in
  all), and the verifier and the recursion leaves evaluate TIMESTAMP
  themselves (4 + 4 times the row index's value at the point), with no
  commitment.
- Every statement tag over the count list moves (DOMAIN and CONTINUATION
  EPOCH V6, MULTILINEAR V3, MULTILINEAR BLOCK and MULTILINEAR EPOCH V2), the
  block proof file is version 2 and RECURSION_INPUT_VERSION is 6, past both
  sides.
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.

4 participants