Files
rustytorch/crates/core/rtx-backend-cuda/src/lib.rs
T
Omar SobhandClaude Opus 5 796b8487ad
GPU Tests / CUDA Tests (12.1) (push) Skipped
GPU Tests / Metal Tests (push) Skipped
GPU Tests / Check GPU Availability (push) Successful in 0s
GPU Tests / CUDA Tests (11.8) (push) Skipped
Documentation / Build API Documentation (push) Failing after 4s
CI / Format Check (push) Failing after 12s
Documentation / Build User Guide (push) Successful in 20s
CI / Build CPU-Only (Explicit) (push) Failing after 33s
CI / Clippy Check (push) Failing after 44s
CI / Build (ubuntu-latest) (push) Failing after 2m21s
Performance Benchmarks / Run Benchmarks (push) Successful in 3m4s
CI / Build (macos-latest) (push) Failing after 12s
CI / Test (macos-latest) (push) Skipped
CI / Test (ubuntu-latest) (push) Skipped
CI / Python Bindings (maturin) (macos-latest) (push) Skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Skipped
CI / WASM Build + Size Check (push) Skipped
CI / Distributed Training Tests (push) Skipped
CI / CI Success (push) Failing after 0s
rtx-backend-cuda: native index_select / index_add — HetGAT training 2.5x, inference 4.3x
The Backend trait's index_select and index_add have default bodies that
round-trip through host memory. That is correct everywhere and was the only
implementation CUDA had. Graph message passing is made of these two ops, so
dg-gnn's HetGAT paid a device->host->device copy per layer per pass and the
RTX 5060 Ti sat at ~10% utilisation during training.

Design follows rtx-backend-metal's ops::index: gather is one thread per output
element; scatter-add walks the CSR of the adjoint selection matrix S^T, built
host-side by counting sort, so it needs NO atomics and is deterministic with
duplicate indices — the training loss is bit-identical to the host reference.
Device index buffers are cached per thread keyed by the exact index list, so a
static graph topology uploads once. Two small NVRTC kernels; no cuSPARSE.

Measured on dg-gnn, Harris 42,955 links, v8 recipe, RTX 5060 Ti:

  training batch 8      9,042 -> 3,562 ms/step   (2.5x)
  inference single p50   55.4 ->  12.9 ms        (4.3x)
  inference batch 8       257 ->    20 ms/scen   (13x; batching helps again)
  GPU utilisation       median 10% -> 21%, p90 17% -> 43%

Tests: gather with repeats, scatter-add with duplicates and untouched rows,
the adjoint identity <S x, y> == <x, S^T y> (what autograd relies on), a
hub-heavy pattern against the host reference, and the range-check panic.
rtx-backend-cuda --features cuda: 60 + 16 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-11 23:09:01 -05:00

502 lines
15 KiB
Rust

