Skip to content

Commit cf3b1e9

Browse files
fix(gpu): recover device-only declines at the remaining cliff sites (R4 DEEP, comp-tree, R3 barycentric) (#935)
* fix(gpu): recover device-only declines at the remaining cliff sites (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. * test(gpu): exercise the cliff-site recoveries end to end 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. * fix(gpu): address cliff-recovery review — race-free sticky hook, parallel 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. * style(gpu): rustfmt check_sticky and correct its doc 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. * test(gpu): assert the parts-download counter is zero on the happy path (#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. --------- Co-authored-by: Mauro Toscano <12560266+MauroToscano@users.noreply.github.com>
1 parent ec58a7f commit cf3b1e9

11 files changed

Lines changed: 514 additions & 83 deletions

File tree

‎crypto/math-cuda/src/barycentric.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ pub fn barycentric_base_on_device(
143143
inv_denoms_ext3: &[u64],
144144
n: usize,
145145
) -> Result<Vec<u64>> {
146+
#[cfg(feature = "test-faults")]
147+
crate::faults::check_sticky(&crate::faults::FAULT_BARYCENTRIC_STICKY)?;
146148
assert_eq!(coset_points.len(), n);
147149
assert_eq!(inv_denoms_ext3.len(), 3 * n);
148150
let num_cols = main_handle.m;
@@ -204,6 +206,8 @@ pub fn barycentric_base_on_device_with_dev_inv_denoms(
204206
inv_offset_u64: usize,
205207
n: usize,
206208
) -> Result<Vec<u64>> {
209+
#[cfg(feature = "test-faults")]
210+
crate::faults::check_sticky(&crate::faults::FAULT_BARYCENTRIC_STICKY)?;
207211
main_handle.wait_ready_on(stream)?;
208212
assert!(coset_points_dev.len() >= n);
209213
let inv_end = inv_offset_u64
@@ -255,6 +259,8 @@ pub fn barycentric_ext3_on_device(
255259
inv_denoms_ext3: &[u64],
256260
n: usize,
257261
) -> Result<Vec<u64>> {
262+
#[cfg(feature = "test-faults")]
263+
crate::faults::check_sticky(&crate::faults::FAULT_BARYCENTRIC_STICKY)?;
258264
assert_eq!(coset_points.len(), n);
259265
assert_eq!(inv_denoms_ext3.len(), 3 * n);
260266
let num_cols = aux_handle.m;
@@ -308,6 +314,8 @@ pub fn barycentric_ext3_on_device_with_dev_inv_denoms(
308314
inv_offset_u64: usize,
309315
n: usize,
310316
) -> Result<Vec<u64>> {
317+
#[cfg(feature = "test-faults")]
318+
crate::faults::check_sticky(&crate::faults::FAULT_BARYCENTRIC_STICKY)?;
311319
aux_handle.wait_ready_on(stream)?;
312320
assert!(coset_points_dev.len() >= n);
313321
let inv_end = inv_offset_u64

‎crypto/math-cuda/src/deep.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ pub fn deep_composition_ext3(
4141
row_stride: usize,
4242
domain_size: usize,
4343
) -> Result<Vec<u64>> {
44+
#[cfg(feature = "test-faults")]
45+
crate::faults::check_sticky(&crate::faults::FAULT_DEEP_STICKY)?;
4446
let be = backend()?;
4547
let stream = be.next_stream();
4648
deep_composition_ext3_impl(
@@ -86,6 +88,8 @@ pub fn deep_composition_ext3_with_dev_parts(
8688
row_stride: usize,
8789
domain_size: usize,
8890
) -> Result<Vec<u64>> {
91+
#[cfg(feature = "test-faults")]
92+
crate::faults::check_sticky(&crate::faults::FAULT_DEEP_STICKY)?;
8993
let be = backend()?;
9094
let stream = be.next_stream();
9195
deep_composition_ext3_impl(
@@ -262,6 +266,8 @@ pub fn deep_composition_ext3_with_dev_parts_and_inv_denoms(
262266
row_stride: usize,
263267
domain_size: usize,
264268
) -> Result<Vec<u64>> {
269+
#[cfg(feature = "test-faults")]
270+
crate::faults::check_sticky(&crate::faults::FAULT_DEEP_STICKY)?;
265271
let deep_out = deep_fully_resident_launch(
266272
stream,
267273
main_lde,
@@ -324,6 +330,8 @@ pub fn deep_composition_ext3_fully_resident_keep(
324330
row_stride: usize,
325331
domain_size: usize,
326332
) -> Result<GpuDeepCodeword> {
333+
#[cfg(feature = "test-faults")]
334+
crate::faults::check_sticky(&crate::faults::FAULT_DEEP_STICKY)?;
327335
assert!(
328336
domain_size.is_power_of_two() && domain_size >= 2,
329337
"bit-reverse needs a power-of-two codeword"

‎crypto/math-cuda/src/faults.rs‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//! Sticky fault-injection hooks for the GPU error-path tests.
2+
//!
3+
//! Unlike the one-shot hooks in `fri` and `inverse` (which disarm after
4+
//! firing, so a drain-and-retry absorbs the injected error before it can
5+
//! surface), a sticky hook keeps failing once its armed call count is
6+
//! reached, until explicitly disarmed. The device-decline recovery tests
7+
//! need that: a stage falls through to its host path only when every device
8+
//! arm of that stage declines in the same prove.
9+
10+
use std::sync::atomic::{AtomicI64, Ordering};
11+
12+
use crate::Result;
13+
14+
/// R3 barycentric entries (`barycentric_{base,ext3}_on_device{,_with_dev_inv_denoms}`).
15+
pub static FAULT_BARYCENTRIC_STICKY: AtomicI64 = AtomicI64::new(-1);
16+
/// R4 DEEP composition entries (`deep_composition_ext3*`).
17+
pub static FAULT_DEEP_STICKY: AtomicI64 = AtomicI64::new(-1);
18+
/// R2 comp-poly tree entries (`build_comp_poly_tree_from_{evals_ext3_keep,slabs_dev}`).
19+
pub static FAULT_COMP_TREE_STICKY: AtomicI64 = AtomicI64::new(-1);
20+
21+
/// Countdown check shared by the sticky hooks: negative = disarmed (the
22+
/// production state); N > 0 counts down across calls and the Nth call — and
23+
/// every call after it — returns Err (the counter parks at 0); 0 therefore
24+
/// doubles as the "fired" marker. Disarm by storing -1.
25+
///
26+
/// The transition is a single `fetch_update`, so concurrent table dispatches
27+
/// (the prover runs a rayon task per table) cannot race the load against the
28+
/// decrement: each caller walks the counter one step (the closure returns
29+
/// `None` at `<= 0`, so it parks at 0 and never underflows), which keeps both
30+
/// the sticky guarantee and the `== 0` fired check sound. The fire decision
31+
/// reads `fetch_update`'s own result — `Ok(prev)` for the call that
32+
/// decremented, `Err(cur)` for a no-op — so no second load is needed.
33+
pub fn check_sticky(counter: &AtomicI64) -> Result<()> {
34+
// One atomic transition, so concurrent dispatches saturate at 0 rather
35+
// than underflowing: a decrement only happens from a positive value.
36+
let fired = counter
37+
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| match v {
38+
n if n < 0 => None, // disarmed: never fires
39+
0 => None, // already parked: stay fired (sticky)
40+
_ => Some(v - 1), // count down toward the parked 0
41+
})
42+
// Ok(prev): this call decremented — the 1 → 0 step fires.
43+
// Err(cur): no-op — fires only if already parked at 0.
44+
.map_or_else(|cur| cur == 0, |prev| prev <= 1);
45+
if fired {
46+
return Err(cudarc::driver::DriverError(
47+
cudarc::driver::sys::CUresult::CUDA_ERROR_UNKNOWN,
48+
));
49+
}
50+
Ok(())
51+
}

‎crypto/math-cuda/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub mod barycentric;
99
pub mod constraint_interp;
1010
pub mod deep;
1111
pub mod device;
12+
#[cfg(feature = "test-faults")]
13+
pub mod faults;
1214
pub mod fri;
1315
pub mod inverse;
1416
pub mod lde;

‎crypto/math-cuda/src/merkle.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,8 @@ pub fn build_comp_poly_tree_from_slabs_dev(
497497
m: usize,
498498
lde_size: usize,
499499
) -> Result<crate::lde::GpuMerkleTree> {
500+
#[cfg(feature = "test-faults")]
501+
crate::faults::check_sticky(&crate::faults::FAULT_COMP_TREE_STICKY)?;
500502
assert!(m > 0);
501503
assert!(lde_size.is_power_of_two() && lde_size >= 2);
502504
assert_eq!(buf.len(), 3 * m * lde_size, "slab buffer shape");
@@ -544,6 +546,8 @@ pub fn build_comp_poly_tree_from_slabs_dev(
544546
pub fn build_comp_poly_tree_from_evals_ext3_keep(
545547
parts_interleaved: &[&[u64]],
546548
) -> Result<crate::lde::GpuMerkleTree> {
549+
#[cfg(feature = "test-faults")]
550+
crate::faults::check_sticky(&crate::faults::FAULT_COMP_TREE_STICKY)?;
547551
let (nodes_dev, num_leaves, stream) = build_comp_poly_tree_nodes_dev(parts_interleaved)?;
548552
let mut root = [0u8; 32];
549553
stream.memcpy_dtoh(&nodes_dev.slice(0..32), &mut root)?;

‎crypto/stark/src/gpu_lde.rs‎

Lines changed: 172 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub fn reset_all_gpu_call_counters() {
117117
GPU_DEVICE_ONLY_DOWNGRADES.store(0, Ordering::Relaxed);
118118
GPU_RESIDENT_AUX_RETRIES.store(0, Ordering::Relaxed);
119119
GPU_RESIDENT_AUX_DOWNGRADES.store(0, Ordering::Relaxed);
120+
GPU_COMPOSITION_PARTS_DOWNLOADS.store(0, Ordering::Relaxed);
120121
}
121122

122123
pub(crate) static GPU_EXTEND_HALVES_CALLS: AtomicU64 = AtomicU64::new(0);
@@ -1464,16 +1465,19 @@ pub fn gpu_fri_calls() -> u64 {
14641465
/// are counted here, so a single failed dispatch does not necessarily lower
14651466
/// the total; R3's fallbacks are CPU-only, so a failure there does.
14661467
pub(crate) static GPU_BATCH_INVERT_CALLS: AtomicU64 = AtomicU64::new(0);
1467-
/// R2 downgrades, and only those: times a device-only table fell back to the
1468-
/// host evaluator and had its resident LDEs downloaded into the host buffers
1469-
/// first ([`materialize_lde_trace_host`], the sole site that bumps this).
1470-
/// Nonzero means the device-only gate cleared a table whose R2 dispatch then
1471-
/// declined at runtime — the table continued host-backed, correct but slower —
1472-
/// so every count is a gate miss, and the fix is to mirror the missing
1473-
/// condition into the gate. The R1 resident-aux downgrade is counted by
1474-
/// [`GPU_RESIDENT_AUX_DOWNGRADES`] instead: it fires on tables the gate never
1475-
/// marked device-only, so summing the two would blame the gate for declines it
1476-
/// never made.
1468+
/// Device-only trace downgrades: times a device-only table fell back to a
1469+
/// host arm and had its resident LDEs downloaded into the host buffers first
1470+
/// ([`materialize_lde_trace_host`], the sole function that bumps this —
1471+
/// entered from the R2 host evaluator, the R3 barycentric arms and the R4
1472+
/// DEEP host loop). Nonzero means the device-only gate cleared a table whose
1473+
/// downstream dispatch then declined at runtime — the table continued
1474+
/// host-backed, correct but slower. A count is either a gate miss (a static
1475+
/// condition worth mirroring into the gate) or a transient device decline
1476+
/// (VRAM pressure), which by definition cannot be gated out — see
1477+
/// [`materialize_lde_trace_host`]'s own note. The R1 resident-aux downgrade
1478+
/// is counted by [`GPU_RESIDENT_AUX_DOWNGRADES`] instead: it fires on tables
1479+
/// the gate never marked device-only, so summing the two would blame the gate
1480+
/// for declines it never made.
14771481
pub(crate) static GPU_DEVICE_ONLY_DOWNGRADES: AtomicU64 = AtomicU64::new(0);
14781482
pub fn gpu_device_only_downgrades() -> u64 {
14791483
GPU_DEVICE_ONLY_DOWNGRADES.load(Ordering::Relaxed)
@@ -1493,6 +1497,18 @@ pub fn gpu_resident_aux_downgrades() -> u64 {
14931497
GPU_RESIDENT_AUX_DOWNGRADES.load(Ordering::Relaxed)
14941498
}
14951499

1500+
/// Times the composition-poly parts of a device-only table were downloaded
1501+
/// from the resident R2 handle so a host consumer could run
1502+
/// ([`download_composition_parts_host`], the sole site that bumps this). The
1503+
/// parts-side counterpart of [`GPU_DEVICE_ONLY_DOWNGRADES`]: that one covers
1504+
/// the trace LDEs, this one the H part evaluations whose R2 host drain was
1505+
/// skipped, when the R2 commit, the R3 parts OOD or the R4 DEEP H terms later
1506+
/// fall back to the host path.
1507+
pub(crate) static GPU_COMPOSITION_PARTS_DOWNLOADS: AtomicU64 = AtomicU64::new(0);
1508+
pub fn gpu_composition_parts_downloads() -> u64 {
1509+
GPU_COMPOSITION_PARTS_DOWNLOADS.load(Ordering::Relaxed)
1510+
}
1511+
14961512
/// Times the R1 resident-aux LDE declined and the prover drained the device to
14971513
/// retry it (prover.rs). Nonzero means the device hit transient VRAM pressure —
14981514
/// the retry is what keeps a decline from becoming a
@@ -1725,6 +1741,109 @@ where
17251741
true
17261742
}
17271743

1744+
/// Parts counterpart of [`materialize_lde_trace_host`]: download the resident
1745+
/// composition-poly parts (de-interleaved ext3 slabs, natural evaluation
1746+
/// order) into per-part host Vecs. Serves the host consumers of the part
1747+
/// evaluations — the R2 Merkle commit, the R3 parts OOD and the R4 DEEP H
1748+
/// terms — when a device dispatch declines on a table whose R2 host drain was
1749+
/// skipped (device-only). Returns `None` when the handle cannot serve the
1750+
/// data: a non-ext3 field, a failed download or sync.
1751+
pub(crate) fn download_composition_parts_host<E>(
1752+
h: &math_cuda::lde::GpuLdeExt3,
1753+
stream: &Arc<math_cuda::CudaStream>,
1754+
) -> Option<Vec<Vec<FieldElement<E>>>>
1755+
where
1756+
E: IsField + 'static,
1757+
{
1758+
if TypeId::of::<E>() != TypeId::of::<Degree3GoldilocksExtensionField>() {
1759+
return None;
1760+
}
1761+
h.wait_ready_on(stream).ok()?;
1762+
let slabs = stream.clone_dtoh(h.buf.as_ref()).ok()?;
1763+
stream.synchronize().ok()?;
1764+
let (m, lde) = (h.m, h.lde_size);
1765+
if slabs.len() != m * lde * 3 {
1766+
return None;
1767+
}
1768+
// Per part: de-interleave the 3 slabs into row-major ext3 and reinterpret
1769+
// the u64 buffer in place — mirroring `materialize_lde_trace_host` rather
1770+
// than copying again through `u64_to_ext3_vec`. The row fill is parallel;
1771+
// this path fires often under VRAM pressure and otherwise dominates the
1772+
// D2H it follows.
1773+
let parts = (0..m)
1774+
.map(|p| {
1775+
let mut interleaved = vec![0u64; lde * 3];
1776+
#[cfg(feature = "parallel")]
1777+
interleaved
1778+
.par_chunks_exact_mut(3)
1779+
.enumerate()
1780+
.for_each(|(r, dst)| {
1781+
for (k, d) in dst.iter_mut().enumerate() {
1782+
*d = slabs[(p * 3 + k) * lde + r];
1783+
}
1784+
});
1785+
#[cfg(not(feature = "parallel"))]
1786+
for (r, dst) in interleaved.chunks_exact_mut(3).enumerate() {
1787+
for (k, d) in dst.iter_mut().enumerate() {
1788+
*d = slabs[(p * 3 + k) * lde + r];
1789+
}
1790+
}
1791+
// SAFETY: E == Ext3 per the tower check above; FieldElement<Ext3>
1792+
// is [u64; 3]. `vec![0u64; lde*3]` has len == capacity == lde*3.
1793+
unsafe {
1794+
let mut v = std::mem::ManuallyDrop::new(interleaved);
1795+
debug_assert!(
1796+
v.len().is_multiple_of(3) && v.capacity().is_multiple_of(3),
1797+
"interleaved len/capacity must be a multiple of 3 for Fp3 reinterpret"
1798+
);
1799+
Vec::from_raw_parts(
1800+
v.as_mut_ptr() as *mut FieldElement<E>,
1801+
v.len() / 3,
1802+
v.capacity() / 3,
1803+
)
1804+
}
1805+
})
1806+
.collect();
1807+
GPU_COMPOSITION_PARTS_DOWNLOADS.fetch_add(1, Ordering::Relaxed);
1808+
Some(parts)
1809+
}
1810+
1811+
/// Repopulate empty host part evaluations from the resident R2 parts handle
1812+
/// held by `lde_trace`. Already-populated evaluations are left untouched (the
1813+
/// R2 host drain ran, nothing is missing). Returns false only when the parts
1814+
/// are empty and the handle cannot serve them — a missing handle or bound
1815+
/// stream, a handle whose part count disagrees with the evaluations, or a
1816+
/// failed download — so the caller's abort carries the device-only contract's
1817+
/// message.
1818+
pub(crate) fn materialize_composition_parts_host<F, E>(
1819+
lde_trace: &crate::trace::LDETraceTable<F, E>,
1820+
evals: &mut [Vec<FieldElement<E>>],
1821+
) -> bool
1822+
where
1823+
F: IsField + IsSubFieldOf<E> + 'static,
1824+
E: IsField + 'static,
1825+
{
1826+
if evals.first().is_none_or(|p| !p.is_empty()) {
1827+
return true;
1828+
}
1829+
let Some(h) = lde_trace.gpu_composition_parts() else {
1830+
return false;
1831+
};
1832+
let Some(stream) = lde_trace.bound_stream() else {
1833+
return false;
1834+
};
1835+
if h.m != evals.len() {
1836+
return false;
1837+
}
1838+
let Some(parts) = download_composition_parts_host::<E>(h, &stream) else {
1839+
return false;
1840+
};
1841+
for (dst, src) in evals.iter_mut().zip(parts) {
1842+
*dst = src;
1843+
}
1844+
true
1845+
}
1846+
17281847
pub fn gpu_batch_invert_calls() -> u64 {
17291848
GPU_BATCH_INVERT_CALLS.load(Ordering::Relaxed)
17301849
}
@@ -1764,6 +1883,49 @@ pub fn inverse_fault_fired() -> bool {
17641883
math_cuda::inverse::FAULT_INVERSE_REMAINING_UNTIL_ERR.load(Ordering::Relaxed) < 0
17651884
}
17661885

1886+
/// Test-only: make the Nth upcoming math-cuda barycentric dispatch — and
1887+
/// every one after it — return Err. Sticky, unlike the one-shot hooks above:
1888+
/// the retry arms would absorb a single-shot fault before the fall-through
1889+
/// could reach a device-only cliff site. Pass -1 to disarm (the production
1890+
/// state). Only available with the `test-cuda-faults` feature.
1891+
#[cfg(feature = "test-cuda-faults")]
1892+
pub fn schedule_barycentric_fault_sticky(n_calls_until_err: i64) {
1893+
math_cuda::faults::FAULT_BARYCENTRIC_STICKY.store(n_calls_until_err, Ordering::Relaxed);
1894+
}
1895+
1896+
/// Test-only: whether the sticky barycentric fault reached its firing point
1897+
/// (the countdown parks at 0 once it fires and stays there until disarmed).
1898+
#[cfg(feature = "test-cuda-faults")]
1899+
pub fn barycentric_fault_fired() -> bool {
1900+
math_cuda::faults::FAULT_BARYCENTRIC_STICKY.load(Ordering::Relaxed) == 0
1901+
}
1902+
1903+
/// Sticky counterpart of [`schedule_barycentric_fault_sticky`] for the R4
1904+
/// DEEP composition dispatches (`deep_composition_ext3*`).
1905+
#[cfg(feature = "test-cuda-faults")]
1906+
pub fn schedule_deep_fault_sticky(n_calls_until_err: i64) {
1907+
math_cuda::faults::FAULT_DEEP_STICKY.store(n_calls_until_err, Ordering::Relaxed);
1908+
}
1909+
1910+
/// Test-only counterpart of [`barycentric_fault_fired`] for the DEEP hook.
1911+
#[cfg(feature = "test-cuda-faults")]
1912+
pub fn deep_fault_fired() -> bool {
1913+
math_cuda::faults::FAULT_DEEP_STICKY.load(Ordering::Relaxed) == 0
1914+
}
1915+
1916+
/// Sticky counterpart of [`schedule_barycentric_fault_sticky`] for the R2
1917+
/// comp-poly tree builds (`build_comp_poly_tree_from_*`).
1918+
#[cfg(feature = "test-cuda-faults")]
1919+
pub fn schedule_comp_tree_fault_sticky(n_calls_until_err: i64) {
1920+
math_cuda::faults::FAULT_COMP_TREE_STICKY.store(n_calls_until_err, Ordering::Relaxed);
1921+
}
1922+
1923+
/// Test-only counterpart of [`barycentric_fault_fired`] for the comp-tree hook.
1924+
#[cfg(feature = "test-cuda-faults")]
1925+
pub fn comp_tree_fault_fired() -> bool {
1926+
math_cuda::faults::FAULT_COMP_TREE_STICKY.load(Ordering::Relaxed) == 0
1927+
}
1928+
17671929
/// R2 GPU dispatch: batched ext3 LDE over `parts_coefs` (composition-poly
17681930
/// coefficient parts). Returns both the host LDE eval Vecs (needed for the
17691931
/// R2 Merkle commit and R3 OOD path) and a device-resident `GpuLdeExt3`

0 commit comments

Comments
 (0)