use crate::utils::Result;
use rayon;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BackendType {
Cpu,
#[cfg(feature = "cuda")]
Cuda,
#[cfg(feature = "opencl")]
OpenCl,
}
impl BackendType {
pub fn detect() -> Self {
#[cfg(feature = "cuda")]
{
if CudaBackend::is_available() {
return BackendType::Cuda;
}
}
#[cfg(feature = "opencl")]
{
if OpenClBackend::is_available() {
return BackendType::OpenCl;
}
}
BackendType::Cpu
}
pub fn name(&self) -> &str {
match self {
BackendType::Cpu => "CPU",
#[cfg(feature = "cuda")]
BackendType::Cuda => "CUDA",
#[cfg(feature = "opencl")]
BackendType::OpenCl => "OpenCL",
}
}
}
pub trait ComputeBackend: Send + Sync {
fn backend_type(&self) -> BackendType;
fn device_name(&self) -> &str;
fn total_memory(&self) -> usize;
fn matmul(&self, a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Result<Vec<f32>>;
fn conv2d(
&self,
input: &[f32],
kernel: &[f32],
bias: Option<&[f32]>,
in_h: usize,
in_w: usize,
in_c: usize,
out_c: usize,
k_h: usize,
k_w: usize,
stride: usize,
padding: usize,
) -> Result<Vec<f32>>;
fn add(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>>;
fn mul(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>>;
fn relu(&self, x: &[f32]) -> Result<Vec<f32>>;
fn sigmoid(&self, x: &[f32]) -> Result<Vec<f32>>;
fn tanh(&self, x: &[f32]) -> Result<Vec<f32>>;
fn softmax(&self, x: &[f32], rows: usize, cols: usize) -> Result<Vec<f32>>;
fn add_bias(&self, x: &[f32], bias: &[f32], rows: usize, cols: usize) -> Result<Vec<f32>>;
}
pub struct CpuBackend {
num_threads: usize,
}
impl CpuBackend {
pub fn new() -> Self {
Self {
num_threads: rayon::current_num_threads(),
}
}
}
impl Default for CpuBackend {
fn default() -> Self {
Self::new()
}
}
impl ComputeBackend for CpuBackend {
fn backend_type(&self) -> BackendType {
BackendType::Cpu
}
fn device_name(&self) -> &str {
"CPU (rayon)"
}
fn total_memory(&self) -> usize {
0 }
fn matmul(&self, a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Result<Vec<f32>> {
let mut c = vec![0.0f32; m * n];
for i in 0..m {
for j in 0..n {
let mut sum = 0.0f32;
for p in 0..k {
sum += a[i * k + p] * b[p * n + j];
}
c[i * n + j] = sum;
}
}
Ok(c)
}
fn conv2d(
&self,
input: &[f32],
kernel: &[f32],
bias: Option<&[f32]>,
in_h: usize,
in_w: usize,
in_c: usize,
out_c: usize,
k_h: usize,
k_w: usize,
stride: usize,
padding: usize,
) -> Result<Vec<f32>> {
let out_h = (in_h + 2 * padding - k_h) / stride + 1;
let out_w = (in_w + 2 * padding - k_w) / stride + 1;
let mut output = vec![0.0f32; out_c * out_h * out_w];
for oc in 0..out_c {
for oh in 0..out_h {
for ow in 0..out_w {
let mut sum = 0.0f32;
for ic in 0..in_c {
for kh in 0..k_h {
for kw in 0..k_w {
let ih = (oh * stride + kh) as isize - padding as isize;
let iw = (ow * stride + kw) as isize - padding as isize;
if ih >= 0 && ih < in_h as isize && iw >= 0 && iw < in_w as isize {
let input_idx = (ic * in_h + ih as usize) * in_w + iw as usize;
let kernel_idx = ((oc * in_c + ic) * k_h + kh) * k_w + kw;
sum += input[input_idx] * kernel[kernel_idx];
}
}
}
}
if let Some(b) = bias {
sum += b[oc];
}
output[(oc * out_h + oh) * out_w + ow] = sum;
}
}
}
Ok(output)
}
fn add(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>> {
Ok(a.iter().zip(b.iter()).map(|(x, y)| x + y).collect())
}
fn mul(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>> {
Ok(a.iter().zip(b.iter()).map(|(x, y)| x * y).collect())
}
fn relu(&self, x: &[f32]) -> Result<Vec<f32>> {
Ok(x.iter().map(|&v| if v > 0.0 { v } else { 0.0 }).collect())
}
fn sigmoid(&self, x: &[f32]) -> Result<Vec<f32>> {
Ok(x.iter().map(|&v| 1.0 / (1.0 + (-v).exp())).collect())
}
fn tanh(&self, x: &[f32]) -> Result<Vec<f32>> {
Ok(x.iter().map(|&v| v.tanh()).collect())
}
fn softmax(&self, x: &[f32], rows: usize, cols: usize) -> Result<Vec<f32>> {
let mut result = vec![0.0f32; rows * cols];
for r in 0..rows {
let start = r * cols;
let end = start + cols;
let max_val = x[start..end]
.iter()
.fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let sum: f32 = x[start..end].iter().map(|&v| (v - max_val).exp()).sum();
for (c, val) in result[start..end].iter_mut().enumerate() {
*val = (x[start + c] - max_val).exp() / sum;
}
}
Ok(result)
}
fn add_bias(&self, x: &[f32], bias: &[f32], rows: usize, cols: usize) -> Result<Vec<f32>> {
let mut result = x.to_vec();
for r in 0..rows {
for c in 0..cols {
result[r * cols + c] += bias[c];
}
}
Ok(result)
}
}
#[cfg(feature = "cuda")]
pub struct CudaBackend {
device_name: String,
total_memory: usize,
}
#[cfg(feature = "cuda")]
impl CudaBackend {
pub fn is_available() -> bool {
std::env::var("CUDA_VISIBLE_DEVICES").is_ok() || cfg!(target_os = "linux")
}
pub fn new() -> Result<Self> {
Ok(Self {
device_name: "NVIDIA CUDA GPU".to_string(),
total_memory: 0,
})
}
}
#[cfg(feature = "cuda")]
impl ComputeBackend for CudaBackend {
fn backend_type(&self) -> BackendType {
BackendType::Cuda
}
fn device_name(&self) -> &str {
&self.device_name
}
fn total_memory(&self) -> usize {
self.total_memory
}
fn matmul(&self, a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Result<Vec<f32>> {
CpuBackend::new().matmul(a, b, m, k, n)
}
fn conv2d(
&self,
input: &[f32],
kernel: &[f32],
bias: Option<&[f32]>,
in_h: usize,
in_w: usize,
in_c: usize,
out_c: usize,
k_h: usize,
k_w: usize,
stride: usize,
padding: usize,
) -> Result<Vec<f32>> {
CpuBackend::new().conv2d(
input, kernel, bias, in_h, in_w, in_c, out_c, k_h, k_w, stride, padding,
)
}
fn add(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().add(a, b)
}
fn mul(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().mul(a, b)
}
fn relu(&self, x: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().relu(x)
}
fn sigmoid(&self, x: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().sigmoid(x)
}
fn tanh(&self, x: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().tanh(x)
}
fn softmax(&self, x: &[f32], rows: usize, cols: usize) -> Result<Vec<f32>> {
CpuBackend::new().softmax(x, rows, cols)
}
fn add_bias(&self, x: &[f32], bias: &[f32], rows: usize, cols: usize) -> Result<Vec<f32>> {
CpuBackend::new().add_bias(x, bias, rows, cols)
}
}
#[cfg(feature = "opencl")]
pub struct OpenClBackend {
device_name: String,
total_memory: usize,
}
#[cfg(feature = "opencl")]
impl OpenClBackend {
pub fn is_available() -> bool {
cfg!(any(
target_os = "macos",
target_os = "linux",
target_os = "windows"
))
}
pub fn new() -> Result<Self> {
Ok(Self {
device_name: "OpenCL GPU".to_string(),
total_memory: 0,
})
}
}
#[cfg(feature = "opencl")]
impl ComputeBackend for OpenClBackend {
fn backend_type(&self) -> BackendType {
BackendType::OpenCl
}
fn device_name(&self) -> &str {
&self.device_name
}
fn total_memory(&self) -> usize {
self.total_memory
}
fn matmul(&self, a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Result<Vec<f32>> {
CpuBackend::new().matmul(a, b, m, k, n)
}
fn conv2d(
&self,
input: &[f32],
kernel: &[f32],
bias: Option<&[f32]>,
in_h: usize,
in_w: usize,
in_c: usize,
out_c: usize,
k_h: usize,
k_w: usize,
stride: usize,
padding: usize,
) -> Result<Vec<f32>> {
CpuBackend::new().conv2d(
input, kernel, bias, in_h, in_w, in_c, out_c, k_h, k_w, stride, padding,
)
}
fn add(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().add(a, b)
}
fn mul(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().mul(a, b)
}
fn relu(&self, x: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().relu(x)
}
fn sigmoid(&self, x: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().sigmoid(x)
}
fn tanh(&self, x: &[f32]) -> Result<Vec<f32>> {
CpuBackend::new().tanh(x)
}
fn softmax(&self, x: &[f32], rows: usize, cols: usize) -> Result<Vec<f32>> {
CpuBackend::new().softmax(x, rows, cols)
}
fn add_bias(&self, x: &[f32], bias: &[f32], rows: usize, cols: usize) -> Result<Vec<f32>> {
CpuBackend::new().add_bias(x, bias, rows, cols)
}
}
pub fn create_backend(backend_type: BackendType) -> Result<Box<dyn ComputeBackend>> {
match backend_type {
BackendType::Cpu => Ok(Box::new(CpuBackend::new())),
#[cfg(feature = "cuda")]
BackendType::Cuda => Ok(Box::new(CudaBackend::new()?)),
#[cfg(feature = "opencl")]
BackendType::OpenCl => Ok(Box::new(OpenClBackend::new()?)),
}
}
pub fn create_auto_backend() -> Result<Box<dyn ComputeBackend>> {
create_backend(BackendType::detect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cpu_matmul() {
let backend = CpuBackend::new();
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let b = vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0]; let c = backend.matmul(&a, &b, 2, 3, 2).unwrap();
assert!((c[0] - 58.0).abs() < 0.01);
assert!((c[1] - 64.0).abs() < 0.01);
assert!((c[2] - 139.0).abs() < 0.01);
assert!((c[3] - 154.0).abs() < 0.01);
}
#[test]
fn test_cpu_relu() {
let backend = CpuBackend::new();
let x = vec![-1.0, 0.0, 2.0, -0.5];
let y = backend.relu(&x).unwrap();
assert_eq!(y, vec![0.0, 0.0, 2.0, 0.0]);
}
#[test]
fn test_cpu_sigmoid() {
let backend = CpuBackend::new();
let x = vec![0.0f32];
let y = backend.sigmoid(&x).unwrap();
assert!((y[0] - 0.5).abs() < 0.01);
}
#[test]
fn test_cpu_softmax() {
let backend = CpuBackend::new();
let x = vec![1.0, 2.0, 3.0];
let y = backend.softmax(&x, 1, 3).unwrap();
let sum: f32 = y.iter().sum();
assert!((sum - 1.0).abs() < 0.01);
assert!(y[0] < y[1] && y[1] < y[2]);
}
#[test]
fn test_backend_detect() {
let backend_type = BackendType::detect();
let is_valid = matches!(backend_type, BackendType::Cpu);
#[cfg(feature = "cuda")]
let is_valid = is_valid || matches!(backend_type, BackendType::Cuda);
#[cfg(feature = "opencl")]
let is_valid = is_valid || matches!(backend_type, BackendType::OpenCl);
assert!(is_valid);
}
}