//! # RustyTorch++ CUDA Backend
//!
//! NVIDIA GPU backend implementation using hand-optimized CUDA kernels.
//!
//! ## Features
//!
//! - **cuBLAS Integration**: High-performance matrix operations via cuBLAS
//! - **cuDNN Integration**: Optimized convolutions and normalization
//! - **Flash Attention**: Hand-optimized FlashAttention-3 kernels
//! - **Tensor Core Support**: FP16/BF16/FP8 with Tensor Core acceleration
//!
//! ## Architecture
//!
//! ```text
//! CudaBackend
//! ├── CudaTensorPrimitive - GPU memory handle with cudarc
//! ├── CudaDevice - Device context and stream management
//! └── Ops
//! ├── Basic - Add, mul, etc. (cuBLAS)
//! ├── GEMM - Matrix multiply (cuBLAS LT)
//! └── Attention - Flash Attention (hand-optimized)
//! ```
//!
//! ## Performance Notes
//!
//! - Uses RTX 5090 (sm_90) optimizations by default
//! - Tensor Core scheduling for BF16 and FP8 operations
//! - CUDA Graph capture for reduced kernel launch overhead
//!
//! ## Example
//!
//! ```rust,ignore
//! use rtx_backend_cuda::{CudaBackend, CudaDevice};
//! use rtx_backend::Backend;
//!
//! let device = CudaDevice::new(0)?;
//! let a = CudaBackend::zeros([1024, 1024], &device);
//! let b = CudaBackend::randn([1024, 1024], &device);
//! let c = CudaBackend::matmul(&a, &b);
//! ```
#![warn(missing_docs)]
// Note: When the cuda feature is disabled, stub implementations are provided.
// Enable the cuda feature for actual CUDA functionality.
mod device;
mod error;
mod tensor;
#[cfg(feature = "cuda")]
mod kernels;
#[cfg(feature = "cuda")]
/// Operations module containing GPU-accelerated tensor operations.
pub mod ops;
#[cfg(not(feature = "cuda"))]
/// Operations module (stub - requires cuda feature).
pub mod ops {
//! Stub operations module - requires cuda feature.
/// Tensor creation operations (stub).
pub mod creation {}
/// Basic arithmetic operations (stub).
pub mod basic {}
/// Unary mathematical operations (stub).
pub mod unary {}
/// General matrix multiplication operations (stub).
pub mod gemm {}
/// Reduction operations like sum and mean (stub).
pub mod reduction {}
/// Shape manipulation operations (stub).
pub mod shape {}
/// Activation functions (stub).
pub mod activation {}
/// Normalization operations (stub).
pub mod normalization {}
/// Attention mechanism operations (stub).
pub mod attention {}
/// Device management operations (stub).
pub mod device {}
use parking_lot::Mutex;
static RNG: Mutex<Option<rand::rngs::StdRng>> = Mutex::new(None);
/// Seeds the random number generator with the given seed (stub).
pub fn seed_rng(_seed: u64) {}
pub(crate) fn get_rng() -> rand::rngs::StdRng {
use rand::SeedableRng;
rand::rngs::StdRng::from_entropy()
}
}
/// Windows-specific CUDA support.
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
pub use windows::{WindowsCudaConfig, WindowsGpuInfo};
pub use device::CudaDevice;
pub use error::{CudaError, CudaResult};
pub use tensor::CudaTensorPrimitive;
#[cfg(feature = "cuda")]
use rtx_backend::{Backend, BoolU8, DeviceId, DeviceOps};
use std::fmt::Debug;
/// CUDA backend for RustyTorch++.
///
/// This backend uses NVIDIA GPUs via cudarc and provides:
/// - cuBLAS for matrix operations
/// - cuDNN for convolutions
/// - Hand-optimized Flash Attention kernels
/// - Tensor Core acceleration for FP16/BF16/FP8
#[derive(Clone, Debug, Default)]
pub struct CudaBackend;
#[cfg(feature = "cuda")]
impl Backend for CudaBackend {
type TensorPrimitive<const D: usize> = CudaTensorPrimitive<D>;
type Device = CudaDevice;
type FloatElem = f32; // Default to f32, with FP16/BF16 support via type parameter
type IntElem = i32;
type BoolElem = BoolU8;
fn name() -> &'static str {
"cuda"
}
fn seed(seed: u64) {
ops::seed_rng(seed);
}
// ==================== Tensor Creation ====================
fn zeros<const D: usize>(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive<D> {
ops::creation::zeros(shape, device)
}
fn ones<const D: usize>(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive<D> {
ops::creation::ones(shape, device)
}
fn full<const D: usize>(
shape: [usize; D],
fill_value: Self::FloatElem,
device: &Self::Device,
) -> Self::TensorPrimitive<D> {
ops::creation::full(shape, fill_value, device)
}
fn rand<const D: usize>(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive<D> {
ops::creation::rand(shape, device)
}
fn randn<const D: usize>(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive<D> {
ops::creation::randn(shape, device)
}
fn from_data<const D: usize>(
data: &[Self::FloatElem],
shape: [usize; D],
device: &Self::Device,
) -> Self::TensorPrimitive<D> {
ops::creation::from_data(data, shape, device)
}
// ==================== Basic Operations ====================
fn add<const D: usize>(
lhs: Self::TensorPrimitive<D>,
rhs: Self::TensorPrimitive<D>,
) -> Self::TensorPrimitive<D> {
ops::basic::add(&lhs, &rhs)
}
fn sub<const D: usize>(
lhs: Self::TensorPrimitive<D>,
rhs: Self::TensorPrimitive<D>,
) -> Self::TensorPrimitive<D> {
ops::basic::sub(&lhs, &rhs)
}
fn mul<const D: usize>(
lhs: Self::TensorPrimitive<D>,
rhs: Self::TensorPrimitive<D>,
) -> Self::TensorPrimitive<D> {
ops::basic::mul(&lhs, &rhs)
}
fn div<const D: usize>(
lhs: Self::TensorPrimitive<D>,
rhs: Self::TensorPrimitive<D>,
) -> Self::TensorPrimitive<D> {
ops::basic::div(&lhs, &rhs)
}
fn matmul(
lhs: Self::TensorPrimitive<2>,
rhs: Self::TensorPrimitive<2>,
) -> Self::TensorPrimitive<2> {
ops::gemm::matmul(&lhs, &rhs)
}
fn bmm(
lhs: Self::TensorPrimitive<3>,
rhs: Self::TensorPrimitive<3>,
) -> Self::TensorPrimitive<3> {
ops::gemm::bmm(&lhs, &rhs)
}
// ==================== Unary Operations ====================
fn neg<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::unary::neg(&tensor)
}
fn exp<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::unary::exp(&tensor)
}
fn log<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::unary::log(&tensor)
}
fn sqrt<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::unary::sqrt(&tensor)
}
fn abs<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::unary::abs(&tensor)
}
fn sin<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::unary::sin(&tensor)
}
fn cos<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::unary::cos(&tensor)
}
fn pow<const D: usize>(
tensor: Self::TensorPrimitive<D>,
exp: Self::FloatElem,
) -> Self::TensorPrimitive<D> {
ops::unary::pow(&tensor, exp)
}
fn clamp<const D: usize>(
tensor: Self::TensorPrimitive<D>,
min: Self::FloatElem,
max: Self::FloatElem,
) -> Self::TensorPrimitive<D> {
ops::unary::clamp(&tensor, min, max)
}
// ==================== Activation Functions ====================
// ==================== Index Operations ====================
// Native kernels: the trait defaults round-trip through host memory.
fn index_select<const D: usize>(
tensor: Self::TensorPrimitive<D>,
indices: &[usize],
) -> Self::TensorPrimitive<D> {
ops::index::index_select(&tensor, indices)
}
fn index_add<const D: usize>(
tensor: Self::TensorPrimitive<D>,
indices: &[usize],
num_rows: usize,
) -> Self::TensorPrimitive<D> {
ops::index::index_add(&tensor, indices, num_rows)
}
fn relu<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::activation::relu(&tensor)
}
fn sigmoid<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::activation::sigmoid(&tensor)
}
fn tanh<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::unary::tanh(&tensor)
}
// ==================== Reduction Operations ====================
fn sum<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<1> {
ops::reduction::sum(&tensor)
}
fn sum_dim<const D: usize>(
tensor: Self::TensorPrimitive<D>,
dim: usize,
) -> Self::TensorPrimitive<D> {
ops::reduction::sum_dim(&tensor, dim)
}
fn mean<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<1> {
ops::reduction::mean(&tensor)
}
fn mean_dim<const D: usize>(
tensor: Self::TensorPrimitive<D>,
dim: usize,
) -> Self::TensorPrimitive<D> {
ops::reduction::mean_dim(&tensor, dim)
}
fn var<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<1> {
ops::reduction::var(&tensor)
}
fn var_dim<const D: usize>(
tensor: Self::TensorPrimitive<D>,
dim: usize,
) -> Self::TensorPrimitive<D> {
ops::reduction::var_dim(&tensor, dim)
}
fn max<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<1> {
ops::reduction::max(&tensor)
}
fn min<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<1> {
ops::reduction::min(&tensor)
}
// ==================== Shape Operations ====================
fn shape<const D: usize>(tensor: &Self::TensorPrimitive<D>) -> [usize; D] {
tensor.shape
}
fn reshape<const D1: usize, const D2: usize>(
tensor: Self::TensorPrimitive<D1>,
shape: [usize; D2],
) -> Self::TensorPrimitive<D2> {
ops::shape::reshape(tensor, shape)
}
fn transpose<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::shape::transpose(&tensor)
}
fn swap_dims<const D: usize>(
tensor: Self::TensorPrimitive<D>,
dim1: usize,
dim2: usize,
) -> Self::TensorPrimitive<D> {
ops::shape::swap_dims(&tensor, dim1, dim2)
}
// ==================== LLM-Specific Operations ====================
fn flash_attention(
query: Self::TensorPrimitive<4>,
key: Self::TensorPrimitive<4>,
value: Self::TensorPrimitive<4>,
mask: Option<&Self::TensorPrimitive<4>>,
scale: Self::FloatElem,
causal: bool,
) -> Self::TensorPrimitive<4> {
ops::attention::flash_attention(&query, &key, &value, mask, scale, causal)
}
fn softmax<const D: usize>(
tensor: Self::TensorPrimitive<D>,
dim: usize,
) -> Self::TensorPrimitive<D> {
ops::activation::softmax(&tensor, dim)
}
fn layer_norm<const D: usize>(
tensor: Self::TensorPrimitive<D>,
weight: &Self::TensorPrimitive<1>,
bias: Option<&Self::TensorPrimitive<1>>,
eps: Self::FloatElem,
) -> Self::TensorPrimitive<D> {
ops::normalization::layer_norm(&tensor, weight, bias, eps)
}
fn rms_norm<const D: usize>(
tensor: Self::TensorPrimitive<D>,
weight: &Self::TensorPrimitive<1>,
eps: Self::FloatElem,
) -> Self::TensorPrimitive<D> {
ops::normalization::rms_norm(&tensor, weight, eps)
}
fn rope<const D: usize>(
tensor: Self::TensorPrimitive<D>,
cos: &Self::TensorPrimitive<2>,
sin: &Self::TensorPrimitive<2>,
) -> Self::TensorPrimitive<D> {
ops::attention::rope(&tensor, cos, sin)
}
fn gelu<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::activation::gelu(&tensor)
}
fn silu<const D: usize>(tensor: Self::TensorPrimitive<D>) -> Self::TensorPrimitive<D> {
ops::activation::silu(&tensor)
}
fn leaky_relu<const D: usize>(
tensor: Self::TensorPrimitive<D>,
negative_slope: Self::FloatElem,
) -> Self::TensorPrimitive<D> {
ops::activation::leaky_relu(&tensor, negative_slope)
}
fn elu<const D: usize>(
tensor: Self::TensorPrimitive<D>,
alpha: Self::FloatElem,
) -> Self::TensorPrimitive<D> {
ops::activation::elu(&tensor, alpha)
}
// ==================== Comparison Operations ====================
fn gt_scalar<const D: usize>(
tensor: Self::TensorPrimitive<D>,
value: Self::FloatElem,
) -> Self::TensorPrimitive<D> {
ops::unary::gt_scalar(&tensor, value)
}
// ==================== Convolution Operations ====================
fn conv2d(
input: Self::TensorPrimitive<4>,
weight: &Self::TensorPrimitive<4>,
bias: Option<&Self::TensorPrimitive<1>>,
stride: [usize; 2],
padding: [usize; 2],
dilation: [usize; 2],
groups: usize,
) -> Self::TensorPrimitive<4> {
ops::conv::conv2d(input, weight, bias, stride, padding, dilation, groups)
}
// ==================== Pooling Operations ====================
fn max_pool2d(
input: Self::TensorPrimitive<4>,
kernel_size: [usize; 2],
stride: [usize; 2],
padding: [usize; 2],
) -> Self::TensorPrimitive<4> {
ops::conv::max_pool2d(input, kernel_size, stride, padding)
}
fn avg_pool2d(
input: Self::TensorPrimitive<4>,
kernel_size: [usize; 2],
stride: [usize; 2],
padding: [usize; 2],
count_include_pad: bool,
) -> Self::TensorPrimitive<4> {
ops::conv::avg_pool2d(input, kernel_size, stride, padding, count_include_pad)
}
// ==================== Device Management ====================
fn device<const D: usize>(tensor: &Self::TensorPrimitive<D>) -> Self::Device {
tensor.device.clone()
}
fn to_device<const D: usize>(
tensor: Self::TensorPrimitive<D>,
device: &Self::Device,
) -> Self::TensorPrimitive<D> {
if tensor.device == *device {
tensor
} else {
ops::device::copy_to_device(tensor, device)
}
}
fn to_data<const D: usize>(tensor: &Self::TensorPrimitive<D>) -> Vec<Self::FloatElem> {
ops::device::copy_to_host(tensor)
}
fn sync(device: &Self::Device) {
device.synchronize();
}
}
/// Type alias for training with CUDA + autodiff.
pub type CudaTraining = CudaBackend; // Will become Autodiff<CudaBackend>
/// Type alias for inference with CUDA (no autodiff overhead).
pub type CudaInference = CudaBackend;