use super::{GpuBackend, GpuError, TaylorBatchResult};
use crate::stde::EstimatorResult;
fn require_scalar_output<B: GpuBackend>(
backend: &B,
tape: &B::TapeBuffers,
caller: &str,
) -> Result<(), GpuError> {
let outputs = backend.num_outputs(tape);
if outputs != 1 {
return Err(GpuError::Other(format!(
"{caller} requires a scalar-output tape; got {outputs} outputs"
)));
}
Ok(())
}
fn require_nonempty_x(n: usize, caller: &str, quantity: &str) -> Result<(), GpuError> {
if n == 0 {
return Err(GpuError::Other(format!(
"{caller} requires a non-empty x (a zero-input tape has no \
{quantity}); zero-sized GPU buffers would panic"
)));
}
Ok(())
}
pub fn laplacian_gpu<B: GpuBackend>(
backend: &B,
tape: &B::TapeBuffers,
x: &[f32],
directions: &[&[f32]],
) -> Result<EstimatorResult<f32>, GpuError> {
require_scalar_output(backend, tape, "laplacian_gpu")?;
let n = x.len();
require_nonempty_x(n, "laplacian_gpu", "Laplacian")?;
let s = directions.len();
if s == 0 {
return Err(GpuError::Other("no directions provided".into()));
}
let mut primals = Vec::with_capacity(s * n);
let mut seeds = Vec::with_capacity(s * n);
for dir in directions {
assert_eq!(dir.len(), n, "direction length must match x");
primals.extend_from_slice(x);
seeds.extend_from_slice(dir);
}
assert!(
s <= u32::MAX as usize,
"too many directions for GPU dispatch"
);
let result = backend.taylor_forward_2nd_batch(tape, &primals, &seeds, s as u32)?;
Ok(aggregate_laplacian(&result, s))
}
fn aggregate_laplacian(result: &TaylorBatchResult<f32>, s: usize) -> EstimatorResult<f32> {
let value = result.values[0];
let mut acc = crate::stde::WelfordAccumulator::new();
for i in 0..s {
acc.update(2.0 * result.c2s[i]); }
acc.into_result(value)
}
pub fn hessian_diagonal_gpu<B: GpuBackend>(
backend: &B,
tape: &B::TapeBuffers,
x: &[f32],
) -> Result<(f32, Vec<f32>), GpuError> {
require_scalar_output(backend, tape, "hessian_diagonal_gpu")?;
let n = x.len();
require_nonempty_x(n, "hessian_diagonal_gpu", "Hessian diagonal")?;
let mut primals = Vec::with_capacity(n * n);
let mut seeds = vec![0.0f32; n * n];
for j in 0..n {
primals.extend_from_slice(x);
seeds[j * n + j] = 1.0;
}
assert!(n <= u32::MAX as usize, "too many inputs for GPU dispatch");
let result = backend.taylor_forward_2nd_batch(tape, &primals, &seeds, n as u32)?;
let value = result.values[0];
let diag: Vec<f32> = result.c2s.iter().map(|&c2| 2.0 * c2).collect();
Ok((value, diag))
}
pub fn laplacian_with_control_gpu<B: GpuBackend>(
backend: &B,
tape: &B::TapeBuffers,
x: &[f32],
directions: &[&[f32]],
control_diagonal: &[f32],
) -> Result<EstimatorResult<f32>, GpuError> {
require_scalar_output(backend, tape, "laplacian_with_control_gpu")?;
let n = x.len();
require_nonempty_x(n, "laplacian_with_control_gpu", "Laplacian")?;
let s = directions.len();
assert_eq!(
control_diagonal.len(),
n,
"control diagonal length must match x"
);
if s == 0 {
return Err(GpuError::Other("no directions provided".into()));
}
let mut primals = Vec::with_capacity(s * n);
let mut seeds = Vec::with_capacity(s * n);
for dir in directions {
assert_eq!(dir.len(), n, "direction length must match x");
primals.extend_from_slice(x);
seeds.extend_from_slice(dir);
}
assert!(
s <= u32::MAX as usize,
"too many directions for GPU dispatch"
);
let result = backend.taylor_forward_2nd_batch(tape, &primals, &seeds, s as u32)?;
let trace_diag: f32 = control_diagonal.iter().sum();
let value = result.values[0];
let mut acc = crate::stde::WelfordAccumulator::new();
for (i, dir) in directions.iter().enumerate() {
let vhv = 2.0 * result.c2s[i]; let v_diag_v: f32 = dir
.iter()
.zip(control_diagonal.iter())
.map(|(&vj, &dj)| dj * vj * vj)
.sum();
acc.update(vhv - v_diag_v); }
let (residual_mean, sample_variance, standard_error) = acc.finalize();
let estimate = trace_diag + residual_mean;
Ok(EstimatorResult {
value,
estimate,
sample_variance,
standard_error,
num_samples: acc.contributing(),
})
}