Skip to content

perf(prover): faster CPU trace generation#786

Merged
MauroToscano merged 27 commits into
mainfrom
perf/tracegen-cpu-optimizations
Jul 8, 2026
Merged

perf(prover): faster CPU trace generation#786
MauroToscano merged 27 commits into
mainfrom
perf/tracegen-cpu-optimizations

Conversation

@ColoCarletti

Copy link
Copy Markdown
Collaborator

What & why

Trace generation was taking much longer than it should. The core problem was that trace-gen is memory-bandwidth-bound: it materialized millions of intermediate memory-operation structs and then re-read them to fill the trace. This PR removes that intermediate work and writes data straight into the trace columns, roughly halving trace-generation time on memory-heavy workloads.

Optimizations

  1. Skip the intermediate copy of memory operations. Register accesses are now written directly into their trace columns (via a compact 48-byte RegRow), instead of first building a MemwOperation and re-reading it. This is the biggest win — it eliminates a whole materialize + re-read pass over the hottest data.

  2. Sort memory ops into buckets up front. Operations are routed into their register / aligned / general buckets when created, instead of collecting everything into one list and classifying it afterwards.

  3. Smaller memory-op structs. The value / old fields shran]` (the values always fit in 32 bits), halving the struct size andcutting cache/memory traffic.

  4. Faster bitwise multiplicity counting. Replaced per-operation multiplicity updates with a dense histogram that each thread fills locally and then merges with a tree-reduce — parallel-friendly and allocation-free.

  5. Removed the old trace-gen code paths. Deleted the legacy con path.

Results

Measured on 2× AMD EPYC 9J14 (96-core) + RTX 5090, ethrex blocks,

Trace generation:

workload main branch Δ
1tx 4.1s 3.0s −26%
5tx 8.2s 4.2s −49%
10tx 11.4s 5.5s −51%

End-to-end proving time (GPU):

workload main branch Δ
1tx 14.6s 14.0s −4%
5tx 23.7s 19.8s −16%
10tx 32.5s 27.7s −15%

The end-to-end gain grows with workload size, since trace-gen is a bigger blocks.

Correctness

  • Full test suite passes; prove + verify pass on all ethrex fixtur
  • Trace-gen is CPU work, so CPU and GPU builds report matching trace-gen numbers (e.g. 10tx: 10.9s vs 11.4s on main, 5.5s vs 5.5s on branch) — a good consistency
    check.

@ColoCarletti

Copy link
Copy Markdown
Collaborator Author

/bench

@ColoCarletti

Copy link
Copy Markdown
Collaborator Author

/ai-review

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codex Code Review

Findings

  • Medium prover/src/tables/memw.rs:143 and prover/src/tables/memw.rs:164: MemwOperation::new / with_old now cast u64 values to u32, but the only guard is debug_assert!. In release builds, an out-of-domain value silently truncates and produces a different witness than the caller requested. Current in-tree callers appear to pass bytes or 32-bit register limbs, but this constructor still accepts [u64; 8], so the invariant should be enforced in release (assert!/try_from) or encoded in the API by accepting [u32; 8].

  • Medium prover/src/tables/trace_builder.rs:3235: the default parallel path allocates an 80 MiB BitwiseHistogram per rayon fold/reduce worker/split, while also keeping the base histogram and transient collector Vecs. With the ~15 collectors here, peak memory can exceed 1 GiB before trace generation, and disk-spill does not help this phase. This risks OOM on memory-constrained proving runs. Bound the number of dense histograms, shard the histogram storage, or fold collectors into one dense histogram when memory is the goal.

Notes

I did not build or run tests, per the static-review constraints.

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review: perf(prover) faster CPU trace generation

Reviewed against the diff. This is a solid, well-documented optimization. Routing at creation time (MemwBuckets), the direct-to-column RegRow fill, and the histogram-based multiplicity count all preserve byte-identical trace output:

  • Routing parity is correct. reg_ts_delta_in_range factors out exactly conditions 3–5 of is_register_op; conditions 1–2 (is_register && width==2, atomic old_timestamp) hold by construction on the fast path, and the fallback closure rebuilds the identical MemwOperation and re-classifies it via is_aligned_op. Stable-partition order is preserved because each op is routed into its bucket in encounter order (halt/precompile ops appended last), matching the old "append then partition" behavior.
  • Histogram is sound. type_mu_column ∘ lookup_type_index maps to the contiguous MU_MSB8..=MU_BYTE_ALU_XOR columns (11..=20), verified by the added histogram_column_map_guard test that runs in plain cargo test. Counting via a commutative monoid + tree-reduce is order-independent and matches update_multiplicities (skipping count==0 is fine since generate_bitwise_trace zeroes MU columns). bitwise::update_multiplicities/mu_column are still live via continuation.rs, so keeping them is correct.
  • The RegRow direct fill mirrors generate_memw_register_trace cell-for-cell.

Minor notes (non-blocking)

  1. Orphaned doc comment / duplicate #[allow]. The /// Returns: (memw_ops, load_ops, …) doc block that documents collect_ops_from_cpu is now separated from that function by the entire RegRow definition, and a stray #[allow(clippy::type_complexity)] was inserted directly above RegRow's doc (a no-op on a plain-field struct; the real one is re-added above collect_ops_from_cpu). Move the RegRow definition out from between the function's doc and its signature so the "Returns:" doc and the attribute stay attached to collect_ops_from_cpu.

  2. [u32;8] truncation is only debug-asserted. In release builds value.map(|v| v as u32) / with_old silently truncate if an element ever exceeds u32::MAX. The byte/32-bit-register-half invariant is genuinely true for all current callers, and the debug_asserts catch violations in tests — but note this assumption now silently guards soundness (a truncated value would produce a valid proof of the wrong computation). Acceptable as-is; worth keeping the invariant in mind for future memory-op callers.

  3. Transient memory during parallel histogram. The rayon fold/reduce allocates an 80 MiB BitwiseHistogram per work-split and per reduce identity; with ~15 collectors across many cores this is a transient spike on top of the 80 MiB base. Fine for the target hardware, just noting it since the histogram's whole point is to avoid the ~560 MB op stream.

No blocking issues.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

AI Review

PR #786 · 5 changed files

Findings

Status Sev Location Finding Found by
candidate medium prover/src/tables/memw.rs:148 Soundness risk: silent truncation in MemwOperation::new value/old in release builds minimax
minimax/MiniMax-M3
candidate medium prover/src/tables/trace_builder.rs:3235 Parallel BitwiseHistogram fold+reduce allocates many 80 MiB histograms concurrently moonmath
zro/minimax-m3
candidate medium prover/src/tables/trace_builder.rs:3237 Significant peak-memory regression from per-worker BitwiseHistogram allocation minimax
minimax/MiniMax-M3
candidate low prover/src/tables/bitwise.rs:535 BitwiseHistogram counters are u64; a single cell overflow would silently corrupt multiplicity moonmath
zro/minimax-m3
candidate low prover/src/tables/bitwise.rs:563 fill_multiplicities is sequential and could be parallelized minimax
minimax/MiniMax-M3
candidate low prover/src/tables/bitwise.rs:580 Dead code: BitwiseHistogram::default() allocates 80 MiB and is never called minimax
minimax/MiniMax-M3
candidate low prover/src/tables/memw.rs:148 MemwOperation::new and with_old silently truncate u64->u32 in release builds moonmath
zro/minimax-m3
candidate low prover/src/tables/trace_builder.rs:481 Stale doc comment referencing deleted collect_bitwise_from_memw_register minimax
minimax/MiniMax-M3
candidate low prover/src/tables/trace_builder.rs:576 MemwSink default push_reg_access silently ignores the decomposed fields moonmath
zro/minimax-m3
candidate low prover/src/tables/trace_builder.rs:652 MemwBuckets over-allocates register_rows capacity upfront moonmath
zro/minimax-m3
candidate low prover/src/tables/trace_builder.rs:3253 bitwise_histogram lives for the entire phase 5 instead of being dropped after gen_bitwise moonmath
zro/minimax-m3

Status column reflects the verdict from the verifier: deepseek-verifier (openrouter/deepseek/deepseek-v4-pro).

AI-001: Soundness risk: silent truncation in MemwOperation::new value/old in release builds
  • Status: candidate
  • Severity: medium
  • Location: prover/src/tables/memw.rs:148
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

MemwOperation::new converts [u64; 8] to [u32; 8] via v as u32 after only a debug_assert!. In release builds (where debug_assert! is stripped), a future caller passing a value > u32::MAX would silently corrupt the trace columns for VAL_/OLD_ values — which the MEMW/MEMW_A/MEMW_R tables read back via set_u64 (u64 cast). The comment correctly notes this would be a soundness bug, but the release-build guard is missing.

Evidence

The PR adds value: value.map(|v| { debug_assert!(v <= u32::MAX as u64, ...); v as u32 }) in MemwOperation::new (memw.rs ~148) and with_old (~165). All current callers pass byte values or pack_register_value u32 halves, so the assert is satisfied, but the check is stripped under release builds. The set_u64 calls then op.value[i] as u64 (memw.rs 217, memw_aligned.rs 110, memw_register.rs 127).

Suggested fix

Replace debug_assert! with assert! (always-on, panics), or use u32::try_from(v).expect(...) to propagate, or change the field to [u64; 8] and only narrow in the trace generator. This makes the domain contract a hard runtime invariant regardless of build profile.

AI-002: Parallel BitwiseHistogram fold+reduce allocates many 80 MiB histograms concurrently
  • Status: candidate
  • Severity: medium
  • Location: prover/src/tables/trace_builder.rs:3235
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

The new collectors.par_iter().fold(bitwise::BitwiseHistogram::new, ...).reduce(bitwise::BitwiseHistogram::new, ...) allocates one 80 MiB histogram per rayon task plus one reduce-identity, and on top of the base 80 MiB histogram the peak allocation can exceed 1 GiB on many-core machines, risking OOM in memory-constrained runners.

Evidence

Each fold task calls BitwiseHistogram::new, which allocates vec![0u64; NUM_ROWS * NUM_LOOKUP_TYPES] (10*2^20 u64 = 80 MiB). With ~14 collectors and rayon defaults, the number of per-task accumulators is ~min(14, num_threads), so on a 64-core box ~14 × 80 MiB = 1.12 GiB is held simultaneously; the reduce identity adds another 80 MiB, plus the 80 MiB base, giving ~1.3 GiB transiently.

Suggested fix

Either reuse a single histogram across folds (e.g., pass &mut base through scoped helpers) or use a streaming accumulate-into-base pattern; or document the peak memory cost and gate the parallel path behind an env flag.

AI-003: Significant peak-memory regression from per-worker BitwiseHistogram allocation
  • Status: candidate
  • Severity: medium
  • Location: prover/src/tables/trace_builder.rs:3237
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

The new par_iter().fold(BitwiseHistogram::new, ...) allocates one 80 MiB BitwiseHistogram per rayon worker thread, plus the base and reduced histograms (each 80 MiB). At 8 workers the histograms alone reach ~720 MiB, and each worker's transient collector Vec (the page collector can hit ~hundreds of MB) overlaps with its histogram. Compared to the previous single growing Vec (~560 MB peak) this is ~2-3× more peak memory, on the hot path of every proof.

Evidence

trace_builder.rs lines 3222-3254: let mut base = bitwise::BitwiseHistogram::new(); then collectors.par_iter().fold(bitwise::BitwiseHistogram::new, |mut h, f| { h.add_ops(&f()); h }).reduce(bitwise::BitwiseHistogram::new, |mut a, b| { a.merge(&b); a }); base.merge(&reduced);. BitwiseHistogram::new allocates vec![0u64; NUM_ROWS * NUM_LOOKUP_TYPES].into_boxed_slice() = 2^20 * 10 * 8 = 80 MiB per call.

Suggested fix

Either (a) split the large per-source Vecs (especially the page collector) into a streaming histogram-bump loop without materializing the Vec, or (b) pre-size the worker histograms smaller and overflow to a hashmap/dense rep, or (c) keep the legacy single-Vec extend path on memory-constrained configurations and only parallelize when peak memory budget allows.

AI-004: BitwiseHistogram counters are u64; a single cell overflow would silently corrupt multiplicity
  • Status: candidate
  • Severity: low
  • Location: prover/src/tables/bitwise.rs:535
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

self.counters[idx] += 1 in bump is unchecked; on u64 overflow the value wraps, producing a smaller-than-actual multiplicity that could break BITWISE bus balance and yield an unsound proof. The Goldilocks prime is ~2^64, so FE::from(count) would also wrap, but the earlier u64 wrap would mask the real count.

Evidence

Single-cell lookups reaching 2^64 are unrealistic in practice, but BitwiseHistogram is positioned as a replacement for update_multiplicities, which also lacked overflow checks; the new code is no worse but the regression risk is now higher since one path replaces a per-increment with a per-bump that is easier to overflow unintentionally if a source emits many duplicates at one cell.

Suggested fix

Either use a wider counter (e.g., u128 or a packed overflow sentinel) or assert at fill time that no cell exceeds the Goldilocks prime. Document the explicit assumption.

AI-005: fill_multiplicities is sequential and could be parallelized
  • Status: candidate
  • Severity: low
  • Location: prover/src/tables/bitwise.rs:563
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

fill_multiplicities iterates NUM_LOOKUP_TYPES * NUM_ROWS = 10 * 2^20 = ~10.5M cells in a single thread, with no parallelism despite each cell being independent (no shared writes). Given the rest of the BitwiseHistogram path is parallelized for the bump phase, leaving the fill phase serial is inconsistent and a missed ~worker-count speedup for a step that runs unconditionally after every proof.

Evidence

bitwise.rs lines 563-577: nested for type_idx in 0..NUM_LOOKUP_TYPES { for row in 0..NUM_ROWS { ... } } — no rayon use; mutates trace.main_table row-by-row via set_fe which is a simple cell write.

Suggested fix

Parallelize with rayon: for type_idx in 0..NUM_LOOKUP_TYPES { let counters_slice = ...; let mu_col = type_mu_column(type_idx); trace.main_table.par_rows_mut().enumerate().for_each(...) } or batch per-type into a parallel chunk.

AI-006: Dead code: BitwiseHistogram::default() allocates 80 MiB and is never called
  • Status: candidate
  • Severity: low
  • Location: prover/src/tables/bitwise.rs:580
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

impl Default for BitwiseHistogram allocates an 80 MiB zeroed Box<[u64]>. It is not referenced anywhere in the codebase (the parallel fold/reduce call bitwise::BitwiseHistogram::new directly, not via ::default()). Keeping it means a stray #[derive(Default)] or ..Default::default() consumer would silently allocate 80 MiB.

Evidence

grep over the crate for BitwiseHistogram::default returns 0 hits; the only uses of bitwise::BitwiseHistogram::new are at trace_builder.rs:3224, 3237, 3241.

Suggested fix

Remove the impl Default for BitwiseHistogram block (lines 580-584) since it's unused and would be misleading if anyone added #[derive(Default)] later.

AI-007: MemwOperation::new and with_old silently truncate u64->u32 in release builds
  • Status: candidate
  • Severity: low
  • Location: prover/src/tables/memw.rs:148
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

The new conversion v as u32 is only guarded by debug_assert!. In release builds (or with debug_assertions disabled) a future caller passing a u64 > u32::MAX will silently truncate, which the doc itself calls a soundness bug. The type-level invariant ("values never exceed u32") is not enforced by the API.

Evidence

MemwOperation::new takes [u64; 8] and stores [u32; 8] with only a debug_assert. with_old does the same. All current callers in trace_builder.rs and friends pass byte/half-register values, but the field is pub and a future caller can break it without any compile-time error.

Suggested fix

Change the constructor signature to accept [u32; 8] directly (callers convert at the boundary) or introduce a checked constructor that returns Result. The doc explicitly warns this is a soundness risk, so a type-safe API is the right fix.

AI-008: Stale doc comment referencing deleted collect_bitwise_from_memw_register
  • Status: candidate
  • Severity: low
  • Location: prover/src/tables/trace_builder.rs:481
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

The comment on collect_bitwise_from_memw_register_direct says "Byte-identical to collect_bitwise_from_memw_register (both call the shared helper)" — but collect_bitwise_from_memw_register was deleted by this PR and no longer exists. Future readers will grep for the name and find nothing.

Evidence

grep for collect_bitwise_from_memw_register finds only this doc comment (trace_builder.rs:481). The function was removed and only collect_bitwise_from_memw_register_direct remains.

Suggested fix

Rewrite the comment to refer to the actual shared helper, memw_register_is_half_lookup, e.g. "Byte-identical to the legacy per-MemwOperation collector (both call memw_register_is_half_lookup)."

AI-009: MemwSink default push_reg_access silently ignores the decomposed fields
  • Status: candidate
  • Severity: low
  • Location: prover/src/tables/trace_builder.rs:576
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

The default MemwSink::push_reg_access impl takes decomposed (val0, val1, old0, old1, timestamp, old_ts, is_read) parameters and ignores them via let _ = (...), relying entirely on build_fallback() to rebuild the MemwOperation from its captured environment. If a caller of the default impl forgets to capture one of the decomposed fields in the fallback closure, the produced MemwOperation would silently use the wrong value with no compile-time warning.

Evidence

For Vec<MemwOperation> (the only default-impl caller is count_table_lengths), the closure does capture all fields correctly today. But the trait surface invites mistakes for any future sink that forgets to thread the decomposed values through build_fallback.

Suggested fix

Make the default impl require the fields to be moved out (e.g., return a Result or make the fallback closure infallibly take them as parameters), or add a debug_assert that the rebuilt MemwOperation matches the decomposed fields.

AI-010: MemwBuckets over-allocates register_rows capacity upfront
  • Status: candidate
  • Severity: low
  • Location: prover/src/tables/trace_builder.rs:652
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

MemwBuckets::with_register_capacity(cpu_ops.len() * 3) preallocates RegRows for the upper bound of M1/M3/M5 accesses. For 10M CPU ops this is ~30M × sizeof(RegRow) ≈ 1.4 GiB of upfront allocation; many register ops can still fall back to aligned/general (out-of-range timestamp delta), so the upper bound is not always needed.

Evidence

with_register_capacity allocates Vec::with_capacity(n) immediately. RegRow is roughly 48 bytes. The MEMW_R routing is conditional on reg_ts_delta_in_range, so a non-trivial fraction of register accesses fall through to the fallback MemwOperation path and the reserved register_rows capacity is wasted.

Suggested fix

Use a more conservative initial capacity (e.g., M1+M3+M5 upper bound minus a margin for fallback), or grow lazily. Also consider using try_with_capacity to avoid abort on impossibly large cpu_ops.len() (the multiplication itself could overflow usize).

Reviewer Lanes

Lane Model Prompt Status Findings
glm openrouter/z-ai/glm-5.2 general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
kimi openrouter/moonshotai/kimi-k2.7-code general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
minimax minimax/MiniMax-M3 general success 5
moonmath zro/minimax-m3 general success 6
nemotron openrouter/nvidia/nemotron-3-ultra-550b-a55b general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0

Verification Lanes

Lane Model Status Confirmed Rejected Uncertain
deepseek-verifier openrouter/deepseek/deepseek-v4-pro error: opencode failed (provider/auth/runtime error) and no verifications were submitted 0 0 0

Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report.

Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts.

MauroToscano and others added 11 commits July 7, 2026 11:34
- Reattach the collect_ops_from_cpu doc block to the function (it was landing
  on struct RegRow together with a stray #[allow]) and update its Returns list
  to the MemwBuckets shape
- Replace references to deleted code and internal review artifacts: the
  'Shared by BOTH collectors' rationale (the MemwOperation collector is gone),
  the E6/E7 notes, and mu_column's 'legacy path' label (update_multiplicities
  is live for continuation epochs)
- Document fill_multiplicities' overwrite semantics: it assumes zeroed MU
  columns and layered lookups (continuation L2G) must be added strictly after
- Remove BitwiseHistogram::total, a pub method with no callers
MEMW_R:
- Move RegRow and the direct column fill into memw_register.rs;
  generate_memw_register_trace is now a thin wrapper (ops -> RegRow::from_memw
  -> shared fill), so the unit-tested generator and the production fast path
  run the same code and cannot drift
- RegRow::new is the single definition of the row encoding (halved address,
  masked old_ts_lo); the walk fast path and from_memw both delegate to it
- Move the IS_HALFWORD collector next to bus_interactions() so the lookup a
  row sends and the lookup counted derive from the same module

Routing:
- Add MemwRoute + classify_memw, shared by MemwBuckets::push, the
  push_reg_access fallback (which now delegates to push), and
  count_table_lengths' partition_memw — one classification, three consumers
- Drop the single-use push_register/push_register_row helpers

BITWISE:
- BitwiseOperationType::ALL is the single origin of NUM_LOOKUP_TYPES;
  type_mu_column is defined as mu_column(ALL[i]), removing the arithmetic
  column-contiguity assumption instead of guarding it
- Replace the runtime histogram_column_map_guard test with a compile-time
  bijection assert (a mismatch is now a compile error, not a test failure)
- Correct bump()'s comment: in release an out-of-domain z would mis-count in
  another lane rather than panic; constructors' masking upholds the invariant
MemwOperation::new and with_old take [u32; 8] directly and
pack_register_value returns u32 limbs, so the u64->u32 narrowing
debug_asserts (and the release-mode silent-truncation hazard behind
them) are deleted rather than guarded: out-of-domain values are now
unrepresentable. The register fast path's bare `as u32` casts, which
skipped even the debug_assert, disappear for the same reason.
…ations

- Collectors now take &mut BitwiseHistogram instead of returning
  Vec<BitwiseOperation>. The heavy sources count with no per-source
  vector at all: MEMW_R bumps one IS_HALFWORD per row (tens of
  millions of rows), PAGE bumps one ARE_BYTES per byte of every
  touched page, and CPU padding collapses to two bump_n calls
  instead of an O(padding_rows) vector of identical zero ops.
- reduce_with replaces reduce(identity, ..) in the rayon tree-reduce,
  eliminating one zeroed 80 MiB identity histogram per reduce leaf.
- RegRow.address shrinks to u16 (register index 0..=255), taking the
  largest persisted array of the walk from 40 to 32 bytes per row.
Docs and dead-code cleanup for tracegen optimizations
…of-truth

Single-source the MEMW_R fill and the BITWISE type↔column map
The bitwise-histogram comments were phrased against the pre-refactor code
("byte-identical to the previous serial .extend() chain", "instead of an
O(n) op vector") — narration that is meaningless once this merges and the
old path is gone. Restate them as standalone rationale: the multiplicities
are order-independent (permutation-invariant bus) so parallel collection is
sound, and the reduce_with note now explains why to prefer it over
reduce(identity) rather than referencing a removed path. Also fix the stale
add_padding_byte_checks doc (it described the pre-shrink 14-op CPU layout).
Repo-wide sweep of the trace-generation code for comments that narrate a
change the next reader can't see, or that contradict the code:

- MemwBuckets doc and collect_all_ops no longer reference "the old two-stage
  partition" / "byte-identical to partition-ing one combined vec" — that
  partition sweep no longer exists anywhere (grep '.partition(' → 0 hits).
  Restated as the current invariant (register-first-then-aligned, deterministic
  insertion order the multiplicity counts rely on).
- Fix the MEMW_R ADDRESS column doc: register index range is 0-255 (x0-x31 plus
  the x254 commit index and x255 PC), not 0-31.
The Bus-14 comment in collect_store_op_from_cpu said the packed value "must
match CPU M7 which sends full rv2 as [lo32, hi32]", but M7 (the CPU's old
inline store MEMW at timestamp+1) was removed in the ALU-bus migration. The
store value now flows CPU -> MEMORY/MEMOP (rv2 as [lo32, hi32]) -> STORE chip
-> MEMW write. Reword to describe that path; the byte-layout requirement it
gestured at is unchanged.
Feed bitwise collectors straight into the histogram; cut reduce allocations
Make the u32 value domain a compile-time fact for MemwOperation
MauroToscano and others added 3 commits July 8, 2026 12:17
The bitwise multiplicity phase ran each source as one atomic collector across
a capped par_chunks, so the two dominant sources — the in-walk lookups and
MEMW_R (each tens of millions of items) — each pinned a single core while the
rest idled, and the in-walk fold ran serially before the parallel region.

Split those two sources into ~cap row-range slices and round-robin every unit
(whole collectors + the heavy slices) into exactly cap buckets, one 80 MiB
histogram each. Peak memory is unchanged (still cap concurrent histograms), but
the heavy work is now spread across buckets instead of piled onto one core, and
the in-walk fold moved into the parallel region. add_ops/bump/merge form a
commutative monoid, so multiplicities are byte-identical (prove+verify pass).

PAGE is left whole for now; if it becomes the bottleneck it can be sliced the
same way in a follow-up.
…tighter visibility, stale docs (#795)

- BITWISE MU columns now derive from one `MU_COLUMNS` array with a
  compile-time distinctness assert, so a duplicate column is a build
  error rather than a silent overwrite in fill_multiplicities.
- push_reg_access takes [u32;2] val/old pairs and builds its own
  fallback op instead of an 8-scalar signature plus a repacking closure.
- Narrow BitwiseHistogram + the MU-column/lookup-type helpers to
  pub(crate); scope the test-only generate_memw_register_trace wrapper.
- Fix stale docs (MemwSink Vec impl is the sizing pass, not tests;
  generate_bitwise_trace MU columns are filled by fill_multiplicities).
@ColoCarletti

Copy link
Copy Markdown
Collaborator Author

/bench
/bench-gpu

@MauroToscano

Copy link
Copy Markdown
Contributor

/bench

@MauroToscano

Copy link
Copy Markdown
Contributor

/bench-gpu

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

GPU Benchmark (ABBA) — af9751c9ad vs main (14 pairs)

RTX 5090 · Vast.ai datacenter @ $0.8680555555555556/hr · prover/cuda · drift-free A/B/B/A

=== ABBA paired result  (improvement: - = PR faster) ===
  pairs: 14   mean A (PR): 19.769s   mean B (base): 22.164s

  [parametric] paired-t   mean -10.79%   sd 1.93%   se 0.52%
               95% CI: [-11.90%, -9.68%]   (t df=13 = 2.16)
  [robust]     median -10.91%   Wilcoxon W+=0 W-=105  p(exact)=0.0001  (z=-3.26)

  --- server stability (this run; compare across servers) ---
  run-to-run jitter:    A CV 1.63%   B CV 1.42%        (lower = steadier)
  within-session drift: -1.75% over the run, 1st->2nd half -1.18%
    (jitter -> Tier-1 cached gate floor; drift -> whether the cached baseline can be trusted)

  VERDICT: REAL IMPROVEMENT - PR faster by ~10.79% (t-CI and Wilcoxon agree)

  raw pairs: /tmp/abba_run/pairs.csv

- = PR faster. Trust the verdict when paired-t and Wilcoxon agree.

MauroToscano and others added 2 commits July 8, 2026 17:02
zeroed_fe_vec built its Vec<FE> with mem::transmute::<Vec<u64>, Vec<FE>>.
That relies on Vec's field layout being identical across element types, which
the language does not guarantee (Vec is #[repr(Rust)], and the std transmute
docs flag exactly this Vec-to-Vec pattern as a Bad Idea). It happens to work on
the pinned toolchain, but a layout change (e.g. -Zrandomize-layout) could swap
the ptr/cap words and make it UB.

Rebuild the Vec from its raw parts instead: same calloc'd allocation, same
element size/align (already const-asserted), so the dealloc Layout matches and
there is no dependence on Vec's internal layout. Behavior is unchanged — the
zeroed_fe_vec_matches_fe_zero test and full prove+verify still pass.
…w-parts

zeroed_fe_vec: reinterpret via from_raw_parts, not Vec transmute
@MauroToscano MauroToscano enabled auto-merge July 8, 2026 20:12
@MauroToscano MauroToscano added this pull request to the merge queue Jul 8, 2026
Merged via the queue into main with commit a5e51d6 Jul 8, 2026
15 checks passed
@MauroToscano MauroToscano deleted the perf/tracegen-cpu-optimizations branch July 8, 2026 20:37
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.

2 participants