use std::ffi::c_void;
use openvm_cuda_common::{
copy::cuda_memcpy_on,
d_buffer::DeviceBuffer,
error::{CudaError, MemCopyError},
stream::GpuDeviceCtx,
};
use openvm_stark_backend::{
p3_challenger::{CanObserve, CanSample},
FiatShamirTranscript, StarkProtocolConfig,
};
use openvm_stark_sdk::config::baby_bear_poseidon2::poseidon2_perm;
use p3_baby_bear::default_babybear_poseidon2_16;
use p3_field::{PrimeCharacteristicRing, PrimeField32};
use p3_symmetric::Permutation;
use crate::types::{Challenger, Digest, CHUNK, F, SC, WIDTH};
pub(crate) fn validate_gpu_grind_bits(bits: usize) -> Result<(), GrindError> {
if bits >= u32::BITS as usize || (1u64 << bits) >= u64::from(F::ORDER_U32) {
return Err(CudaError::new(1).into());
}
Ok(())
}
#[repr(C)]
#[derive(Clone, Debug)]
pub struct DeviceSpongeState {
pub state: [F; WIDTH],
pub absorb_idx: u32,
pub sample_idx: u32,
}
impl Default for DeviceSpongeState {
fn default() -> Self {
Self {
state: [F::default(); WIDTH],
absorb_idx: 0,
sample_idx: 0,
}
}
}
impl DeviceSpongeState {
#[inline]
pub fn observe(&mut self, value: F) {
self.state[self.absorb_idx as usize] = value;
self.absorb_idx += 1;
if self.absorb_idx == CHUNK as u32 {
poseidon2_perm().permute_mut(&mut self.state);
self.absorb_idx = 0;
self.sample_idx = CHUNK as u32;
}
}
#[inline]
pub fn sample(&mut self) -> F {
if self.absorb_idx != 0 || self.sample_idx == 0 {
poseidon2_perm().permute_mut(&mut self.state);
self.absorb_idx = 0;
self.sample_idx = CHUNK as u32;
}
self.sample_idx -= 1;
self.state[self.sample_idx as usize]
}
}
impl FiatShamirTranscript<SC> for DeviceSpongeState {
#[inline]
fn observe(&mut self, value: F) {
DeviceSpongeState::observe(self, value);
}
#[inline]
fn sample(&mut self) -> F {
DeviceSpongeState::sample(self)
}
#[inline]
fn observe_commit(&mut self, digest: Digest) {
for x in digest {
self.observe(x);
}
}
}
#[derive(Debug)]
pub struct DuplexSpongeGpu {
host: Challenger,
device: DeviceBuffer<DeviceSpongeState>,
}
impl Default for DuplexSpongeGpu {
fn default() -> Self {
Self::new()
}
}
impl Clone for DuplexSpongeGpu {
fn clone(&self) -> Self {
Self {
host: self.host.clone(),
device: DeviceBuffer::new(),
}
}
}
impl DuplexSpongeGpu {
pub fn new() -> Self {
Self {
host: Challenger::new(default_babybear_poseidon2_16()),
device: DeviceBuffer::new(),
}
}
pub fn is_device_allocated(&self) -> bool {
!self.device.is_empty()
}
fn ensure_device_allocated(&mut self, device_ctx: &GpuDeviceCtx) {
if self.device.is_empty() {
self.device = DeviceBuffer::with_capacity_on(1, device_ctx);
}
}
pub fn sync_h2d(&mut self, device_ctx: &GpuDeviceCtx) -> Result<(), MemCopyError> {
self.ensure_device_allocated(device_ctx);
let mut device_state = DeviceSpongeState {
state: self.host.sponge_state,
absorb_idx: self.host.input_buffer.len() as u32,
sample_idx: self.host.output_buffer.len() as u32,
};
for (i, &val) in self.host.input_buffer.iter().enumerate() {
device_state.state[i] = val;
}
unsafe {
cuda_memcpy_on::<false, true>(
self.device.as_mut_ptr() as *mut c_void,
&device_state as *const DeviceSpongeState as *const c_void,
std::mem::size_of::<DeviceSpongeState>(),
device_ctx,
)
}
}
pub fn device_ptr(&self) -> Option<*const DeviceSpongeState> {
if self.device.is_empty() {
None
} else {
Some(self.device.as_ptr())
}
}
pub fn device_ptr_mut(&mut self) -> Option<*mut DeviceSpongeState> {
if self.device.is_empty() {
None
} else {
Some(self.device.as_mut_ptr())
}
}
pub fn grind_gpu(&mut self, bits: usize, device_ctx: &GpuDeviceCtx) -> Result<F, GrindError> {
validate_gpu_grind_bits(bits)?;
if bits == 0 {
return Ok(F::ZERO);
}
self.sync_h2d(device_ctx)?;
let witness_u32 = unsafe {
crate::cuda::sponge::sponge_grind(
self.device.as_ptr(),
bits as u32,
F::ORDER_U32 - 1,
device_ctx,
)?
};
let witness = F::from_u32(witness_u32);
debug_assert!(self.clone().check_witness(bits, witness));
self.host.observe(witness);
let _: F = self.host.sample();
Ok(witness)
}
}
#[derive(Debug, thiserror::Error)]
pub enum GrindError {
#[error("Memory copy error: {0}")]
MemCopy(#[from] MemCopyError),
#[error("CUDA error: {0}")]
Cuda(#[from] CudaError),
#[error("Failed to find PoW witness within search space")]
WitnessNotFound,
}
impl FiatShamirTranscript<SC> for DuplexSpongeGpu {
#[inline]
fn observe(&mut self, value: F) {
self.host.observe(value);
}
#[inline]
fn sample(&mut self) -> F {
self.host.sample()
}
#[inline]
fn observe_commit(&mut self, digest: Digest) {
for x in digest {
self.observe(x);
}
}
}
pub trait GpuFiatShamirTranscript<Config: StarkProtocolConfig>:
FiatShamirTranscript<Config>
{
fn grind_gpu(
&mut self,
bits: usize,
device_ctx: &GpuDeviceCtx,
) -> Result<Config::F, GrindError>;
}
impl GpuFiatShamirTranscript<SC> for DuplexSpongeGpu {
fn grind_gpu(&mut self, bits: usize, device_ctx: &GpuDeviceCtx) -> Result<F, GrindError> {
DuplexSpongeGpu::grind_gpu(self, bits, device_ctx)
}
}
#[cfg(test)]
mod tests {
use std::time::Instant;
use openvm_cuda_common::{
common::get_device,
stream::{CudaStream, GpuDeviceCtx, StreamGuard},
};
use openvm_stark_sdk::config::baby_bear_poseidon2::default_duplex_sponge;
use p3_field::PrimeCharacteristicRing;
use super::*;
use crate::prelude::SC;
fn test_ctx() -> GpuDeviceCtx {
GpuDeviceCtx {
device_id: get_device().unwrap() as u32,
stream: StreamGuard::new(CudaStream::new_non_blocking().unwrap()),
}
}
#[test]
fn test_device_sponge_state_size() {
let expected_size = std::mem::size_of::<[F; WIDTH]>() + std::mem::size_of::<u32>() + std::mem::size_of::<u32>();
assert_eq!(
std::mem::size_of::<DeviceSpongeState>(),
expected_size,
"DeviceSpongeState size mismatch - check repr(C) and padding"
);
}
#[test]
fn test_device_sponge_state_alignment() {
assert!(
std::mem::align_of::<DeviceSpongeState>() >= 4,
"DeviceSpongeState should be at least 4-byte aligned"
);
}
#[test]
fn test_default_state() {
let state = DeviceSpongeState::default();
assert_eq!(state.absorb_idx, 0);
assert_eq!(state.sample_idx, 0);
for elem in state.state.iter() {
assert_eq!(*elem, F::default());
}
}
#[test]
fn test_sponge_gpu_new() {
let sponge = DuplexSpongeGpu::new();
assert!(!sponge.is_device_allocated());
}
#[test]
fn test_device_sponge_state_matches_duplex_sponge() {
let mut device_state = DeviceSpongeState::default();
let mut duplex_sponge = default_duplex_sponge();
for i in 0..20 {
let val = F::from_u32(i * 42 + 17);
device_state.observe(val);
FiatShamirTranscript::<SC>::observe(&mut duplex_sponge, val);
}
for _ in 0..10 {
let device_sample = device_state.sample();
let duplex_sample = FiatShamirTranscript::<SC>::sample(&mut duplex_sponge);
assert_eq!(device_sample, duplex_sample);
}
for i in 0..5 {
let val = F::from_u32(i * 100);
device_state.observe(val);
FiatShamirTranscript::<SC>::observe(&mut duplex_sponge, val);
let device_sample = device_state.sample();
let duplex_sample = FiatShamirTranscript::<SC>::sample(&mut duplex_sponge);
assert_eq!(device_sample, duplex_sample);
}
for _ in 0..15 {
let device_sample = device_state.sample();
let duplex_sample = FiatShamirTranscript::<SC>::sample(&mut duplex_sponge);
assert_eq!(device_sample, duplex_sample);
}
}
#[test]
fn test_sponge_gpu_uses_host_transcript() {
let mut gpu_sponge = DuplexSpongeGpu::default();
let mut cpu_sponge = default_duplex_sponge();
for i in 0..10 {
let val = F::from_u32(i * 42 + 17);
gpu_sponge.observe(val);
FiatShamirTranscript::<SC>::observe(&mut cpu_sponge, val);
}
for _ in 0..5 {
let gpu_sample = gpu_sponge.sample();
let cpu_sample = FiatShamirTranscript::<SC>::sample(&mut cpu_sponge);
assert_eq!(gpu_sample, cpu_sample);
}
}
#[test]
fn test_grind_cpu_vs_gpu() {
let device_ctx = test_ctx();
{
let mut warmup = DuplexSpongeGpu::default();
let _ = warmup.grind_gpu(8, &device_ctx);
}
let bit_counts = [8, 12, 16, 18, 20]
.iter()
.flat_map(|x| std::iter::repeat_n(*x, 5))
.collect::<Vec<_>>();
eprintln!("\n{}", "=".repeat(60));
eprintln!("Grinding Performance: CPU vs GPU");
eprintln!("{}", "=".repeat(60));
eprintln!(
"{:>6} {:>12} {:>12} {:>10}",
"bits", "CPU (ms)", "GPU (ms)", "speedup"
);
eprintln!("{:->6} {:->12} {:->12} {:->10}", "", "", "", "");
let mut seed = 265;
for bits in bit_counts {
let mut cpu_sponge = default_duplex_sponge();
let mut gpu_sponge = DuplexSpongeGpu::default();
for _ in 0..5 {
let val = F::from_u32(seed);
seed += 228;
FiatShamirTranscript::<SC>::observe(&mut cpu_sponge, val);
gpu_sponge.observe(val);
}
let cpu_start = Instant::now();
let cpu_witness = FiatShamirTranscript::<SC>::grind(&mut cpu_sponge, bits);
let cpu_time = cpu_start.elapsed();
let gpu_start = Instant::now();
let gpu_witness = gpu_sponge
.grind_gpu(bits, &device_ctx)
.expect("GPU grinding failed");
let gpu_time = gpu_start.elapsed();
let speedup = cpu_time.as_secs_f64() / gpu_time.as_secs_f64();
eprintln!(
"{:>6} {:>12.2} {:>12.2} {:>10.2}x",
bits,
cpu_time.as_secs_f64() * 1000.0,
gpu_time.as_secs_f64() * 1000.0,
speedup
);
let _ = (cpu_witness, gpu_witness); }
eprintln!("{}\n", "=".repeat(60));
}
}