etensor-core 0.0.1

The pure Rust tensor math and autograd engine
Documentation
//! Binary element-wise operations for the CPU execution backend.
//!
//! These operations are memory-bandwidth-bound (just read + write, minimal math).
//! Parallelism via Rayon is NOT used because the memory bus is the bottleneck,
//! not the CPU. Adding thread wake-up overhead (~20µs) hurts performance
//! at all practical tensor sizes. The single-threaded vectorized loop already
//! saturates the memory bus on modern CPUs.

use crate::tensor::Tensor;
use crate::buffer::Buffer;
use crate::errors::EtensorResult;
use crate::backends::traits::Backend;

/// The execution driver for standard host CPU memory.
pub struct CpuBackend;

impl Backend for CpuBackend {
    fn add(a: &Tensor, b: &Tensor) -> EtensorResult<Tensor> {
        let slice_a = a.data.as_f32_slice()?;
        let slice_b = b.data.as_f32_slice()?;

        // Single-threaded vectorized loop. LLVM auto-vectorizes this to SIMD
        // (SSE2/AVX2) which already saturates memory bandwidth on one core.
        let out_vec: Vec<f32> = slice_a.iter().zip(slice_b).map(|(x, y)| x + y).collect();

        Ok(Tensor::new(
            Buffer::from_f32_vec(out_vec),
            a.shape.clone(),
            a.device,
            a.dtype,
            false, // Gradients are exclusively managed by the Dispatcher, not the hardware kernel.
        ))
    }

    fn mul(a: &Tensor, b: &Tensor) -> EtensorResult<Tensor> {
        let slice_a = a.data.as_f32_slice()?;
        let slice_b = b.data.as_f32_slice()?;

        let out_vec: Vec<f32> = slice_a.iter().zip(slice_b).map(|(x, y)| x * y).collect();

        Ok(Tensor::new(
            Buffer::from_f32_vec(out_vec),
            a.shape.clone(),
            a.device,
            a.dtype,
            false,
        ))
    }

    fn matmul(a: &Tensor, b: &Tensor) -> EtensorResult<Tensor> {
        crate::backends::cpu::matmul::matmul_forward(a, b)
    }

    fn sum_all(a: &Tensor) -> EtensorResult<Tensor> {
        crate::backends::cpu::reduce::sum_all(a)
    }

    fn mean_all(a: &Tensor) -> EtensorResult<Tensor> {
        crate::backends::cpu::reduce::mean_all(a)
    }

    fn max_all(a: &Tensor) -> EtensorResult<Tensor> {
        crate::backends::cpu::reduce::max_all(a)
    }

    fn relu(a: &Tensor) -> EtensorResult<Tensor> {
        crate::backends::cpu::unary::relu_forward(a)
    }

    fn sigmoid(a: &Tensor) -> EtensorResult<Tensor> {
        crate::backends::cpu::unary::sigmoid_forward(a)
    }

    fn add_relu(a: &Tensor, b: &Tensor) -> EtensorResult<Tensor> {
        crate::backends::cpu::fusion::add_relu_forward(a, b)
    }

    fn linear(x: &Tensor, w: &Tensor, b: &Tensor) -> EtensorResult<Tensor> {
        crate::backends::cpu::fusion::linear_forward(x, w, b)
    }
}

// =====================================================================
// UNIT TESTS
// =====================================================================
#[cfg(test)]
mod tests {
    use super::*;
    use crate::errors::EtensorError;
    use crate::device::Device;
    use crate::dtypes::DType;
    use crate::shape::Shape;

    // Helper to generate purely physical test tensors
    fn make_test_tensor(data: Vec<f32>) -> Tensor {
        let len = data.len();
        Tensor::new(
            Buffer::from_f32_vec(data),
            Shape::new(vec![len]),
            Device::Cpu,
            DType::F32,
            false,
        )
    }

    #[test]
    fn test_cpu_backend_add() {
        let a = make_test_tensor(vec![1.5, 2.5, 3.5]);
        let b = make_test_tensor(vec![2.0, 3.0, 4.0]);
        
        let c = CpuBackend::add(&a, &b).unwrap();
        let slice = c.data.as_f32_slice().unwrap();
        
        assert_eq!(slice, &[3.5, 5.5, 7.5]);
    }

    #[test]
    fn test_cpu_backend_mul() {
        let a = make_test_tensor(vec![2.0, 4.0, 6.0]);
        let b = make_test_tensor(vec![3.0, 0.5, 10.0]);
        
        let c = CpuBackend::mul(&a, &b).unwrap();
        let slice = c.data.as_f32_slice().unwrap();
        
        assert_eq!(slice, &[6.0, 2.0, 60.0]);
    }

    #[test]
    fn test_cpu_backend_rejects_gpu_tensors() {
        let shape = Shape::new(vec![1]);
        
        let a = Tensor::new(
            Buffer::new_cpu_zeros(1, DType::I32), 
            shape.clone(), 
            Device::Cpu, 
            DType::I32, 
            false
        );
        let b = Tensor::new(
            Buffer::new_cpu_zeros(1, DType::I32), 
            shape, 
            Device::Cpu, 
            DType::I32, 
            false
        );

        let result = CpuBackend::add(&a, &b);
        assert!(result.is_err());
        
        if let Err(EtensorError::DTypeMismatch { .. }) = result {
            // Success: The backend protected itself from invalid memory layouts
        } else {
            panic!("Expected DTypeMismatch error from the CPU Kernel!");
        }
    }
}