Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crypto/math-cuda/src/barycentric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ pub fn barycentric_base_on_device(
inv_denoms_ext3: &[u64],
n: usize,
) -> Result<Vec<u64>> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_BARYCENTRIC_STICKY)?;
assert_eq!(coset_points.len(), n);
assert_eq!(inv_denoms_ext3.len(), 3 * n);
let num_cols = main_handle.m;
Expand Down Expand Up @@ -204,6 +206,8 @@ pub fn barycentric_base_on_device_with_dev_inv_denoms(
inv_offset_u64: usize,
n: usize,
) -> Result<Vec<u64>> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_BARYCENTRIC_STICKY)?;
main_handle.wait_ready_on(stream)?;
assert!(coset_points_dev.len() >= n);
let inv_end = inv_offset_u64
Expand Down Expand Up @@ -255,6 +259,8 @@ pub fn barycentric_ext3_on_device(
inv_denoms_ext3: &[u64],
n: usize,
) -> Result<Vec<u64>> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_BARYCENTRIC_STICKY)?;
assert_eq!(coset_points.len(), n);
assert_eq!(inv_denoms_ext3.len(), 3 * n);
let num_cols = aux_handle.m;
Expand Down Expand Up @@ -308,6 +314,8 @@ pub fn barycentric_ext3_on_device_with_dev_inv_denoms(
inv_offset_u64: usize,
n: usize,
) -> Result<Vec<u64>> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_BARYCENTRIC_STICKY)?;
aux_handle.wait_ready_on(stream)?;
assert!(coset_points_dev.len() >= n);
let inv_end = inv_offset_u64
Expand Down
8 changes: 8 additions & 0 deletions crypto/math-cuda/src/deep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub fn deep_composition_ext3(
row_stride: usize,
domain_size: usize,
) -> Result<Vec<u64>> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_DEEP_STICKY)?;
let be = backend()?;
let stream = be.next_stream();
deep_composition_ext3_impl(
Expand Down Expand Up @@ -86,6 +88,8 @@ pub fn deep_composition_ext3_with_dev_parts(
row_stride: usize,
domain_size: usize,
) -> Result<Vec<u64>> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_DEEP_STICKY)?;
let be = backend()?;
let stream = be.next_stream();
deep_composition_ext3_impl(
Expand Down Expand Up @@ -262,6 +266,8 @@ pub fn deep_composition_ext3_with_dev_parts_and_inv_denoms(
row_stride: usize,
domain_size: usize,
) -> Result<Vec<u64>> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_DEEP_STICKY)?;
let deep_out = deep_fully_resident_launch(
stream,
main_lde,
Expand Down Expand Up @@ -324,6 +330,8 @@ pub fn deep_composition_ext3_fully_resident_keep(
row_stride: usize,
domain_size: usize,
) -> Result<GpuDeepCodeword> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_DEEP_STICKY)?;
assert!(
domain_size.is_power_of_two() && domain_size >= 2,
"bit-reverse needs a power-of-two codeword"
Expand Down
39 changes: 39 additions & 0 deletions crypto/math-cuda/src/faults.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! Sticky fault-injection hooks for the GPU error-path tests.
//!
//! Unlike the one-shot hooks in `fri` and `inverse` (which disarm after
//! firing, so a drain-and-retry absorbs the injected error before it can
//! surface), a sticky hook keeps failing once its armed call count is
//! reached, until explicitly disarmed. The device-decline recovery tests
//! need that: a stage falls through to its host path only when every device
//! arm of that stage declines in the same prove.

use std::sync::atomic::{AtomicI64, Ordering};

use crate::Result;

/// R3 barycentric entries (`barycentric_{base,ext3}_on_device{,_with_dev_inv_denoms}`).
pub static FAULT_BARYCENTRIC_STICKY: AtomicI64 = AtomicI64::new(-1);
/// R4 DEEP composition entries (`deep_composition_ext3*`).
pub static FAULT_DEEP_STICKY: AtomicI64 = AtomicI64::new(-1);
/// R2 comp-poly tree entries (`build_comp_poly_tree_from_{evals_ext3_keep,slabs_dev}`).
pub static FAULT_COMP_TREE_STICKY: AtomicI64 = AtomicI64::new(-1);

/// Countdown check shared by the sticky hooks: negative = disarmed (the
/// production state); N > 0 counts down across calls and the Nth call — and
/// every call after it — returns Err (the counter parks at 0); 0 therefore
/// doubles as the "fired" marker. Disarm by storing -1.
pub fn check_sticky(counter: &AtomicI64) -> Result<()> {
let v = counter.load(Ordering::Relaxed);
if v < 0 {
return Ok(());
}
if v > 0 {
counter.fetch_sub(1, Ordering::Relaxed);
}
if v <= 1 {
return Err(cudarc::driver::DriverError(
cudarc::driver::sys::CUresult::CUDA_ERROR_UNKNOWN,
));
}
Ok(())
}
2 changes: 2 additions & 0 deletions crypto/math-cuda/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub mod barycentric;
pub mod constraint_interp;
pub mod deep;
pub mod device;
#[cfg(feature = "test-faults")]
pub mod faults;
pub mod fri;
pub mod inverse;
pub mod lde;
Expand Down
4 changes: 4 additions & 0 deletions crypto/math-cuda/src/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ pub fn build_comp_poly_tree_from_slabs_dev(
m: usize,
lde_size: usize,
) -> Result<crate::lde::GpuMerkleTree> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_COMP_TREE_STICKY)?;
assert!(m > 0);
assert!(lde_size.is_power_of_two() && lde_size >= 2);
assert_eq!(buf.len(), 3 * m * lde_size, "slab buffer shape");
Expand Down Expand Up @@ -544,6 +546,8 @@ pub fn build_comp_poly_tree_from_slabs_dev(
pub fn build_comp_poly_tree_from_evals_ext3_keep(
parts_interleaved: &[&[u64]],
) -> Result<crate::lde::GpuMerkleTree> {
#[cfg(feature = "test-faults")]
crate::faults::check_sticky(&crate::faults::FAULT_COMP_TREE_STICKY)?;
let (nodes_dev, num_leaves, stream) = build_comp_poly_tree_nodes_dev(parts_interleaved)?;
let mut root = [0u8; 32];
stream.memcpy_dtoh(&nodes_dev.slice(0..32), &mut root)?;
Expand Down
153 changes: 143 additions & 10 deletions crypto/stark/src/gpu_lde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ pub fn reset_all_gpu_call_counters() {
GPU_DEVICE_ONLY_DOWNGRADES.store(0, Ordering::Relaxed);
GPU_RESIDENT_AUX_RETRIES.store(0, Ordering::Relaxed);
GPU_RESIDENT_AUX_DOWNGRADES.store(0, Ordering::Relaxed);
GPU_COMPOSITION_PARTS_DOWNLOADS.store(0, Ordering::Relaxed);
}

pub(crate) static GPU_EXTEND_HALVES_CALLS: AtomicU64 = AtomicU64::new(0);
Expand Down Expand Up @@ -1464,16 +1465,17 @@ pub fn gpu_fri_calls() -> u64 {
/// are counted here, so a single failed dispatch does not necessarily lower
/// the total; R3's fallbacks are CPU-only, so a failure there does.
pub(crate) static GPU_BATCH_INVERT_CALLS: AtomicU64 = AtomicU64::new(0);
/// R2 downgrades, and only those: times a device-only table fell back to the
/// host evaluator and had its resident LDEs downloaded into the host buffers
/// first ([`materialize_lde_trace_host`], the sole site that bumps this).
/// Nonzero means the device-only gate cleared a table whose R2 dispatch then
/// declined at runtime — the table continued host-backed, correct but slower —
/// so every count is a gate miss, and the fix is to mirror the missing
/// condition into the gate. The R1 resident-aux downgrade is counted by
/// [`GPU_RESIDENT_AUX_DOWNGRADES`] instead: it fires on tables the gate never
/// marked device-only, so summing the two would blame the gate for declines it
/// never made.
/// Device-only trace downgrades: times a device-only table fell back to a
/// host arm and had its resident LDEs downloaded into the host buffers first
/// ([`materialize_lde_trace_host`], the sole function that bumps this —
/// entered from the R2 host evaluator, the R3 barycentric arms and the R4
/// DEEP host loop). Nonzero means the device-only gate cleared a table whose
/// downstream dispatch then declined at runtime — the table continued
/// host-backed, correct but slower — so every count is a gate miss, and the
/// fix is to mirror the missing condition into the gate. The R1 resident-aux
/// downgrade is counted by [`GPU_RESIDENT_AUX_DOWNGRADES`] instead: it fires
/// on tables the gate never marked device-only, so summing the two would
/// blame the gate for declines it never made.
pub(crate) static GPU_DEVICE_ONLY_DOWNGRADES: AtomicU64 = AtomicU64::new(0);
pub fn gpu_device_only_downgrades() -> u64 {
GPU_DEVICE_ONLY_DOWNGRADES.load(Ordering::Relaxed)
Expand All @@ -1493,6 +1495,18 @@ pub fn gpu_resident_aux_downgrades() -> u64 {
GPU_RESIDENT_AUX_DOWNGRADES.load(Ordering::Relaxed)
}

/// Times the composition-poly parts of a device-only table were downloaded
/// from the resident R2 handle so a host consumer could run
/// ([`download_composition_parts_host`], the sole site that bumps this). The
/// parts-side counterpart of [`GPU_DEVICE_ONLY_DOWNGRADES`]: that one covers
/// the trace LDEs, this one the H part evaluations whose R2 host drain was
/// skipped, when the R2 commit, the R3 parts OOD or the R4 DEEP H terms later
/// fall back to the host path.
pub(crate) static GPU_COMPOSITION_PARTS_DOWNLOADS: AtomicU64 = AtomicU64::new(0);
pub fn gpu_composition_parts_downloads() -> u64 {
GPU_COMPOSITION_PARTS_DOWNLOADS.load(Ordering::Relaxed)
}

/// Times the R1 resident-aux LDE declined and the prover drained the device to
/// retry it (prover.rs). Nonzero means the device hit transient VRAM pressure —
/// the retry is what keeps a decline from becoming a
Expand Down Expand Up @@ -1725,6 +1739,82 @@ where
true
}

/// Parts counterpart of [`materialize_lde_trace_host`]: download the resident
/// composition-poly parts (de-interleaved ext3 slabs, natural evaluation
/// order) into per-part host Vecs. Serves the host consumers of the part
/// evaluations — the R2 Merkle commit, the R3 parts OOD and the R4 DEEP H
/// terms — when a device dispatch declines on a table whose R2 host drain was
/// skipped (device-only). Returns `None` when the handle cannot serve the
/// data: a non-ext3 field, a failed download or sync.
pub(crate) fn download_composition_parts_host<E>(
h: &math_cuda::lde::GpuLdeExt3,
stream: &Arc<math_cuda::CudaStream>,
) -> Option<Vec<Vec<FieldElement<E>>>>
where
E: IsField + 'static,
{
if TypeId::of::<E>() != TypeId::of::<Degree3GoldilocksExtensionField>() {
return None;
}
h.wait_ready_on(stream).ok()?;
let slabs = stream.clone_dtoh(h.buf.as_ref()).ok()?;
stream.synchronize().ok()?;
let (m, lde) = (h.m, h.lde_size);
if slabs.len() != m * lde * 3 {
return None;
}
let parts = (0..m)
.map(|p| {
let mut interleaved = vec![0u64; lde * 3];
for k in 0..3 {
let slab = &slabs[(p * 3 + k) * lde..(p * 3 + k + 1) * lde];
for (r, v) in slab.iter().enumerate() {
interleaved[r * 3 + k] = *v;
}
}
u64_to_ext3_vec::<E>(&interleaved)
})
.collect();
GPU_COMPOSITION_PARTS_DOWNLOADS.fetch_add(1, Ordering::Relaxed);
Some(parts)
}

/// Repopulate empty host part evaluations from the resident R2 parts handle
/// held by `lde_trace`. Already-populated evaluations are left untouched (the
/// R2 host drain ran, nothing is missing). Returns false only when the parts
/// are empty and the handle cannot serve them — a missing handle or bound
/// stream, a handle whose part count disagrees with the evaluations, or a
/// failed download — so the caller's abort carries the device-only contract's
/// message.
pub(crate) fn materialize_composition_parts_host<F, E>(
lde_trace: &crate::trace::LDETraceTable<F, E>,
evals: &mut [Vec<FieldElement<E>>],
) -> bool
where
F: IsField + IsSubFieldOf<E> + 'static,
E: IsField + 'static,
{
if evals.first().is_none_or(|p| !p.is_empty()) {
return true;
}
let Some(h) = lde_trace.gpu_composition_parts() else {
return false;
};
let Some(stream) = lde_trace.bound_stream() else {
return false;
};
if h.m != evals.len() {
return false;
}
let Some(parts) = download_composition_parts_host::<E>(h, &stream) else {
return false;
};
for (dst, src) in evals.iter_mut().zip(parts) {
*dst = src;
}
true
}

pub fn gpu_batch_invert_calls() -> u64 {
GPU_BATCH_INVERT_CALLS.load(Ordering::Relaxed)
}
Expand Down Expand Up @@ -1764,6 +1854,49 @@ pub fn inverse_fault_fired() -> bool {
math_cuda::inverse::FAULT_INVERSE_REMAINING_UNTIL_ERR.load(Ordering::Relaxed) < 0
}

/// Test-only: make the Nth upcoming math-cuda barycentric dispatch — and
/// every one after it — return Err. Sticky, unlike the one-shot hooks above:
/// the retry arms would absorb a single-shot fault before the fall-through
/// could reach a device-only cliff site. Pass -1 to disarm (the production
/// state). Only available with the `test-cuda-faults` feature.
#[cfg(feature = "test-cuda-faults")]
pub fn schedule_barycentric_fault_sticky(n_calls_until_err: i64) {
math_cuda::faults::FAULT_BARYCENTRIC_STICKY.store(n_calls_until_err, Ordering::Relaxed);
}

/// Test-only: whether the sticky barycentric fault reached its firing point
/// (the countdown parks at 0 once it fires and stays there until disarmed).
#[cfg(feature = "test-cuda-faults")]
pub fn barycentric_fault_fired() -> bool {
math_cuda::faults::FAULT_BARYCENTRIC_STICKY.load(Ordering::Relaxed) == 0
}

/// Sticky counterpart of [`schedule_barycentric_fault_sticky`] for the R4
/// DEEP composition dispatches (`deep_composition_ext3*`).
#[cfg(feature = "test-cuda-faults")]
pub fn schedule_deep_fault_sticky(n_calls_until_err: i64) {
math_cuda::faults::FAULT_DEEP_STICKY.store(n_calls_until_err, Ordering::Relaxed);
}

/// Test-only counterpart of [`barycentric_fault_fired`] for the DEEP hook.
#[cfg(feature = "test-cuda-faults")]
pub fn deep_fault_fired() -> bool {
math_cuda::faults::FAULT_DEEP_STICKY.load(Ordering::Relaxed) == 0
}

/// Sticky counterpart of [`schedule_barycentric_fault_sticky`] for the R2
/// comp-poly tree builds (`build_comp_poly_tree_from_*`).
#[cfg(feature = "test-cuda-faults")]
pub fn schedule_comp_tree_fault_sticky(n_calls_until_err: i64) {
math_cuda::faults::FAULT_COMP_TREE_STICKY.store(n_calls_until_err, Ordering::Relaxed);
}

/// Test-only counterpart of [`barycentric_fault_fired`] for the comp-tree hook.
#[cfg(feature = "test-cuda-faults")]
pub fn comp_tree_fault_fired() -> bool {
math_cuda::faults::FAULT_COMP_TREE_STICKY.load(Ordering::Relaxed) == 0
}

/// R2 GPU dispatch: batched ext3 LDE over `parts_coefs` (composition-poly
/// coefficient parts). Returns both the host LDE eval Vecs (needed for the
/// R2 Merkle commit and R3 OOD path) and a device-resident `GpuLdeExt3`
Expand Down
Loading
Loading