//! # RustyTorch++ Backend Abstraction //! //! Burn-inspired compile-time backend dispatch for RustyTorch++. //! //! This crate provides the core `Backend` trait that enables: //! - Compile-time backend selection (no runtime overhead) //! - Clean separation of backend-specific code //! - Easy addition of new backends //! - Type-safe operations //! //! ## Architecture //! //! The backend system uses associated types for compile-time polymorphism: //! //! ```text //! Backend Trait //! ├── CudaBackend - NVIDIA GPU (hand-optimized kernels) //! ├── MetalBackend - Apple Silicon (Metal Performance Shaders) //! ├── RocmBackend - AMD GPU (HIP runtime) //! ├── WebGpuBackend - Browser/WASM (WebGPU shaders) //! └── CpuBackend - CPU fallback (BLAS/SIMD) //! ``` //! //! ## Example //! //! ```rust,ignore //! use rtx_backend::{Backend, CudaBackend}; //! //! // Training uses autodiff wrapper //! type TrainingBackend = Autodiff; //! //! // Inference uses raw backend (zero overhead) //! type InferenceBackend = CudaBackend; //! //! fn train(model: &Model, data: &Tensor) { //! // Backend-agnostic training code //! } //! ``` #![warn(missing_docs)] pub mod auto_select; mod device; mod element; pub mod error; mod ops; mod tensor; pub use device::{DeviceId, DeviceOps}; pub use element::{BoolElement, BoolU8, FloatElement, IntElement}; pub use error::{BackendError, BackendResult}; pub use ops::{ ActivationOps, // LLM-specific ops (keep hand-optimized) AttentionOps, KVCacheOps, ModuleOps, TensorOps, }; pub use tensor::TensorHandle; use std::fmt::Debug; /// Core backend trait for RustyTorch++ - Burn-inspired compile-time dispatch. /// /// Each backend implementation provides its own tensor primitive type and /// operations. This enables: /// - Zero-cost abstraction through monomorphization /// - Backend-specific optimizations /// - Type-safe device management /// /// # Design Philosophy /// /// Unlike the previous Device enum approach, the Backend trait: /// 1. Uses associated types for compile-time dispatch (no match arms) /// 2. Allows each backend to define its own tensor storage /// 3. Enables composable wrappers (Autodiff, Quantized, etc.) /// 4. Provides better type safety /// /// # LLM Performance /// /// For LLM-critical operations, backends delegate to hand-optimized kernels: /// - `flash_attention` - FlashAttention-3 for CUDA, Metal MSL for Apple /// - `ring_attention` - Distributed attention for 16M+ context /// - `kv_cache_ops` - Entropy-guided eviction pub trait Backend: Clone + Send + Sync + Debug + Default + 'static { /// The tensor primitive type for this backend. /// This is the actual storage/handle that holds tensor data. type TensorPrimitive: Clone + Send + Sync + Debug; /// The device type for this backend. type Device: DeviceOps; /// The floating-point element type (f32, f16, bf16, fp8). type FloatElem: FloatElement; /// The integer element type (i32, i64, u32, u64). type IntElem: IntElement; /// The boolean element type. type BoolElem: BoolElement; /// Backend name for debugging and logging. fn name() -> &'static str; /// Seed the random number generator. fn seed(seed: u64); // ==================== Tensor Creation ==================== /// Create a tensor filled with zeros. fn zeros(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive; /// Create a tensor filled with ones. fn ones(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive; /// Create a tensor filled with a scalar value. fn full( shape: [usize; D], fill_value: Self::FloatElem, device: &Self::Device, ) -> Self::TensorPrimitive; /// Create a tensor with random uniform values in [0, 1). fn rand(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive; /// Create a tensor with random normal values (mean=0, std=1). fn randn(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive; /// Create a tensor from raw data. fn from_data( data: &[Self::FloatElem], shape: [usize; D], device: &Self::Device, ) -> Self::TensorPrimitive; // ==================== Basic Operations ==================== // NOTE: All operations take OWNED tensors (not references) to enable // ownership-based kernel fusion. Use .clone() when a tensor is needed // multiple times - the clone count informs the fusion system. /// Element-wise addition. fn add( lhs: Self::TensorPrimitive, rhs: Self::TensorPrimitive, ) -> Self::TensorPrimitive; /// Element-wise subtraction. fn sub( lhs: Self::TensorPrimitive, rhs: Self::TensorPrimitive, ) -> Self::TensorPrimitive; /// Element-wise multiplication. fn mul( lhs: Self::TensorPrimitive, rhs: Self::TensorPrimitive, ) -> Self::TensorPrimitive; /// Element-wise division. fn div( lhs: Self::TensorPrimitive, rhs: Self::TensorPrimitive, ) -> Self::TensorPrimitive; /// Matrix multiplication. fn matmul( lhs: Self::TensorPrimitive<2>, rhs: Self::TensorPrimitive<2>, ) -> Self::TensorPrimitive<2>; /// Batched matrix multiplication. fn bmm( lhs: Self::TensorPrimitive<3>, rhs: Self::TensorPrimitive<3>, ) -> Self::TensorPrimitive<3>; // ==================== Unary Operations ==================== // NOTE: Unary ops also take owned tensors for fusion opportunities. /// Element-wise negation. fn neg(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Element-wise exponential. fn exp(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Element-wise natural logarithm. fn log(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Element-wise square root. fn sqrt(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Element-wise absolute value. fn abs(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Element-wise sine. fn sin(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Element-wise cosine. fn cos(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Element-wise power. fn pow( tensor: Self::TensorPrimitive, exp: Self::FloatElem, ) -> Self::TensorPrimitive; /// Clamp tensor values to a range. fn clamp( tensor: Self::TensorPrimitive, min: Self::FloatElem, max: Self::FloatElem, ) -> Self::TensorPrimitive; // ==================== Activation Functions ==================== /// ReLU activation: max(0, x). fn relu(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Sigmoid activation: 1 / (1 + exp(-x)). fn sigmoid(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Tanh activation. fn tanh(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; // ==================== Reduction Operations ==================== // NOTE: Reductions consume input tensor for fusion with preceding ops. /// Sum all elements. fn sum(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1>; /// Sum along a dimension. fn sum_dim( tensor: Self::TensorPrimitive, dim: usize, ) -> Self::TensorPrimitive; /// Mean of all elements. fn mean(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1>; /// Mean along a dimension. fn mean_dim( tensor: Self::TensorPrimitive, dim: usize, ) -> Self::TensorPrimitive; /// Variance of all elements. fn var(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1>; /// Variance along a dimension. fn var_dim( tensor: Self::TensorPrimitive, dim: usize, ) -> Self::TensorPrimitive; /// Maximum element. fn max(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1>; /// Minimum element. fn min(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1>; // ==================== Shape Operations ==================== /// Get the shape of a tensor. fn shape(tensor: &Self::TensorPrimitive) -> [usize; D]; /// Reshape a tensor (no data copy if contiguous). fn reshape( tensor: Self::TensorPrimitive, shape: [usize; D2], ) -> Self::TensorPrimitive; /// Transpose last two dimensions. fn transpose(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Swap two dimensions. fn swap_dims( tensor: Self::TensorPrimitive, dim1: usize, dim2: usize, ) -> Self::TensorPrimitive; // ==================== Row Indexing (gather / scatter-add) ==================== // Differentiable row indexing along dim 0. These two ops are each other's // adjoint, which is exactly what message passing on a graph needs: // d/dx index_select(x, idx) = index_add(grad, idx, rows(x)) // d/dx index_add(x, idx, num_rows) = index_select(grad, idx) // // Both have default bodies that round-trip through host memory via // `to_data` / `from_data`, so every backend is correct out of the box; // backends override them with native kernels for speed. /// Gather rows along dim 0: `out[i, ..] = tensor[indices[i], ..]`. /// /// Output shape is `[indices.len(), shape[1..]]`. Indices may repeat. /// /// # Panics /// Panics if any index is `>= shape[0]`, or if `D == 0`. fn index_select( tensor: Self::TensorPrimitive, indices: &[usize], ) -> Self::TensorPrimitive { let shape = Self::shape(&tensor); assert!(D >= 1, "index_select requires at least one dimension"); let num_rows = shape[0]; let row_len: usize = shape[1..].iter().product(); let src = Self::to_data(&tensor); let mut out = Vec::with_capacity(indices.len() * row_len); for &idx in indices { assert!( idx < num_rows, "index_select: index {idx} out of range for {num_rows} rows" ); out.extend_from_slice(&src[idx * row_len..(idx + 1) * row_len]); } let mut out_shape = shape; out_shape[0] = indices.len(); Self::from_data(&out, out_shape, &Self::device(&tensor)) } /// Scatter-add rows along dim 0 into a zero tensor with `num_rows` rows: /// `out = zeros([num_rows, shape[1..]]); out[indices[i], ..] += tensor[i, ..]`. /// /// Indices may repeat (contributions accumulate); rows never referenced /// stay zero. This is the adjoint of [`Backend::index_select`]. /// /// # Panics /// Panics if `indices.len() != shape[0]`, if any index is `>= num_rows`, /// or if `D == 0`. fn index_add( tensor: Self::TensorPrimitive, indices: &[usize], num_rows: usize, ) -> Self::TensorPrimitive { let shape = Self::shape(&tensor); assert!(D >= 1, "index_add requires at least one dimension"); assert_eq!( indices.len(), shape[0], "index_add: indices.len() must equal the number of input rows" ); let row_len: usize = shape[1..].iter().product(); let src = Self::to_data(&tensor); let mut out = vec![Self::FloatElem::zero(); num_rows * row_len]; for (i, &idx) in indices.iter().enumerate() { assert!( idx < num_rows, "index_add: index {idx} out of range for {num_rows} rows" ); let dst = &mut out[idx * row_len..(idx + 1) * row_len]; let row = &src[i * row_len..(i + 1) * row_len]; for (d, &s) in dst.iter_mut().zip(row) { *d = Self::FloatElem::from_f64(d.to_f64() + s.to_f64()); } } let mut out_shape = shape; out_shape[0] = num_rows; Self::from_data(&out, out_shape, &Self::device(&tensor)) } // ==================== LLM-Specific Operations ==================== // These delegate to hand-optimized kernels for maximum performance. /// Flash Attention (optimized for each backend). /// /// Uses FlashAttention-3 on CUDA, Metal MSL kernels on Apple Silicon, /// or efficient fallbacks on other backends. /// NOTE: Q/K/V are owned to enable fusion; mask stays borrowed (read-only). fn flash_attention( query: Self::TensorPrimitive<4>, // [batch, heads, seq_len, head_dim] key: Self::TensorPrimitive<4>, value: Self::TensorPrimitive<4>, mask: Option<&Self::TensorPrimitive<4>>, scale: Self::FloatElem, causal: bool, ) -> Self::TensorPrimitive<4>; /// Softmax along the last dimension (numerically stable). fn softmax( tensor: Self::TensorPrimitive, dim: usize, ) -> Self::TensorPrimitive; /// Layer normalization. /// NOTE: Input tensor is owned; weight/bias stay borrowed (shared params). fn layer_norm( tensor: Self::TensorPrimitive, weight: &Self::TensorPrimitive<1>, bias: Option<&Self::TensorPrimitive<1>>, eps: Self::FloatElem, ) -> Self::TensorPrimitive; /// RMS normalization (used in LLaMA, etc.). /// NOTE: Input tensor is owned; weight stays borrowed (shared param). fn rms_norm( tensor: Self::TensorPrimitive, weight: &Self::TensorPrimitive<1>, eps: Self::FloatElem, ) -> Self::TensorPrimitive; /// Rotary Position Embeddings (RoPE). /// NOTE: Input tensor is owned; cos/sin stay borrowed (precomputed). fn rope( tensor: Self::TensorPrimitive, cos: &Self::TensorPrimitive<2>, sin: &Self::TensorPrimitive<2>, ) -> Self::TensorPrimitive; /// GELU activation. fn gelu(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// SiLU (Swish) activation. fn silu(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Leaky ReLU activation: max(negative_slope * x, x). fn leaky_relu( tensor: Self::TensorPrimitive, negative_slope: Self::FloatElem, ) -> Self::TensorPrimitive; /// ELU activation: x if x > 0, else alpha * (exp(x) - 1). fn elu( tensor: Self::TensorPrimitive, alpha: Self::FloatElem, ) -> Self::TensorPrimitive; // ==================== Comparison Operations ==================== /// Greater than scalar comparison. /// Returns a tensor with 1.0 where elements are > value, 0.0 otherwise. fn gt_scalar( tensor: Self::TensorPrimitive, value: Self::FloatElem, ) -> Self::TensorPrimitive; // ==================== Convolution Operations ==================== /// 2D Convolution. /// /// # Arguments /// * `input` - Input tensor `[batch, in_channels, height, width]` /// * `weight` - Kernel weights `[out_channels, in_channels/groups, kernel_h, kernel_w]` /// * `bias` - Optional bias `[out_channels]` /// * `stride` - Stride (height, width) /// * `padding` - Padding (height, width) /// * `dilation` - Dilation (height, width) /// * `groups` - Number of groups for grouped convolution 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>; // ==================== Pooling Operations ==================== /// 2D max pooling. /// /// # Arguments /// * `input` - Input tensor `[batch, channels, height, width]` /// * `kernel_size` - Size of the pooling window `[kH, kW]` /// * `stride` - Stride of the pooling window `[sH, sW]` /// * `padding` - Padding added to both sides `[pH, pW]` /// /// # Returns /// Output tensor `[batch, channels, out_height, out_width]` fn max_pool2d( input: Self::TensorPrimitive<4>, kernel_size: [usize; 2], stride: [usize; 2], padding: [usize; 2], ) -> Self::TensorPrimitive<4>; /// 2D average pooling. /// /// # Arguments /// * `input` - Input tensor `[batch, channels, height, width]` /// * `kernel_size` - Size of the pooling window `[kH, kW]` /// * `stride` - Stride of the pooling window `[sH, sW]` /// * `padding` - Padding added to both sides `[pH, pW]` /// * `count_include_pad` - Whether to include padding in the averaging calculation /// /// # Returns /// Output tensor `[batch, channels, out_height, out_width]` fn avg_pool2d( input: Self::TensorPrimitive<4>, kernel_size: [usize; 2], stride: [usize; 2], padding: [usize; 2], count_include_pad: bool, ) -> Self::TensorPrimitive<4>; // ==================== Device Management ==================== /// Get the device of a tensor. fn device(tensor: &Self::TensorPrimitive) -> Self::Device; /// Move tensor to a device. fn to_device( tensor: Self::TensorPrimitive, device: &Self::Device, ) -> Self::TensorPrimitive; /// Copy data to CPU for inspection. fn to_data(tensor: &Self::TensorPrimitive) -> Vec; /// Synchronize the backend (wait for all operations to complete). fn sync(device: &Self::Device); } /// Marker trait for backends that support automatic differentiation. /// /// This trait is implemented by autodiff wrappers like `Autodiff`. pub trait AutodiffBackend: Backend { /// The inner backend (without autodiff). type InnerBackend: Backend; /// Get a reference to the inner backend. fn inner(&self) -> &Self::InnerBackend; /// Convert a tensor to require gradients. fn require_grad(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; /// Compute gradients via backpropagation. /// /// This method panics if: /// - The tensor dimension is > 6 (compile-time prevented by const generics) /// - The backward pass encounters an error /// /// For fallible gradient computation, use [`try_backward`](Self::try_backward). fn backward( tensor: &Self::TensorPrimitive, ) -> GradientMap; /// Compute gradients via backpropagation, returning errors instead of panicking. /// /// This is the fallible version of [`backward`](Self::backward). It returns: /// - `Ok(GradientMap)` on success /// - `Err(AutogradError)` if the backward pass fails /// /// # Errors /// /// Returns `AutogradError::UnsupportedDimension` if the tensor dimension is > 6. /// Returns `AutogradError::BackwardError` if gradient computation fails. fn try_backward( tensor: &Self::TensorPrimitive, ) -> Result, crate::error::BackendError>; } /// Map from tensor IDs to their gradients. /// /// This structure stores computed gradients during backpropagation. /// The `gradients` field will be used when `AutodiffBackend` implementations /// populate gradients during the backward pass. pub struct GradientMap { #[allow(dead_code)] // Used by AutodiffBackend implementations gradients: std::collections::HashMap>, _marker: std::marker::PhantomData, } impl GradientMap { /// Create a new empty gradient map. pub fn new() -> Self { Self { gradients: std::collections::HashMap::new(), _marker: std::marker::PhantomData, } } /// Get the gradient for a tensor by ID. pub fn get(&self, _id: usize) -> Option<&B::TensorPrimitive> { // Implementation would downcast from Any None } } impl Default for GradientMap { fn default() -> Self { Self::new() } } /// Marker trait for backends that support quantization. pub trait QuantizedBackend: Backend { /// The quantization scheme. type QuantScheme; /// Quantize a tensor. fn quantize( tensor: Self::TensorPrimitive, scheme: &Self::QuantScheme, ) -> Self::TensorPrimitive; /// Dequantize a tensor. fn dequantize(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive; }