fix(gpu): recover device-only declines at the remaining cliff sites (R4 DEEP, comp-tree, R3 barycentric) - #935
Conversation
…R2 commit, R3 OOD, R4 DEEP) Under VRAM pressure a device dispatch can decline after the device-only gate already skipped the host drain, and the host fallbacks at the R2 comp-poly commit, the R3 parts/trace OOD and the R4 DEEP loop hard-abort on the empty host buffers. Download the resident data instead: the trace LDEs via materialize_lde_trace_host, the H part evaluations via a new download off the resident R2 parts handle. The asserts remain only for handles that cannot serve the data. The R4 DEEP host loop reads both the trace and the part evals, so it recovers both sides. Also adds sticky fault-injection hooks (test-faults) to the cuda barycentric, DEEP and comp-tree entries: the drain-and-retry absorbs one-shot faults, so the cliff paths need a fault that keeps firing.
Three prove+verify runs under sticky faults (comp-tree, barycentric, DEEP), each requiring the device-only path to fire on the warm-up and the recovery counters to move.
|
/ai-review |
Codex Code Review
|
|
Review Traced the whole recovery chain — R2 commit → R3 parts OOD / barycentric → R4 DEEP — plus the slab layout, stream ordering and feature wiring. No correctness or safety issues found. Details of what I checked and three minor notes below. Verified
Notes (all minor, inline)
One thing to be aware of rather than change: in the R3 arms the recovery is gated on the individual buffer, but |
AI ReviewPR #935 · 10 changed files Findings
Status column reflects the verdict from the verifier: deepseek-verifier (openrouter/deepseek/deepseek-v4-pro). AI-003: Race condition in sticky fault check: non-atomic load-then-decrement
Claim The Evidence
Suggested fix Make the decrement saturate at 0 with a compare-exchange loop, e.g. loop { let v = counter.load(Relaxed); if v < 0 { return Ok; } if v <= 1 { counter.store(0, Relaxed); return Err(...); } if counter.compare_exchange(v, v-1, Relaxed, Relaxed).is_ok() { return Ok; } } (or fetch_update with a closure that maps v>1 to v-1 and v in 0..=1 to 0). AI-008: Doc comment does not mention new mutability contract
Claim
Evidence
Suggested fix Update the doc comment for Reviewer Lanes
Verification Lanes
Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report. Discarded candidates (3) — rejected by the verifier
Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts. |
…llel parts download Review follow-ups on the device-only cliff recovery: - check_sticky: collapse the load-then-decrement into one fetch_update that saturates at 0, so concurrent per-table dispatches can't underflow the counter — which would break both the sticky guarantee and the `== 0` fired check. - cuda_fallback_tests: disarm the sticky faults with a Drop guard, so a panic in prove or a failing assert can't leave one armed and cascade into the next test in the single-threaded binary. - download_composition_parts_host: de-interleave under rayon and reinterpret the u64 buffer in place, matching materialize_lde_trace_host instead of copying again through u64_to_ext3_vec — this path fires often under VRAM pressure. - Docs: the device-only downgrade counter now also covers transient device declines, not only gate misses; note the new &mut contract on get_trace_evaluations_from_lde.
…m into gpu-cliff-recovery
cargo fmt collapses the aligned match-arm comments (the CI lint failure); also drop a stale doc sentence describing an earlier post-load variant that the fetch_update version does not use.
#938) The device-only cliff recoveries replace hard aborts with a silent download-and-continue, so the counters are now the only thing that surfaces a gate/dispatch lockstep break. GPU_DEVICE_ONLY_DOWNGRADES (trace side) already has its == 0 guard here; its parts-side counterpart did not, and its only readers were the > 0 assertions in cuda_fallback_tests, which run with a fault deliberately armed. Without this, a decline in the R2 comp-poly tree build on a device-only table recovers, verifies and passes green, while every such table pays a full parts D2H plus a CPU commit_bit_reversed and loses the resident composition tree. The R4 DEEP site is already covered transitively (it needs the trace to be device-only too, which moves the trace counter), so this closes the R2 commit and R3 parts-OOD sites. Zero is the right expectation: materialize_composition_parts_host early-returns without bumping when the part evals are already populated, so the counter only moves for a device-only table that had to pull its parts back. The message names both causes rather than blaming the gate, matching the counter's own doc, which now allows a transient VRAM decline as well as a gate miss.
|
/bench-gpu |
GPU Benchmark (ABBA) —
|
|
/bench-gpu |
Brings in the four commits that landed since the campaign base 58160b6: the bump guest allocator default (#869), the VRAM-pressure/R2-race fix (#914), the cuda table scheduler K = num_airs default (#911), and the device-only cliff recovery at R4 DEEP / comp-tree / R3 barycentric (#935). Conflicts resolved (one file, three hunks, all the same collision): - crypto/stark/src/prover.rs — the Stage-2 H-threading parameterized Round1/Round2 over the hasher, while #935 changed the same parameters from shared to mutable so the cliff recovery can download resident device data back into the host buffers. Rule: keep both — main's `&mut` mutability and this branch's `H` parameter. Applied at round_3_evaluate_polynomials_in_out_of_domain_element, round_4_compute_and_run_fri_on_the_deep_composition_polynomial, and compute_deep_composition_poly_evaluations. The recovery paths therefore run through the H-generic signatures; nothing is un-genericized. Everything else merged without conflict. Checked by hand rather than trusted to the textual merge: - crypto/stark/src/gpu_lde.rs — the two sides are disjoint. #935 appends the host-download helpers and the sticky fault hooks; the H-threading edits sit in the tree-building and FRI-commit entries. main's one hunk inside threaded territory is comment-only. - crypto/math-cuda/src/lib.rs — both sides add one `pub mod` to the same alphabetized list (`blake3` here, `faults` on main); both survive. - crypto/math-cuda/src/device.rs — touched only by this branch, so #935's math-cuda edits (barycentric, deep, faults, merkle) do not collide. - prover/tests/calibration.rs and prover/src/auto_storage.rs — #911 splits the scheduler's `k` from the storage estimate's, so both call sites move to `storage_estimate_parallelism()`. This branch never touched either file, so main's version lands whole and the RAM-vs-Disk decision is unmoved. - The `table_parallelism()` call site takes main's `table_parallelism(num_airs)`, which clamps internally to the same range this branch clamped by hand.
…t MainLdeSlot Belongs to the origin/main merge in the previous commit; it is separate only because the collision is invisible off the `cuda` feature, so it surfaced in `make lint`'s cuda pass rather than in the merge itself. #935's resident-aux downgrade added a consumer of `main_lde_cells` that destructures the slot as a plain `(data, _)` tuple and fills it from the device handle. This branch had already replaced that tuple with the `MainLdeSlot` enum, whose entire purpose is stated in its doc comment: a consumer added between Round 1 and the fused task must not be able to read an empty buffer as if it were an LDE. The guard worked — #935 is exactly such a consumer, and it failed to compile rather than silently reading a dropped buffer. Resolution rule: match the slot exhaustively, and split the two states by what each one actually owns. - `Retained`: unchanged #935 behaviour. This buffer is the one the fused task reads, so under device-only it is empty and has to be downloaded off the resident main handle, with the download's failure still failing the table. - `Dropped` (`ResidencyMode::RecomputeLde`): nothing to download and nothing to fail. The buffer was dropped deliberately and the fused task rebuilds the main LDE from the host trace via `expand_main_lde_row_major`, a path the device decline never touched. Only the aux recovery above is needed, so `recovered` stands. Matched without a `_` arm so a future `MainLdeSlot` variant has to state its own answer here, which is the property the enum was introduced for. NOTE: this interleaving is `cuda`-gated and device-resident, so no host test reaches it. #935's own `cuda_fallback_tests` cover the `Retained` arm on a GPU box; the `Dropped` arm — a resident-aux decline on a table under `ResidencyMode::RecomputeLde` — is not covered by any existing test and wants one added to that suite on the next box session.
Carries origin/main (cf3b1e9) onto the flip branch: the bump guest allocator default (#869), the VRAM-pressure/R2-race fix (#914), the cuda table scheduler K = num_airs default (#911), and the device-only cliff recovery at R4 DEEP / comp-tree / R3 barycentric (#935). No conflicts. Both of the resolutions made when main met this campaign's tree were already settled one branch down and arrive whole: - `crypto/stark/src/prover.rs` — Round1/Round2 carry both main's `&mut` and the campaign's `H` parameter, so #935's cliff recovery runs through the H-generic signatures. - `crypto/stark/src/prover.rs` — the device-only main-LDE recovery matches `MainLdeSlot` exhaustively: `Retained` downloads off the resident handle, `Dropped` (RecomputeLde) needs nothing because the fused task rebuilds from the host trace. The flip's own collision surface stayed clear: the renamed alias layer (`DefaultStarkHash`, `DefaultStarkTranscript`) and the `assert_keccak_backend` guard over the cuda fork are untouched by main's gpu_lde edits, and the cuda clippy pass — where this branch resolves to keccak — compiles clean. Gates: stark release 287/0; crypto 72/0 on both round arms; `lfm::` 354 passed / 1 failed / 9 ignored, the same single pre-existing `fibonacci.elf` drift exonerated in RESUME-PA-STAGE6.md §5.7, so zero delta; BLAKE3 host KAT green on both round arms; second-source green; `make lint` clean across all five combos; fmt clean. The cross-version king gate against pre-merge refs stays failing by design — that is the flip's inverted polarity (PA-PLAN §6), not a merge regression.
Carries origin/main (cf3b1e9) onto the MMCS integration branch: the bump guest allocator default (#869), the VRAM-pressure/R2-race fix (#914), the cuda table scheduler K = num_airs default (#911), and the device-only cliff recovery at R4 DEEP / comp-tree / R3 barycentric (#935). This is the branch where the two sides genuinely interleave. M-4p2 extracted the round bodies so they take the data they use — `lde_trace`, `composition_parts`, `rap_challenges` — instead of the whole `Round1`/`Round2`, and `multi_prove_batched` reuses those same extractions. #935 works the other way: its recoveries write the resident device data back into those very buffers, which is why upstream widened the round signatures to `&mut Round1`/`&mut Round2`. Neither shape can simply win. Resolution rule, applied to all nine hunks: **keep the extraction, move the mutability onto the extracted parameter.** Each recovery then writes to exactly the buffer its caller owns, and the batched path keeps sharing one implementation with the monolithic one. - `crypto/stark/src/prover.rs` `compute_composition_parts` — `lde_trace` becomes `&mut`; the R2 host-evaluator arm takes #935's recover-then-assert (replacing the old hard abort) against that parameter rather than `round_1_result.lde_trace`. - `crypto/stark/src/prover.rs` `compute_composition_parts` — the `evaluate_dev` arm keeps the extracted `rap_challenges` and the extracted `lde_trace` in the `host_trace_empty` retain flag. - `crypto/stark/src/prover.rs` `round_2_compute_composition_polynomial` — keeps the `CompositionParts` return, and #935's fold of the R2 device parts handle into the session (`set_gpu_composition_parts`) is added after the call, where `round_1_result` is in scope. - `crypto/stark/src/prover.rs` `round_3_evaluate_polynomials_in_out_of_domain_element` — extracted `lde_trace` and `composition_parts` both become `&mut`; the R3 parts OOD arm takes #935's recovery against them. - `crypto/stark/src/prover.rs` `compute_deep_composition_poly_evaluations` — same two parameters become `&mut`; the host DEEP loop's recovery writes through `composition_parts` instead of `round_2_result.lde_composition_poly_evaluations`. - `crypto/stark/src/batched/prover.rs` — the three call sites and `deep_codeword` follow the widened signatures; the FRI combine closure captures `retained_parts` mutably. That closure is `FnOnce` and runs serially, so the capture adds no concurrency requirement. - `crypto/stark/src/prover.rs` — the two `mut` bindings the split moved: the parts the R2 commit recovery writes now live in `round_2_compute_composition_polynomial` (so `computed.parts` is bound `mut` there), and `compute_composition_parts`'s own local is no longer mutated by anything, so it loses the `mut` and the `unused_mut` cfg_attr that went with it. Only the cuda lint pass sees either. Both semantics are live afterwards: nothing is un-genericized, no recovery is dropped, and the parameter each recovery writes to is the one the caller reads next. Gates: stark release 350/0 (RESUME-MMCS-INT.md's 349/0 plus main's new `table_parallelism_stays_within_one_and_num_airs`); debug batched/mmcs 87/0, exactly the recorded baseline; crypto 71+1/0 on both round arms; `make lint` clean across all five combos; fmt clean. `lfm::` reads 345 passed / 19 failed / 9 ignored against a recorded baseline of 349/15/9, and the merge is NOT the cause. Checked out 46798a5 — this branch's own pre-merge tip — and ran the same suite there: 345/19/9, and the 19 failing test names diff byte-identical against the merged tree's. The merge delta is exactly zero; the recorded baseline is stale, drifted by the fixture/toolchain trap already documented in the lfm fixture-drift notes. Independently, every resolution in this merge is inside `#[cfg(feature = "cuda")]` or is a signature mutability change, and that suite runs without cuda, so it could not have moved those tests either way. SEMANTIC-CONFLICT NOTE. The batched path consumes its parts on the host immediately (`parts_builder.absorb`) and never reads the device parts handle, so the recoveries are inert there today — `materialize_composition_parts_host` returns true without touching anything when the evals are already populated, so the widened signatures cost the batched path nothing and cannot trip its asserts. The recovery is only reachable on the monolithic path. Flagged because that is a judgement about reachability, not something a test currently pins.
Closes #927.
Extends the #914 device-decline recovery to the downstream cliff sites, which today hard-abort under the same transient VRAM pressure (census on rented 5090s: R4 DEEP ×97, comp-tree ×42, R3 ×3):
materialize_composition_parts_host) instead of asserting. The parts handle is folded into the trace session before the commit so the recovery and every downstream consumer read it from one place.materialize_lde_trace_host, threaded&mutthroughget_trace_evaluations_from_lde.round_3/round_4/compute_deepnow take&mut Round1/&mut Round2(single caller).The asserts survive only for the case where a resident handle cannot serve the data, with the device-only contract's message. New counter
GPU_COMPOSITION_PARTS_DOWNLOADSmirrors the downgrade counters (parts side).Testing: sticky fault hooks in math-cuda (
test-faults) on the barycentric/DEEP/comp-tree entries — sticky because the drain-and-retry absorbs one-shot faults — plus three end-to-end tests incuda_fallback_tests.rsthat arm them, require the device-only path on warm-up, and assert the recovery counters moved and the proof verifies.Validation (RTX 5090): fault tests 5/5 (3 new + 2 existing),
cuda_path_integration7/7 (happy path, zero downgrades), stark cuda lib 220/220; CPU-only stark suite 217/217 and clippy clean (cuda / no-cuda / test-faults).