Skip to content

prover: route prove-chunk and agg-chunks through the #119 retry (#237) - #240

Open
defenwycke wants to merge 1 commit into
mainfrom
fix/237-prove-chunk-119-retry
Open

prover: route prove-chunk and agg-chunks through the #119 retry (#237)#240
defenwycke wants to merge 1 commit into
mainfrom
fix/237-prove-chunk-119-retry

Conversation

@defenwycke

Copy link
Copy Markdown
Contributor

Closes #237.

Implements option (2) from the issue: prove the segments explicitly so the existing per-segment retry applies, rather than wrapping the whole chunk.

The gap

prove_session proves every segment behind a single call, so a transient #119 fault surfaced as one Err after the whole chunk's work and took the chunk with it. prove_segment_resilient already existed with five callers — prove-chunk was not one of them, because it used a different API. The only retry covering this path lived in scripts/gpu-benchmark.sh, outside the binary, so anyone invoking prove-chunk by hand lost the chunk.

Why it is worth doing properly

From last night's 8× L40S runs (#238):

  • a fault two-thirds through a chunk cost 33% of the block's critical path — 817 s wall against 614 s of useful work — because recovery could only restart from segment zero
  • observed rate 5 faults in 80 chunk attempts (6.3%), i.e. a ~40% chance of at least one per 8-chunk block

Option (1) — a chunk-level retry — would have left that 33% in place. Under option (2) a fault costs one segment.

The change

Adds prove_session_resilient, which drives the segment loop and routes each segment through prove_segment_resilient, then assembles.

Assembly is not reimplemented. assemble_from_segment_receipts is the same code prove_session runs after its own loop, so the two paths cannot drift — and seg_distribute_cmd already demonstrates this shape yields a receipt that verifies against METHOD_ID.

Applied to both unprotected call sites. agg-chunks had the identical prove_session().unwrap(), and a fault there loses the most expensive single unit of work in the pipeline.

FoldProgress is removed — the helper prints progress itself, at the same cadence and the same line format, because recorded benchmark logs are parsed for it.

Verification

  • workspace builds clean, zero warnings
  • an old-vs-new receipt equivalence test is running now: the same block proved by both binaries on CPU, comparing the receipts byte for byte. I will post the result on this PR. If the receipts differ, this PR should not merge.

⚠ This does not fix #119. It changes an intermittent prover fault from "lose the chunk" to "lose a few seconds".

⚠ Retrying on "proof is invalid" is safe only because the guest and inputs are identical across attempts — a genuinely invalid proof fails every attempt and the bounded loop still terminates. That reasoning is written down at the helper, per the issue's request.

Refs #119.

`prove_session` proves every segment behind a single call, so a transient
#119 fault surfaced as one `Err` after the whole chunk's work and took
the chunk with it. The only retry for that path lived in
`scripts/gpu-benchmark.sh` -- outside the binary -- so anyone running
`prove-chunk` by hand simply lost the chunk.

Measured on 8x L40S (docs/history/BENCH_8xL40S_2026-09-08.md): a fault
two-thirds through a chunk cost **33% of the block's critical path** --
817 s wall against 614 s of useful work -- because recovery could only
restart from segment zero. Observed rate was 5 faults in 80 chunk
attempts (6.3%), a ~40% chance of at least one per 8-chunk block.

Adds `prove_session_resilient`, which drives the segment loop itself so
every segment goes through the existing `prove_segment_resilient`
bounded retry, then assembles. A #119 fault now costs ONE segment
instead of the chunk. This is option (2) from the issue.

Assembly is not reimplemented: `assemble_from_segment_receipts` is the
same code `prove_session` runs after its own loop, so the two paths
cannot drift. `seg_distribute_cmd` already proves this shape yields a
receipt that verifies against METHOD_ID.

Applied to BOTH unprotected call sites. `agg-chunks` had the identical
gap, and a fault there loses the most expensive single unit of work in
the pipeline.

`FoldProgress` is removed -- `prove_session_resilient` prints the
progress itself, at the same cadence and in the same line format, since
recorded benchmark logs are parsed for it.

