Files
rustytorch/demos/rtx-slidescope/src/gpu/mod.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

427 lines
12 KiB
Rust

//! GPU abstraction layer for NMF computations
//!
//! Provides a unified interface for GPU-accelerated matrix operations
//! using the rtx-tensor backend for both CUDA (NVIDIA) and Metal (Apple).
//!
//! This module uses rtx-tensor's unified device abstraction, ensuring
//! consistency with other RustyTorch++ demos (hemodynamics, MRE, bioheat).
#[cfg(feature = "cuda")]
pub mod cuda;
#[cfg(feature = "metal")]
pub mod metal;
use anyhow::Result;
use ndarray::Array2;
/// GPU backend trait for NMF matrix operations
///
/// Implementations must provide efficient GPU-accelerated versions
/// of the core NMF operations: matrix multiplication, element-wise
/// operations, and transpose.
pub trait GpuBackend: Send + Sync {
/// Get the name of this GPU backend
fn name(&self) -> &'static str;
/// Get device information
fn device_info(&self) -> DeviceInfo;
/// Check if the backend is available and functional
fn is_available(&self) -> bool;
/// Allocate a matrix on the GPU
fn allocate(&self, rows: usize, cols: usize) -> Result<GpuMatrix>;
/// Upload a CPU matrix to the GPU
fn upload(&self, data: &Array2<f32>) -> Result<GpuMatrix>;
/// Download a GPU matrix to the CPU
fn download(&self, matrix: &GpuMatrix) -> Result<Array2<f32>>;
/// Matrix multiplication: C = A @ B
fn matmul(&self, a: &GpuMatrix, b: &GpuMatrix) -> Result<GpuMatrix>;
/// Matrix transpose
fn transpose(&self, a: &GpuMatrix) -> Result<GpuMatrix>;
/// Element-wise multiplication: C = A .* B
fn element_mul(&self, a: &GpuMatrix, b: &GpuMatrix) -> Result<GpuMatrix>;
/// Element-wise division with epsilon: C = A ./ (B + eps)
fn element_div_eps(&self, a: &GpuMatrix, b: &GpuMatrix, epsilon: f32) -> Result<GpuMatrix>;
/// In-place element-wise update: A = A .* B ./ (C + eps)
fn nmf_update_inplace(
&self,
target: &mut GpuMatrix,
numerator: &GpuMatrix,
denominator: &GpuMatrix,
epsilon: f32,
) -> Result<()>;
/// Compute Frobenius norm of (A - B)
fn frobenius_diff(&self, a: &GpuMatrix, b: &GpuMatrix) -> Result<f32>;
/// Synchronize GPU operations (wait for completion)
fn synchronize(&self) -> Result<()>;
}
/// GPU matrix handle - opaque reference to GPU-allocated memory
#[derive(Debug)]
pub struct GpuMatrix {
/// Unique identifier for this matrix
pub id: u64,
/// Number of rows
pub rows: usize,
/// Number of columns
pub cols: usize,
/// Backend-specific handle (raw pointer or index)
#[allow(dead_code)]
pub(crate) handle: GpuHandle,
}
impl GpuMatrix {
/// Create a new GPU matrix reference
pub fn new(id: u64, rows: usize, cols: usize, handle: GpuHandle) -> Self {
Self {
id,
rows,
cols,
handle,
}
}
/// Get the shape as (rows, cols)
pub fn shape(&self) -> (usize, usize) {
(self.rows, self.cols)
}
/// Get total number of elements
pub fn len(&self) -> usize {
self.rows * self.cols
}
/// Check if matrix is empty
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
/// Backend-specific handle for GPU memory
#[derive(Debug)]
pub enum GpuHandle {
/// Raw device pointer (CUDA)
DevicePtr(u64),
/// Buffer index (Metal/wgpu)
BufferIndex(u32),
/// Null/invalid handle
None,
}
/// Information about a GPU device
#[derive(Debug, Clone)]
pub struct DeviceInfo {
/// Device name
pub name: String,
/// Total memory in bytes
pub total_memory: u64,
/// Available memory in bytes (if known)
pub available_memory: Option<u64>,
/// Compute capability or feature level
pub compute_capability: String,
/// Backend type
pub backend: BackendType,
}
/// Type of GPU backend
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendType {
/// NVIDIA CUDA
Cuda,
/// Apple Metal
Metal,
/// CPU fallback
Cpu,
}
impl std::fmt::Display for BackendType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BackendType::Cuda => write!(f, "CUDA"),
BackendType::Metal => write!(f, "Metal"),
BackendType::Cpu => write!(f, "CPU"),
}
}
}
/// Select the best available GPU backend
///
/// Uses rtx-tensor's unified device detection to find available GPUs.
/// Priority: CUDA > Metal > CPU
pub fn select_backend() -> Result<Box<dyn GpuBackend>> {
use rtx_tensor::Device;
let devices = Device::available_devices();
// Try CUDA first (highest priority for compute)
#[cfg(feature = "cuda")]
{
if devices.iter().any(|d| d.is_cuda()) {
if let Ok(backend) = cuda::CudaBackend::new() {
if backend.is_available() {
tracing::info!("Using CUDA GPU backend: {}", backend.device_info().name);
return Ok(Box::new(backend));
}
}
}
}
// Try Metal on macOS
#[cfg(feature = "metal")]
{
if devices.iter().any(|d| d.is_metal())
&& let Ok(backend) = metal::MetalBackend::new()
&& backend.is_available()
{
tracing::info!(
"Using Metal GPU backend (rtx-tensor): {}",
backend.device_info().name
);
return Ok(Box::new(backend));
}
}
// Return CPU fallback
tracing::info!("Using CPU fallback backend");
Ok(Box::new(CpuFallbackBackend::new()))
}
/// Check if any GPU backend is available
///
/// Uses rtx-tensor's device detection for consistency with other demos.
pub fn gpu_available() -> bool {
use rtx_tensor::Device;
let devices = Device::available_devices();
#[cfg(feature = "cuda")]
{
if devices.iter().any(|d| d.is_cuda()) {
return true;
}
}
#[cfg(feature = "metal")]
{
if devices.iter().any(|d| d.is_metal()) {
return true;
}
}
false
}
/// CPU fallback backend for systems without GPU
pub struct CpuFallbackBackend {
next_id: std::sync::atomic::AtomicU64,
matrices: std::sync::RwLock<std::collections::HashMap<u64, Array2<f32>>>,
}
impl CpuFallbackBackend {
/// Create a new CPU fallback backend
pub fn new() -> Self {
Self {
next_id: std::sync::atomic::AtomicU64::new(1),
matrices: std::sync::RwLock::new(std::collections::HashMap::new()),
}
}
fn next_id(&self) -> u64 {
self.next_id
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
}
fn get_matrix(&self, id: u64) -> Result<Array2<f32>> {
let matrices = self.matrices.read().unwrap();
matrices
.get(&id)
.cloned()
.ok_or_else(|| anyhow::anyhow!("Matrix {} not found", id))
}
fn store_matrix(&self, data: Array2<f32>) -> GpuMatrix {
let id = self.next_id();
let (rows, cols) = data.dim();
self.matrices.write().unwrap().insert(id, data);
GpuMatrix::new(id, rows, cols, GpuHandle::BufferIndex(id as u32))
}
}
impl Default for CpuFallbackBackend {
fn default() -> Self {
Self::new()
}
}
impl GpuBackend for CpuFallbackBackend {
fn name(&self) -> &'static str {
"CPU Fallback"
}
fn device_info(&self) -> DeviceInfo {
DeviceInfo {
name: "CPU".to_string(),
total_memory: 0,
available_memory: None,
compute_capability: "N/A".to_string(),
backend: BackendType::Cpu,
}
}
fn is_available(&self) -> bool {
true
}
fn allocate(&self, rows: usize, cols: usize) -> Result<GpuMatrix> {
let data = Array2::zeros((rows, cols));
Ok(self.store_matrix(data))
}
fn upload(&self, data: &Array2<f32>) -> Result<GpuMatrix> {
Ok(self.store_matrix(data.clone()))
}
fn download(&self, matrix: &GpuMatrix) -> Result<Array2<f32>> {
self.get_matrix(matrix.id)
}
fn matmul(&self, a: &GpuMatrix, b: &GpuMatrix) -> Result<GpuMatrix> {
let a_data = self.get_matrix(a.id)?;
let b_data = self.get_matrix(b.id)?;
let result = a_data.dot(&b_data);
Ok(self.store_matrix(result))
}
fn transpose(&self, a: &GpuMatrix) -> Result<GpuMatrix> {
let data = self.get_matrix(a.id)?;
let result = data.t().to_owned();
Ok(self.store_matrix(result))
}
fn element_mul(&self, a: &GpuMatrix, b: &GpuMatrix) -> Result<GpuMatrix> {
let a_data = self.get_matrix(a.id)?;
let b_data = self.get_matrix(b.id)?;
let result = &a_data * &b_data;
Ok(self.store_matrix(result))
}
fn element_div_eps(&self, a: &GpuMatrix, b: &GpuMatrix, epsilon: f32) -> Result<GpuMatrix> {
let a_data = self.get_matrix(a.id)?;
let b_data = self.get_matrix(b.id)?;
let result = &a_data / &(&b_data + epsilon);
Ok(self.store_matrix(result))
}
fn nmf_update_inplace(
&self,
target: &mut GpuMatrix,
numerator: &GpuMatrix,
denominator: &GpuMatrix,
epsilon: f32,
) -> Result<()> {
let mut target_data = self.get_matrix(target.id)?;
let num_data = self.get_matrix(numerator.id)?;
let denom_data = self.get_matrix(denominator.id)?;
for ((t, n), d) in target_data
.iter_mut()
.zip(num_data.iter())
.zip(denom_data.iter())
{
*t = *t * n / (d + epsilon);
}
self.matrices
.write()
.unwrap()
.insert(target.id, target_data);
Ok(())
}
fn frobenius_diff(&self, a: &GpuMatrix, b: &GpuMatrix) -> Result<f32> {
let a_data = self.get_matrix(a.id)?;
let b_data = self.get_matrix(b.id)?;
let diff = &a_data - &b_data;
let norm = diff.iter().map(|x| x * x).sum::<f32>().sqrt();
Ok(norm)
}
fn synchronize(&self) -> Result<()> {
// No-op for CPU
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cpu_fallback_matmul() {
let backend = CpuFallbackBackend::new();
let a = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
let b = Array2::from_shape_vec((3, 2), vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0]).unwrap();
let gpu_a = backend.upload(&a).unwrap();
let gpu_b = backend.upload(&b).unwrap();
let gpu_c = backend.matmul(&gpu_a, &gpu_b).unwrap();
let c = backend.download(&gpu_c).unwrap();
let expected = a.dot(&b);
assert_eq!(c.shape(), expected.shape());
for (actual, exp) in c.iter().zip(expected.iter()) {
assert!((actual - exp).abs() < 1e-5);
}
}
#[test]
fn test_cpu_fallback_nmf_update() {
let backend = CpuFallbackBackend::new();
let target = Array2::from_elem((3, 3), 1.0_f32);
let numerator = Array2::from_elem((3, 3), 2.0_f32);
let denominator = Array2::from_elem((3, 3), 1.0_f32);
let mut gpu_target = backend.upload(&target).unwrap();
let gpu_num = backend.upload(&numerator).unwrap();
let gpu_denom = backend.upload(&denominator).unwrap();
backend
.nmf_update_inplace(&mut gpu_target, &gpu_num, &gpu_denom, 1e-10)
.unwrap();
let result = backend.download(&gpu_target).unwrap();
// target * numerator / denominator = 1.0 * 2.0 / 1.0 = 2.0
for val in result.iter() {
assert!((val - 2.0).abs() < 1e-5);
}
}
#[test]
fn test_gpu_matrix_shape() {
let matrix = GpuMatrix::new(1, 10, 20, GpuHandle::None);
assert_eq!(matrix.shape(), (10, 20));
assert_eq!(matrix.len(), 200);
assert!(!matrix.is_empty());
}
#[test]
fn test_backend_type_display() {
assert_eq!(format!("{}", BackendType::Cuda), "CUDA");
assert_eq!(format!("{}", BackendType::Metal), "Metal");
assert_eq!(format!("{}", BackendType::Cpu), "CPU");
}
}