//! `CubeCL` Backend implementation for `RustyTorch`++. //! //! This module provides the `CubeclBackend` type that implements the `Backend` trait //! from `rtx-backend`, enabling `CubeCL` to be used as a compute backend for tensor operations. //! //! The `CubeCL` backend provides portable GPU kernels that compile to: //! - CUDA PTX (NVIDIA GPUs) //! - WGSL (WebGPU) //! - HIP (AMD GPUs) //! - Metal MSL (via WebGPU) //! - SPIR-V (Vulkan) //! //! ## Tiered Kernel Strategy //! //! For optimal performance, `RustyTorch`++ uses a tiered approach: //! //! - **`CubeCL` (Tier 1)**: Portable ops (~60% of operations) //! - Elementwise: add, sub, mul, div, exp, log, sqrt //! - Activations: relu, gelu, sigmoid, tanh, silu //! - Reductions: sum, mean, max, min //! - Shape ops: reshape, transpose //! //! - **Native (Tier 2)**: Performance-critical ops (~40% of operations) //! - Large matmuls → cuBLAS/rocBLAS //! - `FlashAttention` → hand-optimized CUDA //! - Convolutions → cuDNN use std::fmt::Debug; use rtx_backend::{Backend, BoolU8, DeviceId, DeviceOps}; use crate::client; use crate::device::CubeclDevice; use crate::ops; use crate::runtime::RuntimeBackend; use crate::tensor::CubeclTensorPrimitive; /// CubeCL-based backend for portable GPU operations. /// /// This backend uses `CubeCL` to compile GPU kernels from Rust code, /// enabling the same kernel source to run on CUDA, WebGPU, HIP, etc. /// /// # Example /// /// ```rust,ignore /// use rtx_cubecl::CubeclBackend; /// use rtx_backend::Backend; /// /// type MyBackend = CubeclBackend; /// /// let device = CubeclBackend::default_device(); /// let tensor = MyBackend::zeros::<2>([3, 4], &device); /// ``` #[derive(Clone, Debug, Default)] pub struct CubeclBackend; /// Device operations for `CubeCL`. impl DeviceOps for CubeclDevice { fn id(&self) -> DeviceId { match self.backend { RuntimeBackend::Cuda => DeviceId::Cuda(self.index), RuntimeBackend::Hip => DeviceId::Rocm(self.index), RuntimeBackend::Wgpu => DeviceId::WebGpu(self.index), RuntimeBackend::Cpu => DeviceId::Cpu, } } fn memory_capacity(&self) -> usize { // TODO: Query actual device memory capacity match self.backend { RuntimeBackend::Cpu => { // Return system memory as approximation 16 * 1024 * 1024 * 1024 // 16 GB default } RuntimeBackend::Cuda | RuntimeBackend::Hip | RuntimeBackend::Wgpu => { // Default GPU memory 8 * 1024 * 1024 * 1024 // 8 GB default } } } fn memory_available(&self) -> usize { // TODO: Query actual available memory self.memory_capacity() / 2 // Conservative estimate } fn compute_capability(&self) -> Option<(u32, u32)> { match self.backend { RuntimeBackend::Cuda => Some((8, 0)), // Default to Ampere RuntimeBackend::Hip => Some((9, 0)), // Default to RDNA 2 RuntimeBackend::Wgpu => None, // WebGPU doesn't have compute capability RuntimeBackend::Cpu => None, } } fn synchronize(&self) { // TODO: Implement actual synchronization // For now, this is a no-op as we don't have actual compute yet } fn is_available(&self) -> bool { match self.backend { RuntimeBackend::Cpu => true, #[cfg(feature = "cuda")] RuntimeBackend::Cuda => true, // TODO: Check actual CUDA availability #[cfg(not(feature = "cuda"))] RuntimeBackend::Cuda => false, #[cfg(feature = "hip")] RuntimeBackend::Hip => true, // TODO: Check actual HIP availability #[cfg(not(feature = "hip"))] RuntimeBackend::Hip => false, #[cfg(feature = "wgpu")] RuntimeBackend::Wgpu => true, // TODO: Check actual WebGPU availability #[cfg(not(feature = "wgpu"))] RuntimeBackend::Wgpu => false, } } } impl Backend for CubeclBackend { type TensorPrimitive = CubeclTensorPrimitive; type Device = CubeclDevice; type FloatElem = f32; type IntElem = i32; type BoolElem = BoolU8; fn name() -> &'static str { "CubeCL" } fn seed(seed: u64) { // TODO: Implement RNG seeding for CubeCL let _ = seed; } // ==================== Tensor Creation ==================== fn zeros(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive { let numel: usize = shape.iter().product(); let data = vec![0.0f32; numel]; // Try to allocate on GPU, fall back to CPU if let Ok(client) = client::get_or_create(device) && let Ok(handle) = client.create_f32(&data) { return CubeclTensorPrimitive::from_handle(shape, handle, device.clone()); } // CPU fallback let bytes: Vec = data.iter().flat_map(|f| f.to_le_bytes()).collect(); CubeclTensorPrimitive::from_cpu_data(shape, bytes, device.clone()) } fn ones(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive { let numel: usize = shape.iter().product(); let data = vec![1.0f32; numel]; if let Ok(client) = client::get_or_create(device) && let Ok(handle) = client.create_f32(&data) { return CubeclTensorPrimitive::from_handle(shape, handle, device.clone()); } let bytes: Vec = data.iter().flat_map(|f| f.to_le_bytes()).collect(); CubeclTensorPrimitive::from_cpu_data(shape, bytes, device.clone()) } fn full( shape: [usize; D], fill_value: Self::FloatElem, device: &Self::Device, ) -> Self::TensorPrimitive { let numel: usize = shape.iter().product(); let data = vec![fill_value; numel]; if let Ok(client) = client::get_or_create(device) && let Ok(handle) = client.create_f32(&data) { return CubeclTensorPrimitive::from_handle(shape, handle, device.clone()); } let bytes: Vec = data.iter().flat_map(|f| f.to_le_bytes()).collect(); CubeclTensorPrimitive::from_cpu_data(shape, bytes, device.clone()) } fn rand(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive { // Use simple LCG for now, replace with proper RNG later let numel: usize = shape.iter().product(); let mut seed = 12345u64; let data: Vec = (0..numel) .map(|_| { seed = seed.wrapping_mul(1103515245).wrapping_add(12345); ((seed >> 16) & 0x7fff) as f32 / 32767.0 }) .collect(); if let Ok(client) = client::get_or_create(device) && let Ok(handle) = client.create_f32(&data) { return CubeclTensorPrimitive::from_handle(shape, handle, device.clone()); } let bytes: Vec = data.iter().flat_map(|f| f.to_le_bytes()).collect(); CubeclTensorPrimitive::from_cpu_data(shape, bytes, device.clone()) } fn randn(shape: [usize; D], device: &Self::Device) -> Self::TensorPrimitive { // Box-Muller transform for normal distribution let numel: usize = shape.iter().product(); let mut seed = 67890u64; let data: Vec = (0..numel) .map(|_| { // Generate two uniform random numbers seed = seed.wrapping_mul(1103515245).wrapping_add(12345); let u1 = ((seed >> 16) & 0x7fff) as f32 / 32768.0 + 1e-10; seed = seed.wrapping_mul(1103515245).wrapping_add(12345); let u2 = ((seed >> 16) & 0x7fff) as f32 / 32768.0; // Box-Muller transform (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos() }) .collect(); if let Ok(client) = client::get_or_create(device) && let Ok(handle) = client.create_f32(&data) { return CubeclTensorPrimitive::from_handle(shape, handle, device.clone()); } let bytes: Vec = data.iter().flat_map(|f| f.to_le_bytes()).collect(); CubeclTensorPrimitive::from_cpu_data(shape, bytes, device.clone()) } fn from_data( data: &[Self::FloatElem], shape: [usize; D], device: &Self::Device, ) -> Self::TensorPrimitive { if let Ok(client) = client::get_or_create(device) && let Ok(handle) = client.create_f32(data) { return CubeclTensorPrimitive::from_handle(shape, handle, device.clone()); } let bytes: Vec = data.iter().flat_map(|f| f.to_le_bytes()).collect(); CubeclTensorPrimitive::from_cpu_data(shape, bytes, device.clone()) } // ==================== Binary Operations ==================== fn add( lhs: Self::TensorPrimitive, rhs: Self::TensorPrimitive, ) -> Self::TensorPrimitive { ops::elementwise_binary(lhs, rhs, |a, b| a + b) } fn sub( lhs: Self::TensorPrimitive, rhs: Self::TensorPrimitive, ) -> Self::TensorPrimitive { ops::elementwise_binary(lhs, rhs, |a, b| a - b) } fn mul( lhs: Self::TensorPrimitive, rhs: Self::TensorPrimitive, ) -> Self::TensorPrimitive { ops::elementwise_binary(lhs, rhs, |a, b| a * b) } fn div( lhs: Self::TensorPrimitive, rhs: Self::TensorPrimitive, ) -> Self::TensorPrimitive { ops::elementwise_binary(lhs, rhs, |a, b| a / b) } fn matmul( lhs: Self::TensorPrimitive<2>, rhs: Self::TensorPrimitive<2>, ) -> Self::TensorPrimitive<2> { ops::matmul(lhs, rhs) } fn bmm( lhs: Self::TensorPrimitive<3>, rhs: Self::TensorPrimitive<3>, ) -> Self::TensorPrimitive<3> { ops::bmm(lhs, rhs) } // ==================== Unary Operations ==================== fn neg(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, |x| -x) } fn exp(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, f32::exp) } fn log(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, f32::ln) } fn sqrt(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, f32::sqrt) } fn abs(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, f32::abs) } fn sin(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, f32::sin) } fn cos(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, f32::cos) } fn pow( tensor: Self::TensorPrimitive, exp: Self::FloatElem, ) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, move |x| x.powf(exp)) } fn clamp( tensor: Self::TensorPrimitive, min: Self::FloatElem, max: Self::FloatElem, ) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, move |x| x.clamp(min, max)) } // ==================== Activation Functions ==================== fn relu(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, |x| x.max(0.0)) } fn sigmoid(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, |x| 1.0 / (1.0 + (-x).exp())) } fn tanh(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { ops::elementwise_unary(tensor, f32::tanh) } fn gelu(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { // GELU approximation: 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3))) ops::elementwise_unary(tensor, |x| { let sqrt_2_over_pi = 0.797_884_6_f32; let coef = 0.044715f32; let inner = sqrt_2_over_pi * (x + coef * x * x * x); 0.5 * x * (1.0 + inner.tanh()) }) } fn silu(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { // SiLU (Swish): x * sigmoid(x) ops::elementwise_unary(tensor, |x| x * (1.0 / (1.0 + (-x).exp()))) } fn leaky_relu( tensor: Self::TensorPrimitive, negative_slope: Self::FloatElem, ) -> Self::TensorPrimitive { ops::elementwise_unary( tensor, move |x| if x > 0.0 { x } else { negative_slope * x }, ) } fn elu( tensor: Self::TensorPrimitive, alpha: Self::FloatElem, ) -> Self::TensorPrimitive { ops::elementwise_unary( tensor, move |x| if x > 0.0 { x } else { alpha * (x.exp() - 1.0) }, ) } // ==================== Reduction Operations ==================== fn sum(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1> { ops::sum(tensor) } fn sum_dim( tensor: Self::TensorPrimitive, dim: usize, ) -> Self::TensorPrimitive { // TODO: Implement proper dimension reduction // For now, return input (placeholder) let _ = dim; tensor } fn mean(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1> { ops::mean(tensor) } fn mean_dim( tensor: Self::TensorPrimitive, dim: usize, ) -> Self::TensorPrimitive { // TODO: Implement proper dimension reduction let _ = dim; tensor } fn var(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1> { ops::var(tensor) } fn var_dim( tensor: Self::TensorPrimitive, dim: usize, ) -> Self::TensorPrimitive { // TODO: Implement proper dimension reduction let _ = dim; tensor } fn max(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1> { ops::max(tensor) } fn min(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive<1> { ops::min(tensor) } // ==================== Shape Operations ==================== fn shape(tensor: &Self::TensorPrimitive) -> [usize; D] { tensor.shape } fn reshape( tensor: Self::TensorPrimitive, shape: [usize; D2], ) -> Self::TensorPrimitive { tensor.reshape(shape).expect("Reshape failed") } fn transpose(tensor: Self::TensorPrimitive) -> Self::TensorPrimitive { // TODO: Launch transpose kernel let mut new_shape = tensor.shape; if D >= 2 { new_shape.swap(D - 2, D - 1); } CubeclTensorPrimitive::new(new_shape, tensor.device) } fn swap_dims( tensor: Self::TensorPrimitive, dim1: usize, dim2: usize, ) -> Self::TensorPrimitive { // TODO: Launch permute kernel let mut new_shape = tensor.shape; new_shape.swap(dim1, dim2); CubeclTensorPrimitive::new(new_shape, tensor.device) } // ==================== LLM-Specific Operations ==================== // These delegate to native kernels for maximum performance 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> { // TODO: Delegate to native FlashAttention kernel // CubeCL is not used here for maximum performance let _ = (key, value, mask, scale, causal); query } fn softmax( tensor: Self::TensorPrimitive, dim: usize, ) -> Self::TensorPrimitive { ops::softmax(tensor, dim) } fn layer_norm( tensor: Self::TensorPrimitive, weight: &Self::TensorPrimitive<1>, bias: Option<&Self::TensorPrimitive<1>>, eps: Self::FloatElem, ) -> Self::TensorPrimitive { // TODO: Launch layer_norm kernel let _ = (weight, bias, eps); tensor } fn rms_norm( tensor: Self::TensorPrimitive, weight: &Self::TensorPrimitive<1>, eps: Self::FloatElem, ) -> Self::TensorPrimitive { // TODO: Launch rms_norm kernel let _ = (weight, eps); tensor } fn rope( tensor: Self::TensorPrimitive, cos: &Self::TensorPrimitive<2>, sin: &Self::TensorPrimitive<2>, ) -> Self::TensorPrimitive { // TODO: Launch rope kernel let _ = (cos, sin); tensor } // ==================== Comparison Operations ==================== fn gt_scalar( tensor: Self::TensorPrimitive, value: Self::FloatElem, ) -> Self::TensorPrimitive { // TODO: Launch gt_scalar kernel let _ = value; tensor } // ==================== 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> { // TODO: Delegate to cuDNN or implement CubeCL convolution let _ = (bias, groups); let batch = input.shape[0]; let out_channels = weight.shape[0]; // Compute output spatial dimensions let h_out = (input.shape[2] + 2 * padding[0] - dilation[0] * (weight.shape[2] - 1) - 1) / stride[0] + 1; let w_out = (input.shape[3] + 2 * padding[1] - dilation[1] * (weight.shape[3] - 1) - 1) / stride[1] + 1; CubeclTensorPrimitive::new([batch, out_channels, h_out, w_out], input.device) } // ==================== Pooling Operations ==================== fn max_pool2d( input: Self::TensorPrimitive<4>, kernel_size: [usize; 2], stride: [usize; 2], padding: [usize; 2], ) -> Self::TensorPrimitive<4> { // TODO: Launch max_pool2d kernel let batch = input.shape[0]; let channels = input.shape[1]; let h_out = (input.shape[2] + 2 * padding[0] - kernel_size[0]) / stride[0] + 1; let w_out = (input.shape[3] + 2 * padding[1] - kernel_size[1]) / stride[1] + 1; CubeclTensorPrimitive::new([batch, channels, h_out, w_out], input.device) } fn avg_pool2d( input: Self::TensorPrimitive<4>, kernel_size: [usize; 2], stride: [usize; 2], padding: [usize; 2], count_include_pad: bool, ) -> Self::TensorPrimitive<4> { // TODO: Launch avg_pool2d kernel let _ = count_include_pad; let batch = input.shape[0]; let channels = input.shape[1]; let h_out = (input.shape[2] + 2 * padding[0] - kernel_size[0]) / stride[0] + 1; let w_out = (input.shape[3] + 2 * padding[1] - kernel_size[1]) / stride[1] + 1; CubeclTensorPrimitive::new([batch, channels, h_out, w_out], input.device) } // ==================== Device Management ==================== fn device(tensor: &Self::TensorPrimitive) -> Self::Device { tensor.device.clone() } fn to_device( tensor: Self::TensorPrimitive, device: &Self::Device, ) -> Self::TensorPrimitive { // TODO: Copy tensor data to new device if different if tensor.device == *device { tensor } else { CubeclTensorPrimitive::new(tensor.shape, device.clone()) } } fn to_data(tensor: &Self::TensorPrimitive) -> Vec { // Try to read from GPU buffer if let Some(buffer) = tensor.buffer() { if let Some(gpu_handle) = buffer.as_gpu() && let Ok(client) = client::get_or_create(&tensor.device) && let Ok(data) = client.read_f32(gpu_handle) { return data; } // Read from CPU buffer if let Some(bytes) = buffer.as_cpu() { return bytes .chunks_exact(4) .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap())) .collect(); } } // No buffer - return zeros vec![0.0; tensor.numel()] } fn sync(device: &Self::Device) { if let Ok(client) = client::get_or_create(device) { client.sync(); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_backend_name() { assert_eq!(CubeclBackend::name(), "CubeCL"); } #[test] fn test_zeros() { let device = CubeclDevice::cpu(); let tensor = CubeclBackend::zeros::<2>([3, 4], &device); assert_eq!(tensor.shape, [3, 4]); } #[test] fn test_matmul_shape() { let device = CubeclDevice::cpu(); let lhs = CubeclBackend::zeros::<2>([3, 4], &device); let rhs = CubeclBackend::zeros::<2>([4, 5], &device); let result = CubeclBackend::matmul(lhs, rhs); assert_eq!(result.shape, [3, 5]); } #[test] fn test_reshape() { let device = CubeclDevice::cpu(); let tensor = CubeclBackend::zeros::<2>([3, 4], &device); let reshaped = CubeclBackend::reshape::<2, 3>(tensor, [2, 2, 3]); assert_eq!(reshaped.shape, [2, 2, 3]); } #[test] fn test_transpose() { let device = CubeclDevice::cpu(); let tensor = CubeclBackend::zeros::<2>([3, 4], &device); let transposed = CubeclBackend::transpose(tensor); assert_eq!(transposed.shape, [4, 3]); } #[test] fn test_device_id() { let cpu = CubeclDevice::cpu(); assert_eq!(cpu.id(), DeviceId::Cpu); let cuda = CubeclDevice::new(RuntimeBackend::Cuda, 0); assert_eq!(cuda.id(), DeviceId::Cuda(0)); } #[test] fn test_from_data_to_data_roundtrip() { let device = CubeclDevice::cpu(); let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0]; let tensor = CubeclBackend::from_data(&data, [2, 3], &device); let retrieved = CubeclBackend::to_data(&tensor); assert_eq!(retrieved.len(), 6); // Check values match (using CPU backend) for (a, b) in data.iter().zip(retrieved.iter()) { assert!((a - b).abs() < 1e-6); } } #[test] fn test_zeros_values() { let device = CubeclDevice::cpu(); let tensor = CubeclBackend::zeros::<2>([3, 4], &device); let data = CubeclBackend::to_data(&tensor); assert_eq!(data.len(), 12); for val in &data { assert!((val - 0.0).abs() < 1e-6); } } #[test] fn test_ones_values() { let device = CubeclDevice::cpu(); let tensor = CubeclBackend::ones::<2>([2, 2], &device); let data = CubeclBackend::to_data(&tensor); assert_eq!(data.len(), 4); for val in &data { assert!((val - 1.0).abs() < 1e-6); } } #[test] fn test_full_values() { let device = CubeclDevice::cpu(); let tensor = CubeclBackend::full::<1>([5], 3.14, &device); let data = CubeclBackend::to_data(&tensor); assert_eq!(data.len(), 5); for val in &data { assert!((val - 3.14).abs() < 1e-6); } } #[test] fn test_add_operation() { let device = CubeclDevice::cpu(); let a = CubeclBackend::from_data(&[1.0, 2.0, 3.0, 4.0], [2, 2], &device); let b = CubeclBackend::from_data(&[5.0, 6.0, 7.0, 8.0], [2, 2], &device); let c = CubeclBackend::add(a, b); let data = CubeclBackend::to_data(&c); assert_eq!(data, vec![6.0, 8.0, 10.0, 12.0]); } #[test] fn test_mul_operation() { let device = CubeclDevice::cpu(); let a = CubeclBackend::from_data(&[1.0, 2.0, 3.0, 4.0], [4], &device); let b = CubeclBackend::from_data(&[2.0, 3.0, 4.0, 5.0], [4], &device); let c = CubeclBackend::mul(a, b); let data = CubeclBackend::to_data(&c); assert_eq!(data, vec![2.0, 6.0, 12.0, 20.0]); } #[test] fn test_relu_operation() { let device = CubeclDevice::cpu(); let a = CubeclBackend::from_data(&[-2.0, -1.0, 0.0, 1.0, 2.0], [5], &device); let b = CubeclBackend::relu(a); let data = CubeclBackend::to_data(&b); assert_eq!(data, vec![0.0, 0.0, 0.0, 1.0, 2.0]); } #[test] fn test_sigmoid_operation() { let device = CubeclDevice::cpu(); let a = CubeclBackend::from_data(&[0.0], [1], &device); let b = CubeclBackend::sigmoid(a); let data = CubeclBackend::to_data(&b); assert!((data[0] - 0.5).abs() < 1e-6); } #[test] fn test_sum_operation() { let device = CubeclDevice::cpu(); let a = CubeclBackend::from_data(&[1.0, 2.0, 3.0, 4.0], [4], &device); let s = CubeclBackend::sum(a); let data = CubeclBackend::to_data(&s); assert_eq!(data, vec![10.0]); } #[test] fn test_mean_operation() { let device = CubeclDevice::cpu(); let a = CubeclBackend::from_data(&[1.0, 2.0, 3.0, 4.0], [4], &device); let m = CubeclBackend::mean(a); let data = CubeclBackend::to_data(&m); assert!((data[0] - 2.5).abs() < 1e-6); } #[test] fn test_matmul_computation() { let device = CubeclDevice::cpu(); let a = CubeclBackend::from_data(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3], &device); let b = CubeclBackend::from_data(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [3, 2], &device); let c = CubeclBackend::matmul(a, b); assert_eq!(c.shape, [2, 2]); let data = CubeclBackend::to_data(&c); // Verify matrix multiplication results assert_eq!(data, vec![22.0, 28.0, 49.0, 64.0]); } #[test] fn test_exp_log_roundtrip() { let device = CubeclDevice::cpu(); let a = CubeclBackend::from_data(&[1.0, 2.0, 3.0], [3], &device); let b = CubeclBackend::exp(a); let c = CubeclBackend::log(b); let data = CubeclBackend::to_data(&c); for (i, val) in data.iter().enumerate() { assert!((val - (i + 1) as f32).abs() < 1e-5); } } }