⚠ This does not fix #119. It changes an intermittent prover fault from
"lose the chunk" to "lose a few seconds". Retrying on "proof is invalid"
is safe only because the guest and inputs are identical across attempts,
so a genuinely invalid proof still fails every attempt and the bounded
loop terminates -- that reasoning is written down at the helper.

Refs #119.
@defenwycke

Copy link
Copy Markdown
Contributor Author

Equivalence result — and the gate I promised in the description could not be run as written

I said I would compare the receipts byte for byte and that the PR should not merge if they
differed. That gate is invalid, and it would have failed a correct PR.

RISC0 seals are not reproducible. SegmentProverImpl::preflight draws a fresh random field element
per segment, per run, from the OS RNG (risc0-circuit-rv32im-4.0.5, prove/hal/mod.rs:136):

let mut rng = rand::rng();
let rand_z = ExtVal::random(&mut rng);

It feeds a Schwartz–Zippel checksum in witgen, so the trace — and therefore the seal — differs on
every run of the same binary on the same input. Comparing receipt bytes measures that draw, not this
change. (Same trap as hazync#182's "CONTROL FAIL", which was also a receipt-SHA comparison and also
was not a real failure.)

So the run below carries its own control: base2 is a second run of the unmodified base binary.
If base-vs-base2 already differs byte for byte, the promised gate is settled as invalid here,
tonight, on this block — rather than by citing an older measurement.

What was run

hazync-coord, 16 cores, both binaries built by prover/build-release.sh cpu (the canonical
container path), block_130000, HAZYNC_CHUNKS=1, HAZYNC_LIFTX_HINT=1 HAZYNC_FIELD_BIGINT2=1 HAZYNC_ECMULT_WINDOW=21, seg-po2 20.

arm binary rc wall segments
base c98a1b0 (prove_session) 0 778 s 6
cand a277b88 (this PR, prove_session_resilient) 0 781 s 6
base2 c98a1b0 again — the control 0 780 s 6

METHOD_ID gate first. Both binaries report canonical
37987b85ec665970ac6c5e8031deb8160ac8ed846f09056c3790b5f78c8bb5dd. A host-only change must not move
the guest id, and if either arm had moved it nothing downstream would mean anything.

The promised gate, for the record

base   sha256 bc64282748d537a302ef005cb9e28f55   230738 bytes
cand   sha256 c76fc6f97763123823e0739089a3765f   230738 bytes
base2  sha256 acab4904a8fc651d9b7d74a572bc3470   230738 bytes

Three different digests, identical length. base and base2 are the same binary on the same
input
— so the byte comparison rejects the shipped code as readily as it rejects this PR.

The gate that binds

Every receipt run through both binaries' receipt-digest, which verifies against METHOD_ID:

receipt verifier result journal_bytes journal_digest
base base VERIFIED 3756 cb0caa8b…9426f1f2
base cand VERIFIED 3756 cb0caa8b…9426f1f2
cand base VERIFIED 3756 cb0caa8b…9426f1f2
cand cand VERIFIED 3756 cb0caa8b…9426f1f2
base2 base VERIFIED 3756 cb0caa8b…9426f1f2
base2 cand VERIFIED 3756 cb0caa8b…9426f1f2

All six verify against the canonical image id, with an identical journal and an identical segment
count across all three arms. prove_session_resilient produces a receipt attesting the same
execution of the same image as prove_session.

Scope — what this does NOT cover

  • prove-chunk only. agg-chunks is the same helper at a second call site, so it shares this
    code, but the aggregate path is not exercised here — a CPU fold is hours.
  • seg-po2 20 on CPU, against 21/22 in production. The claim is about the code path, not the
    card, but it is a CPU-built binary and I am not claiming anything about CUDA from this.
  • 6 segments is a shallow join tree. A deeper run on block_140000 (91 segments) is proving now;
    I will add it if anything disagrees, and say nothing further if it does not.

Nothing here blocks the merge on equivalence grounds.

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.

prove-chunk has no #119 retry, and it is the path people run by hand CUDA prover intermittently emits a succinct receipt that fails its own verify()

1 participant