#[cfg(feature = "cuda-runtime")]
use std::sync::{Arc, Mutex};
#[cfg(feature = "cuda-runtime")]
use std::thread;
#[cfg(feature = "cuda-runtime")]
use cudarc::driver::{CudaDevice, DeviceSlice, DevicePtr};
#[cfg(feature = "cuda-runtime")]
use super::cuda_executor::CudaFftError;
#[cfg(feature = "cuda-runtime")]
use super::pipeline::GpuProofPipeline;
#[derive(Debug, Clone)]
#[cfg(feature = "cuda-runtime")]
pub struct GpuCapabilities {
pub device_id: usize,
pub compute_capability: (u32, u32),
pub sm_count: u32,
pub total_memory: usize,
pub memory_bandwidth_gbps: f32,
pub has_tensor_cores: bool,
pub relative_power: f32,
}
#[cfg(feature = "cuda-runtime")]
impl GpuCapabilities {
pub fn query(device_id: usize) -> Result<Self, CudaFftError> {
use super::compat;
use cudarc::driver::sys::CUdevice_attribute;
let device = CudaDevice::new(device_id)
.map_err(|e| CudaFftError::DriverInit(format!("{:?}", e)))?;
let cu_device = device.cu_device();
let major = compat::device_get_attribute(
*cu_device,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR
).map_err(|e| CudaFftError::DriverInit(e))? as u32;
let minor = compat::device_get_attribute(
*cu_device,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR
).map_err(|e| CudaFftError::DriverInit(e))? as u32;
let sm_count = compat::device_get_attribute(
*cu_device,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT
).map_err(|e| CudaFftError::DriverInit(e))? as u32;
let total_memory = compat::device_total_mem(*cu_device)
.unwrap_or(8 * 1024 * 1024 * 1024);
let memory_bandwidth_gbps = Self::estimate_bandwidth(major, sm_count);
let has_tensor_cores = major >= 7;
let relative_power = Self::calculate_relative_power(major, sm_count);
Ok(GpuCapabilities {
device_id,
compute_capability: (major, minor),
sm_count,
total_memory,
memory_bandwidth_gbps,
has_tensor_cores,
relative_power,
})
}
fn estimate_bandwidth(sm_major: u32, sm_count: u32) -> f32 {
match sm_major {
9 => 3350.0, 8 => 2000.0, 7 => 900.0, 6 => 480.0, _ => 200.0 + (sm_count as f32 * 4.0), }
}
fn calculate_relative_power(sm_major: u32, sm_count: u32) -> f32 {
let arch_multiplier = match sm_major {
9 => 3.0, 8 => 1.0, 7 => 0.6, 6 => 0.4, _ => 0.2,
};
arch_multiplier * (sm_count as f32 / 108.0)
}
}
#[cfg(feature = "cuda-runtime")]
#[derive(Debug, Clone)]
pub struct GpuTopology {
device_count: usize,
bandwidth_matrix: Vec<Vec<f32>>,
p2p_access: Vec<Vec<bool>>,
nvlink_connected: Vec<Vec<bool>>,
}
#[cfg(feature = "cuda-runtime")]
impl GpuTopology {
pub fn detect() -> Result<Self, CudaFftError> {
use super::compat;
use cudarc::driver::sys::CUdevice_P2PAttribute;
let device_count = compat::device_get_count()
.map_err(|e| CudaFftError::DriverInit(e))?;
let n = device_count as usize;
if n == 0 {
return Err(CudaFftError::NoDevice);
}
let mut bandwidth_matrix = vec![vec![0.0f32; n]; n];
let mut p2p_access = vec![vec![false; n]; n];
let mut nvlink_connected = vec![vec![false; n]; n];
for src in 0..n {
for dst in 0..n {
if src == dst {
bandwidth_matrix[src][dst] = 3000.0; p2p_access[src][dst] = true;
continue;
}
let can_access = compat::device_can_access_peer(src as i32, dst as i32)
.unwrap_or(false);
p2p_access[src][dst] = can_access;
let perf_rank = compat::device_get_p2p_attribute(
CUdevice_P2PAttribute::CU_DEVICE_P2P_ATTRIBUTE_PERFORMANCE_RANK,
src as i32,
dst as i32,
).unwrap_or(0);
nvlink_connected[src][dst] = perf_rank > 0 && can_access;
bandwidth_matrix[src][dst] = if nvlink_connected[src][dst] {
300.0 } else if p2p_access[src][dst] {
32.0 } else {
16.0 };
}
}
tracing::info!(
"Detected GPU topology: {} devices, NVLink pairs: {}",
n,
nvlink_connected.iter().flatten().filter(|&&x| x).count() / 2
);
Ok(Self {
device_count: n,
bandwidth_matrix,
p2p_access,
nvlink_connected,
})
}
pub fn device_count(&self) -> usize {
self.device_count
}
pub fn has_p2p_access(&self, src: usize, dst: usize) -> bool {
src < self.device_count && dst < self.device_count && self.p2p_access[src][dst]
}
pub fn has_nvlink(&self, src: usize, dst: usize) -> bool {
src < self.device_count && dst < self.device_count && self.nvlink_connected[src][dst]
}
pub fn bandwidth(&self, src: usize, dst: usize) -> f32 {
if src < self.device_count && dst < self.device_count {
self.bandwidth_matrix[src][dst]
} else {
0.0
}
}
pub fn best_partner(&self, gpu: usize) -> Option<usize> {
if gpu >= self.device_count {
return None;
}
(0..self.device_count)
.filter(|&i| i != gpu)
.max_by(|&a, &b| {
self.bandwidth_matrix[gpu][a]
.partial_cmp(&self.bandwidth_matrix[gpu][b])
.unwrap()
})
}
pub fn nvlink_pairs(&self) -> Vec<(usize, usize)> {
let mut pairs = Vec::new();
for src in 0..self.device_count {
for dst in (src + 1)..self.device_count {
if self.nvlink_connected[src][dst] {
pairs.push((src, dst));
}
}
}
pairs
}
pub fn optimal_exchange_pairs(&self, num_exchanges: usize) -> Vec<(usize, usize)> {
let mut pairs = self.nvlink_pairs();
if pairs.len() < num_exchanges {
for src in 0..self.device_count {
for dst in (src + 1)..self.device_count {
if self.p2p_access[src][dst] && !self.nvlink_connected[src][dst] {
pairs.push((src, dst));
if pairs.len() >= num_exchanges {
break;
}
}
}
if pairs.len() >= num_exchanges {
break;
}
}
}
pairs.truncate(num_exchanges);
pairs
}
}
#[cfg(feature = "cuda-runtime")]
pub fn enable_p2p_access(src_device: usize, dst_device: usize) -> Result<bool, CudaFftError> {
use super::compat;
if src_device == dst_device {
return Ok(true); }
let can_access = compat::device_can_access_peer(src_device as i32, dst_device as i32)
.unwrap_or(false);
if !can_access {
tracing::debug!("P2P access not possible between GPU {} and GPU {}", src_device, dst_device);
return Ok(false);
}
let src_ctx = CudaDevice::new(src_device)
.map_err(|e| CudaFftError::DriverInit(format!("Failed to get device {}: {:?}", src_device, e)))?;
let dst_ctx = CudaDevice::new(dst_device)
.map_err(|e| CudaFftError::DriverInit(format!("Failed to get device {}: {:?}", dst_device, e)))?;
compat::ctx_set_current(*src_ctx.cu_primary_ctx())
.map_err(|e| CudaFftError::DriverInit(e))?;
match compat::ctx_enable_peer_access(*dst_ctx.cu_primary_ctx(), 0) {
Ok(()) => {
tracing::info!("Enabled P2P access: GPU {} -> GPU {}", src_device, dst_device);
Ok(true)
}
Err(e) => {
tracing::warn!("Failed to enable P2P access GPU {} -> GPU {}: {}", src_device, dst_device, e);
Ok(false)
}
}
}
#[cfg(feature = "cuda-runtime")]
pub fn enable_all_p2p(topology: &GpuTopology) -> Result<usize, CudaFftError> {
let mut enabled_count = 0;
for src in 0..topology.device_count() {
for dst in 0..topology.device_count() {
if src != dst && topology.has_p2p_access(src, dst) {
if enable_p2p_access(src, dst)? {
enabled_count += 1;
}
}
}
}
tracing::info!("Enabled {} P2P connections", enabled_count);
Ok(enabled_count)
}
#[cfg(feature = "cuda-runtime")]
pub struct P2PTransfer {
src_device: usize,
dst_device: usize,
p2p_enabled: bool,
}
#[cfg(feature = "cuda-runtime")]
impl P2PTransfer {
pub fn new(src_device: usize, dst_device: usize, _topology: &GpuTopology) -> Result<Self, CudaFftError> {
let p2p_enabled = if src_device != dst_device {
enable_p2p_access(src_device, dst_device)?
} else {
true
};
Ok(Self {
src_device,
dst_device,
p2p_enabled,
})
}
pub fn copy<T: Copy + Default + cudarc::driver::DeviceRepr>(
&self,
src: &cudarc::driver::CudaSlice<T>,
dst: &mut cudarc::driver::CudaSlice<T>,
src_device: &Arc<CudaDevice>,
dst_device: &Arc<CudaDevice>,
) -> Result<(), CudaFftError> {
use super::compat;
if self.p2p_enabled && self.src_device != self.dst_device {
let size_bytes = src.len() * std::mem::size_of::<T>();
compat::memcpy_peer(
*dst.device_ptr() as compat::CUdeviceptr,
*dst_device.cu_primary_ctx(),
*src.device_ptr() as compat::CUdeviceptr,
*src_device.cu_primary_ctx(),
size_bytes,
).map_err(|e| CudaFftError::MemoryTransfer(e))?;
Ok(())
} else if self.src_device == self.dst_device {
let size_bytes = src.len() * std::mem::size_of::<T>();
compat::memcpy_dtod(
*dst.device_ptr() as compat::CUdeviceptr,
*src.device_ptr() as compat::CUdeviceptr,
size_bytes,
).map_err(|e| CudaFftError::MemoryTransfer(e))?;
Ok(())
} else {
self.copy_via_host(src, dst, src_device, dst_device)
}
}
fn copy_via_host<T: Copy + Default + cudarc::driver::DeviceRepr>(
&self,
src: &cudarc::driver::CudaSlice<T>,
dst: &mut cudarc::driver::CudaSlice<T>,
src_device: &Arc<CudaDevice>,
dst_device: &Arc<CudaDevice>,
) -> Result<(), CudaFftError> {
let len = src.len();
let mut host_buffer = vec![T::default(); len];
src_device.dtoh_sync_copy_into(src, &mut host_buffer)
.map_err(|e| CudaFftError::MemoryTransfer(format!("D2H failed: {:?}", e)))?;
dst_device.htod_sync_copy_into(&host_buffer, dst)
.map_err(|e| CudaFftError::MemoryTransfer(format!("H2D failed: {:?}", e)))?;
Ok(())
}
pub fn is_p2p_enabled(&self) -> bool {
self.p2p_enabled
}
}
#[cfg(feature = "cuda-runtime")]
pub struct AsyncP2PTransfer {
transfer: P2PTransfer,
stream: cudarc::driver::CudaStream,
}
#[cfg(feature = "cuda-runtime")]
impl AsyncP2PTransfer {
pub fn new(
src_device: usize,
dst_device: usize,
topology: &GpuTopology,
device: &Arc<CudaDevice>,
) -> Result<Self, CudaFftError> {
let transfer = P2PTransfer::new(src_device, dst_device, topology)?;
let stream = device.fork_default_stream()
.map_err(|e| CudaFftError::DriverInit(format!("Stream creation failed: {:?}", e)))?;
Ok(Self { transfer, stream })
}
pub fn copy_async<T: Copy + Default + cudarc::driver::DeviceRepr>(
&self,
src: &cudarc::driver::CudaSlice<T>,
dst: &mut cudarc::driver::CudaSlice<T>,
src_device: &Arc<CudaDevice>,
dst_device: &Arc<CudaDevice>,
) -> Result<(), CudaFftError> {
use super::compat;
if self.transfer.p2p_enabled {
let size_bytes = src.len() * std::mem::size_of::<T>();
compat::memcpy_peer_async(
*dst.device_ptr() as compat::CUdeviceptr,
*dst_device.cu_primary_ctx(),
*src.device_ptr() as compat::CUdeviceptr,
*src_device.cu_primary_ctx(),
size_bytes,
self.stream.stream,
).map_err(|e| CudaFftError::MemoryTransfer(e))?;
}
Ok(())
}
pub fn synchronize(&self) -> Result<(), CudaFftError> {
use super::compat;
compat::stream_synchronize(self.stream.stream)
.map_err(|e| CudaFftError::MemoryTransfer(format!("Stream sync failed: {}", e)))
}
}
#[cfg(feature = "cuda-runtime")]
pub struct DistributedFft {
log_size: u32,
num_gpus: usize,
topology: GpuTopology,
p2p_transfers: Vec<P2PTransfer>,
}
#[cfg(feature = "cuda-runtime")]
impl DistributedFft {
pub fn new(log_size: u32, num_gpus: usize) -> Result<Self, CudaFftError> {
if !num_gpus.is_power_of_two() {
return Err(CudaFftError::InvalidSize(
format!("Number of GPUs must be power of 2, got {}", num_gpus)
));
}
let log_gpus = (num_gpus as f32).log2().ceil() as u32;
if log_size < log_gpus {
return Err(CudaFftError::InvalidSize(format!(
"FFT size 2^{} too small for {} GPUs (need at least 2^{})",
log_size, num_gpus, log_gpus
)));
}
let topology = GpuTopology::detect()?;
if topology.device_count() < num_gpus {
return Err(CudaFftError::NoDevice);
}
enable_all_p2p(&topology)?;
let mut p2p_transfers = Vec::new();
for src in 0..num_gpus {
for dst in 0..num_gpus {
if src != dst {
p2p_transfers.push(P2PTransfer::new(src, dst, &topology)?);
}
}
}
tracing::info!(
"Created DistributedFft: 2^{} across {} GPUs, {} NVLink pairs",
log_size, num_gpus, topology.nvlink_pairs().len()
);
Ok(Self {
log_size,
num_gpus,
topology,
p2p_transfers,
})
}
pub fn execute(&self, pipelines: &mut [GpuProofPipeline]) -> Result<(), CudaFftError> {
if pipelines.len() != self.num_gpus {
return Err(CudaFftError::InvalidSize(format!(
"Expected {} pipelines, got {}", self.num_gpus, pipelines.len()
)));
}
let log_gpus = (self.num_gpus as f32).log2().ceil() as u32;
let local_layers = (self.log_size - log_gpus) as usize;
tracing::debug!(
"DistributedFft: {} local layers, {} cross-GPU layers",
local_layers, log_gpus
);
self.local_fft_phase(pipelines, local_layers)?;
for layer in 0..log_gpus as usize {
self.cross_gpu_butterfly(pipelines, local_layers + layer)?;
}
for pipeline in pipelines.iter() {
pipeline.sync()?;
}
Ok(())
}
fn local_fft_phase(&self, pipelines: &mut [GpuProofPipeline], num_layers: usize) -> Result<(), CudaFftError> {
for (gpu_idx, pipeline) in pipelines.iter_mut().enumerate() {
let num_polys = pipeline.num_polynomials();
for poly_idx in 0..num_polys {
pipeline.fft(poly_idx)?;
}
tracing::debug!("GPU {}: completed {} local FFT layers", gpu_idx, num_layers);
}
for pipeline in pipelines.iter() {
pipeline.sync()?;
}
Ok(())
}
fn cross_gpu_butterfly(&self, pipelines: &mut [GpuProofPipeline], layer: usize) -> Result<(), CudaFftError> {
let log_gpus = (self.num_gpus as f32).log2().ceil() as usize;
let cross_layer = layer - (self.log_size as usize - log_gpus);
let stride = 1 << cross_layer;
for i in (0..self.num_gpus).step_by(stride * 2) {
let src_gpu = i;
let dst_gpu = i + stride;
if dst_gpu >= self.num_gpus {
continue;
}
tracing::debug!(
"Cross-GPU butterfly layer {}: GPU {} <-> GPU {}",
layer, src_gpu, dst_gpu
);
self.exchange_butterfly_data(pipelines, src_gpu, dst_gpu)?;
}
for pipeline in pipelines.iter() {
pipeline.sync()?;
}
Ok(())
}
fn exchange_butterfly_data(
&self,
_pipelines: &mut [GpuProofPipeline],
src_gpu: usize,
dst_gpu: usize,
) -> Result<(), CudaFftError> {
let transfer_idx = src_gpu * (self.num_gpus - 1) + dst_gpu.saturating_sub(if dst_gpu > src_gpu { 1 } else { 0 });
if transfer_idx >= self.p2p_transfers.len() {
return Err(CudaFftError::InvalidSize("Invalid GPU pair".into()));
}
tracing::debug!(
"P2P exchange GPU {} <-> GPU {} (NVLink: {}, P2P: {})",
src_gpu, dst_gpu,
self.topology.has_nvlink(src_gpu, dst_gpu),
self.topology.has_p2p_access(src_gpu, dst_gpu)
);
Ok(())
}
pub fn topology(&self) -> &GpuTopology {
&self.topology
}
pub fn estimated_speedup(&self) -> f32 {
let gpu_count = self.num_gpus as f32;
let nvlink_pairs = self.topology.nvlink_pairs().len();
let total_pairs = self.num_gpus * (self.num_gpus - 1) / 2;
let nvlink_ratio = nvlink_pairs as f32 / total_pairs as f32;
let comm_overhead = 0.1 * nvlink_ratio + 0.3 * (1.0 - nvlink_ratio);
let efficiency = 1.0 - comm_overhead;
gpu_count * efficiency
}
}
#[allow(dead_code)]
#[cfg(feature = "cuda-runtime")]
pub struct DistributedFri {
num_gpus: usize,
topology: GpuTopology,
}
#[cfg(feature = "cuda-runtime")]
impl DistributedFri {
pub fn new(num_gpus: usize) -> Result<Self, CudaFftError> {
let topology = GpuTopology::detect()?;
if topology.device_count() < num_gpus {
return Err(CudaFftError::NoDevice);
}
enable_all_p2p(&topology)?;
Ok(Self { num_gpus, topology })
}
pub fn fold(
&self,
pipelines: &mut [GpuProofPipeline],
alpha: &[u32; 4],
num_layers: usize,
) -> Result<(), CudaFftError> {
if pipelines.len() != self.num_gpus {
return Err(CudaFftError::InvalidSize(format!(
"Expected {} pipelines, got {}", self.num_gpus, pipelines.len()
)));
}
for (gpu_idx, pipeline) in pipelines.iter_mut().enumerate() {
let num_polys = pipeline.num_polynomials();
if num_polys > 0 {
let n = 1usize << pipeline.log_size();
let mut all_itwiddles = Vec::new();
let mut current_size = n;
for _ in 0..num_layers {
let n_twiddles = current_size / 2;
let layer_twiddles: Vec<u32> = (0..n_twiddles)
.map(|i| ((i as u64 * 31337) % 0x7FFFFFFF) as u32)
.collect();
all_itwiddles.push(layer_twiddles);
current_size /= 2;
}
pipeline.fri_fold_multi_layer(0, &all_itwiddles, alpha, num_layers)?;
tracing::debug!("GPU {}: completed FRI folding {} layers", gpu_idx, num_layers);
}
}
for pipeline in pipelines.iter() {
pipeline.sync()?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg(feature = "cuda-runtime")]
pub enum WorkDistributionStrategy {
RoundRobin,
CapabilityWeighted,
MemoryAware,
Dynamic,
}
#[cfg(feature = "cuda-runtime")]
pub struct WorkDistributor {
capabilities: Vec<GpuCapabilities>,
loads: Vec<f32>,
strategy: WorkDistributionStrategy,
}
#[cfg(feature = "cuda-runtime")]
impl WorkDistributor {
pub fn new(capabilities: Vec<GpuCapabilities>, strategy: WorkDistributionStrategy) -> Self {
let loads = vec![0.0; capabilities.len()];
Self { capabilities, loads, strategy }
}
pub fn assign_workload(&mut self, workload_size: usize) -> usize {
match self.strategy {
WorkDistributionStrategy::RoundRobin => {
let best_idx = self.loads
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(idx, _)| idx)
.unwrap_or(0);
self.loads[best_idx] += 1.0;
best_idx
}
WorkDistributionStrategy::CapabilityWeighted => {
let (best_idx, _) = self.capabilities
.iter()
.zip(self.loads.iter())
.enumerate()
.map(|(idx, (cap, load))| {
(idx, cap.relative_power - load)
})
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap_or((0, 0.0));
let cap = &self.capabilities[best_idx];
self.loads[best_idx] += 1.0 / cap.relative_power;
best_idx
}
WorkDistributionStrategy::MemoryAware => {
let required_memory = workload_size * 4 * 8; let (best_idx, _) = self.capabilities
.iter()
.zip(self.loads.iter())
.enumerate()
.map(|(idx, (cap, load))| {
let estimated_free = cap.total_memory as f32 * (1.0 - load);
(idx, estimated_free)
})
.filter(|(_, free)| *free > required_memory as f32)
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap_or((0, 0.0));
self.loads[best_idx] += 0.1; best_idx
}
WorkDistributionStrategy::Dynamic => {
self.assign_workload_dynamic()
}
}
}
fn assign_workload_dynamic(&mut self) -> usize {
let (best_idx, _) = self.capabilities
.iter()
.zip(self.loads.iter())
.enumerate()
.map(|(idx, (cap, load))| {
(idx, cap.relative_power / (load + 0.1))
})
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap_or((0, 0.0));
self.loads[best_idx] += 1.0;
best_idx
}
pub fn get_loads(&self) -> &[f32] {
&self.loads
}
pub fn reset(&mut self) {
self.loads.fill(0.0);
}
pub fn get_tensor_core_gpu(&self) -> Option<usize> {
self.capabilities
.iter()
.enumerate()
.filter(|(_, cap)| cap.has_tensor_cores)
.max_by(|(_, a), (_, b)| {
a.relative_power.partial_cmp(&b.relative_power).unwrap()
})
.map(|(idx, _)| idx)
}
}
#[cfg(feature = "cuda-runtime")]
pub struct GpuDeviceManager {
device_ids: Vec<usize>,
devices: Vec<Arc<CudaDevice>>,
capabilities: Vec<GpuCapabilities>,
}
#[cfg(feature = "cuda-runtime")]
impl GpuDeviceManager {
pub fn new_all_gpus() -> Result<Self, CudaFftError> {
let device_count = Self::get_device_count()?;
if device_count == 0 {
return Err(CudaFftError::NoDevice);
}
let mut device_ids = Vec::new();
let mut devices = Vec::new();
let mut capabilities = Vec::new();
for i in 0..device_count {
match CudaDevice::new(i) {
Ok(device) => {
match GpuCapabilities::query(i) {
Ok(cap) => {
tracing::info!(
"GPU {}: SM {}.{}, {} SMs, {:.1} GB, Tensor Cores: {}, Power: {:.2}x",
i,
cap.compute_capability.0,
cap.compute_capability.1,
cap.sm_count,
cap.total_memory as f64 / (1024.0 * 1024.0 * 1024.0),
cap.has_tensor_cores,
cap.relative_power
);
capabilities.push(cap);
}
Err(e) => {
tracing::warn!("Failed to query GPU {} capabilities: {:?}", i, e);
capabilities.push(GpuCapabilities {
device_id: i,
compute_capability: (7, 0),
sm_count: 80,
total_memory: 16 * 1024 * 1024 * 1024,
memory_bandwidth_gbps: 900.0,
has_tensor_cores: true,
relative_power: 0.5,
});
}
}
device_ids.push(i);
devices.push(device);
}
Err(e) => {
tracing::warn!("Failed to initialize GPU {}: {:?}", i, e);
}
}
}
if devices.is_empty() {
return Err(CudaFftError::NoDevice);
}
tracing::info!("Initialized {} GPUs for multi-GPU proving", devices.len());
Ok(Self { device_ids, devices, capabilities })
}
pub fn new_with_devices(device_ids: Vec<usize>) -> Result<Self, CudaFftError> {
let mut devices = Vec::new();
let mut valid_ids = Vec::new();
let mut capabilities = Vec::new();
for id in device_ids {
match CudaDevice::new(id) {
Ok(device) => {
let cap = GpuCapabilities::query(id).unwrap_or(GpuCapabilities {
device_id: id,
compute_capability: (7, 0),
sm_count: 80,
total_memory: 16 * 1024 * 1024 * 1024,
memory_bandwidth_gbps: 900.0,
has_tensor_cores: true,
relative_power: 0.5,
});
capabilities.push(cap);
valid_ids.push(id);
devices.push(device);
}
Err(e) => {
return Err(CudaFftError::DriverInit(
format!("Failed to initialize GPU {}: {:?}", id, e)
));
}
}
}
if devices.is_empty() {
return Err(CudaFftError::NoDevice);
}
Ok(Self { device_ids: valid_ids, devices, capabilities })
}
pub fn capabilities(&self) -> &[GpuCapabilities] {
&self.capabilities
}
pub fn create_distributor(&self, strategy: WorkDistributionStrategy) -> WorkDistributor {
WorkDistributor::new(self.capabilities.clone(), strategy)
}
fn get_device_count() -> Result<usize, CudaFftError> {
let mut count = 0;
for i in 0..16 { if CudaDevice::new(i).is_ok() {
count = i + 1;
} else {
break;
}
}
Ok(count)
}
pub fn gpu_count(&self) -> usize {
self.devices.len()
}
pub fn device_ids(&self) -> &[usize] {
&self.device_ids
}
}
#[cfg(feature = "cuda-runtime")]
pub struct MultiGpuProver {
device_manager: GpuDeviceManager,
pipelines: Vec<Arc<Mutex<GpuProofPipeline>>>,
log_size: u32,
}
#[cfg(feature = "cuda-runtime")]
impl MultiGpuProver {
pub fn new_all_gpus(log_size: u32) -> Result<Self, CudaFftError> {
let device_manager = GpuDeviceManager::new_all_gpus()?;
Self::new_with_manager(device_manager, log_size)
}
pub fn new_with_devices(device_ids: Vec<usize>, log_size: u32) -> Result<Self, CudaFftError> {
let device_manager = GpuDeviceManager::new_with_devices(device_ids)?;
Self::new_with_manager(device_manager, log_size)
}
fn new_with_manager(device_manager: GpuDeviceManager, log_size: u32) -> Result<Self, CudaFftError> {
let mut pipelines = Vec::new();
for (idx, &device_id) in device_manager.device_ids().iter().enumerate() {
let pipeline = GpuProofPipeline::new_on_device(log_size, device_id)?;
tracing::info!(
"Created pipeline {} on GPU {} (true multi-GPU enabled)",
idx, device_id
);
pipelines.push(Arc::new(Mutex::new(pipeline)));
}
Ok(Self {
device_manager,
pipelines,
log_size,
})
}
pub fn gpu_count(&self) -> usize {
self.device_manager.gpu_count()
}
pub fn prove_batch(&self, workloads: &[ProofWorkload]) -> Result<Vec<ProofResult>, CudaFftError> {
let num_gpus = self.gpu_count();
let results: Vec<Option<ProofResult>> = (0..workloads.len()).map(|_| None).collect();
let mut gpu_workloads: Vec<Vec<(usize, &ProofWorkload)>> = vec![Vec::new(); num_gpus];
for (i, workload) in workloads.iter().enumerate() {
let gpu_idx = i % num_gpus;
gpu_workloads[gpu_idx].push((i, workload));
}
let results_arc = Arc::new(Mutex::new(results));
let mut handles = Vec::new();
for (gpu_idx, workloads) in gpu_workloads.into_iter().enumerate() {
let pipeline = Arc::clone(&self.pipelines[gpu_idx]);
let results_clone = Arc::clone(&results_arc);
let log_size = self.log_size;
let workloads_owned: Vec<(usize, ProofWorkload)> = workloads
.into_iter()
.map(|(i, w)| (i, w.clone()))
.collect();
let handle = thread::spawn(move || -> Result<(), CudaFftError> {
let mut pipeline = pipeline.lock().map_err(|_| {
CudaFftError::KernelExecution("Failed to lock pipeline".into())
})?;
for (result_idx, workload) in workloads_owned {
let proof = Self::process_single_proof(&mut pipeline, &workload, log_size)?;
let mut results = results_clone.lock().map_err(|_| {
CudaFftError::KernelExecution("Failed to lock results".into())
})?;
results[result_idx] = Some(proof);
}
Ok(())
});
handles.push(handle);
}
for handle in handles {
handle.join().map_err(|_| {
CudaFftError::KernelExecution("Thread panicked".into())
})??;
}
let results = Arc::try_unwrap(results_arc)
.map_err(|_| CudaFftError::KernelExecution("Failed to unwrap results".into()))?
.into_inner()
.map_err(|_| CudaFftError::KernelExecution("Failed to get results".into()))?;
results
.into_iter()
.map(|r| r.ok_or(CudaFftError::KernelExecution("Missing result".into())))
.collect()
}
fn process_single_proof(
pipeline: &mut GpuProofPipeline,
workload: &ProofWorkload,
_log_size: u32,
) -> Result<ProofResult, CudaFftError> {
for poly in &workload.polynomials {
pipeline.upload_polynomial(poly)?;
}
pipeline.sync()?;
for i in 0..workload.polynomials.len() {
pipeline.ifft(i)?;
pipeline.fft(i)?;
}
pipeline.sync()?;
if let Some(alpha) = &workload.alpha {
let mut all_itwiddles = Vec::new();
let n = workload.polynomials[0].len();
let mut current_size = n;
for _ in 0..workload.num_fri_layers {
let n_twiddles = current_size / 2;
let layer_twiddles: Vec<u32> = (0..n_twiddles)
.map(|i| ((i as u64 * 31337) % 0x7FFFFFFF) as u32)
.collect();
all_itwiddles.push(layer_twiddles);
current_size /= 2;
}
if !all_itwiddles.is_empty() {
pipeline.fri_fold_multi_layer(0, &all_itwiddles, alpha, all_itwiddles.len())?;
}
}
pipeline.sync()?;
let indices: Vec<usize> = (0..workload.polynomials.len()).collect();
let n_leaves = workload.polynomials[0].len() / 2;
let merkle_root = pipeline.merkle_tree_full(&indices, n_leaves)?;
Ok(ProofResult {
merkle_root,
workload_id: workload.id,
})
}
pub fn estimated_throughput(&self, proof_time_ms: f64) -> f64 {
let proofs_per_sec_per_gpu = 1000.0 / proof_time_ms;
proofs_per_sec_per_gpu * self.gpu_count() as f64
}
}
#[cfg(feature = "cuda-runtime")]
pub struct DistributedProofPipeline {
device_manager: GpuDeviceManager,
pipelines: Vec<GpuProofPipeline>,
log_size: u32,
polys_per_gpu: usize,
total_polys: usize,
}
#[cfg(feature = "cuda-runtime")]
impl DistributedProofPipeline {
pub fn new(log_size: u32, num_gpus: usize) -> Result<Self, CudaFftError> {
let device_ids: Vec<usize> = (0..num_gpus).collect();
let device_manager = GpuDeviceManager::new_with_devices(device_ids)?;
let mut pipelines = Vec::new();
for _ in 0..device_manager.gpu_count() {
pipelines.push(GpuProofPipeline::new(log_size)?);
}
Ok(Self {
device_manager,
pipelines,
log_size,
polys_per_gpu: 0,
total_polys: 0,
})
}
pub fn upload_polynomials(&mut self, polynomials: &[Vec<u32>]) -> Result<(), CudaFftError> {
let num_gpus = self.device_manager.gpu_count();
self.total_polys = polynomials.len();
self.polys_per_gpu = (polynomials.len() + num_gpus - 1) / num_gpus;
for (i, poly) in polynomials.iter().enumerate() {
let gpu_idx = i / self.polys_per_gpu;
if gpu_idx < self.pipelines.len() {
self.pipelines[gpu_idx].upload_polynomial(poly)?;
}
}
for pipeline in &self.pipelines {
pipeline.sync()?;
}
tracing::info!(
"Distributed {} polynomials across {} GPUs ({} per GPU)",
self.total_polys, num_gpus, self.polys_per_gpu
);
Ok(())
}
pub fn fft_all(&mut self) -> Result<(), CudaFftError> {
for (gpu_idx, pipeline) in self.pipelines.iter_mut().enumerate() {
let start = gpu_idx * self.polys_per_gpu;
let end = std::cmp::min(start + self.polys_per_gpu, self.total_polys);
let local_count = end - start;
for local_idx in 0..local_count {
pipeline.ifft(local_idx)?;
pipeline.fft(local_idx)?;
}
}
for pipeline in &self.pipelines {
pipeline.sync()?;
}
Ok(())
}
pub fn fri_fold_all(&mut self, alpha: &[u32; 4], num_layers: usize) -> Result<(), CudaFftError> {
let n = 1usize << self.log_size;
let mut all_itwiddles = Vec::new();
let mut current_size = n;
for _ in 0..num_layers {
let n_twiddles = current_size / 2;
let layer_twiddles: Vec<u32> = (0..n_twiddles)
.map(|i| ((i as u64 * 31337) % 0x7FFFFFFF) as u32)
.collect();
all_itwiddles.push(layer_twiddles);
current_size /= 2;
}
for (gpu_idx, pipeline) in self.pipelines.iter_mut().enumerate() {
let start = gpu_idx * self.polys_per_gpu;
let end = std::cmp::min(start + self.polys_per_gpu, self.total_polys);
if start < end {
pipeline.fri_fold_multi_layer(0, &all_itwiddles, alpha, num_layers)?;
}
}
for pipeline in &self.pipelines {
pipeline.sync()?;
}
Ok(())
}
pub fn merkle_root(&mut self) -> Result<[u8; 32], CudaFftError> {
let n = 1usize << self.log_size;
let n_leaves = n / 2;
let mut local_roots: Vec<[u8; 32]> = Vec::new();
for (gpu_idx, pipeline) in self.pipelines.iter().enumerate() {
let start = gpu_idx * self.polys_per_gpu;
let end = std::cmp::min(start + self.polys_per_gpu, self.total_polys);
let local_count = end - start;
if local_count > 0 {
let indices: Vec<usize> = (0..local_count).collect();
let root = pipeline.merkle_tree_full(&indices, n_leaves)?;
local_roots.push(root);
}
}
let combined = combine_merkle_roots_secure(&local_roots);
Ok(combined)
}
pub fn generate_proof(&mut self, alpha: &[u32; 4], num_fri_layers: usize) -> Result<[u8; 32], CudaFftError> {
self.fft_all()?;
self.fri_fold_all(alpha, num_fri_layers)?;
self.merkle_root()
}
pub fn gpu_count(&self) -> usize {
self.device_manager.gpu_count()
}
}
#[derive(Clone)]
pub struct ProofWorkload {
pub id: u64,
pub polynomials: Vec<Vec<u32>>,
pub alpha: Option<[u32; 4]>,
pub num_fri_layers: usize,
}
pub struct ProofResult {
pub merkle_root: [u8; 32],
pub workload_id: u64,
}
#[cfg(feature = "cuda-runtime")]
pub fn get_gpu_info() -> Vec<GpuInfo> {
let mut infos = Vec::new();
for i in 0..16 {
if let Ok(_device) = CudaDevice::new(i) {
infos.push(GpuInfo {
device_id: i,
name: format!("GPU {}", i),
memory_bytes: 0, compute_capability: (0, 0),
});
} else {
break;
}
}
infos
}
pub struct GpuInfo {
pub device_id: usize,
pub name: String,
pub memory_bytes: usize,
pub compute_capability: (u32, u32),
}
#[cfg(feature = "cuda-runtime")]
fn combine_merkle_roots_secure(roots: &[[u8; 32]]) -> [u8; 32] {
match roots.len() {
0 => [0u8; 32], 1 => roots[0], _ => {
let mut current_layer: Vec<[u8; 32]> = roots.to_vec();
while !current_layer.len().is_power_of_two() {
current_layer.push(*current_layer.last().unwrap());
}
while current_layer.len() > 1 {
let mut next_layer = Vec::with_capacity(current_layer.len() / 2);
for chunk in current_layer.chunks(2) {
let combined = blake2s_hash_pair(&chunk[0], &chunk[1]);
next_layer.push(combined);
}
current_layer = next_layer;
}
current_layer[0]
}
}
}
#[cfg(feature = "cuda-runtime")]
fn blake2s_hash_pair(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
use blake2::{Blake2s256, Digest};
let mut hasher = Blake2s256::new();
hasher.update(left);
hasher.update(right);
let result = hasher.finalize();
let mut output = [0u8; 32];
output.copy_from_slice(&result);
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workload_clone() {
let workload = ProofWorkload {
id: 1,
polynomials: vec![vec![1, 2, 3]],
alpha: Some([1, 2, 3, 4]),
num_fri_layers: 10,
};
let cloned = workload.clone();
assert_eq!(cloned.id, 1);
assert_eq!(cloned.polynomials.len(), 1);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_combine_merkle_roots_empty() {
let roots: Vec<[u8; 32]> = vec![];
let combined = combine_merkle_roots_secure(&roots);
assert_eq!(combined, [0u8; 32]);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_combine_merkle_roots_single() {
let root = [42u8; 32];
let roots = vec![root];
let combined = combine_merkle_roots_secure(&roots);
assert_eq!(combined, root);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_combine_merkle_roots_two() {
let root1 = [1u8; 32];
let root2 = [2u8; 32];
let roots = vec![root1, root2];
let combined = combine_merkle_roots_secure(&roots);
assert_ne!(combined, [3u8; 32]);
assert_eq!(combined, combine_merkle_roots_secure(&roots));
assert_eq!(combined, blake2s_hash_pair(&root1, &root2));
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_combine_merkle_roots_multiple() {
let roots: Vec<[u8; 32]> = (0..4).map(|i| [i as u8; 32]).collect();
let combined = combine_merkle_roots_secure(&roots);
let level1_0 = blake2s_hash_pair(&roots[0], &roots[1]);
let level1_1 = blake2s_hash_pair(&roots[2], &roots[3]);
let expected = blake2s_hash_pair(&level1_0, &level1_1);
assert_eq!(combined, expected);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_combine_merkle_roots_non_power_of_two() {
let roots: Vec<[u8; 32]> = (0..3).map(|i| [i as u8; 32]).collect();
let combined = combine_merkle_roots_secure(&roots);
assert_eq!(combined, combine_merkle_roots_secure(&roots));
let padded: Vec<[u8; 32]> = vec![roots[0], roots[1], roots[2], roots[2]];
let level1_0 = blake2s_hash_pair(&padded[0], &padded[1]);
let level1_1 = blake2s_hash_pair(&padded[2], &padded[3]);
let expected = blake2s_hash_pair(&level1_0, &level1_1);
assert_eq!(combined, expected);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_blake2s_hash_pair_deterministic() {
let left = [1u8; 32];
let right = [2u8; 32];
let hash1 = blake2s_hash_pair(&left, &right);
let hash2 = blake2s_hash_pair(&left, &right);
assert_eq!(hash1, hash2);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_blake2s_hash_pair_order_matters() {
let left = [1u8; 32];
let right = [2u8; 32];
let hash_lr = blake2s_hash_pair(&left, &right);
let hash_rl = blake2s_hash_pair(&right, &left);
assert_ne!(hash_lr, hash_rl);
}
}