use async_trait::async_trait;
use candle_core::{Device, Tensor, D};
use infernum_core::{DType, DeviceType, Result};
fn to_candle_dtype(dtype: DType) -> candle_core::DType {
match dtype {
DType::F32 => candle_core::DType::F32,
DType::F16 => candle_core::DType::F16,
DType::BF16 => candle_core::DType::BF16,
DType::I8 => candle_core::DType::I64, DType::I4 => candle_core::DType::I64, }
}
fn from_candle_dtype(dtype: candle_core::DType) -> DType {
match dtype {
candle_core::DType::F32 => DType::F32,
candle_core::DType::F16 => DType::F16,
candle_core::DType::BF16 => DType::BF16,
candle_core::DType::F64 => DType::F32, candle_core::DType::U8 | candle_core::DType::U32 | candle_core::DType::I64 => DType::I8,
}
}
pub trait TensorOps: Send + Sync {
fn shape(&self) -> &[usize];
fn dtype(&self) -> DType;
fn numel(&self) -> usize {
self.shape().iter().product()
}
}
pub trait DeviceOps: Send + Sync {
fn device_type(&self) -> DeviceType;
fn total_memory(&self) -> usize;
fn available_memory(&self) -> usize;
fn synchronize(&self) -> Result<()>;
}
#[async_trait]
pub trait ComputeBackend: Send + Sync {
type Tensor: TensorOps;
type Device: DeviceOps;
fn device(&self) -> &Self::Device;
fn allocate(&self, shape: &[usize], dtype: DType) -> Result<Self::Tensor>;
fn from_slice(&self, data: &[f32], shape: &[usize]) -> Result<Self::Tensor>;
fn matmul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor>;
fn batch_matmul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor>;
fn attention(
&self,
q: &Self::Tensor,
k: &Self::Tensor,
v: &Self::Tensor,
mask: Option<&Self::Tensor>,
scale: Option<f32>,
) -> Result<Self::Tensor>;
fn rms_norm(&self, x: &Self::Tensor, weight: &Self::Tensor, eps: f32) -> Result<Self::Tensor>;
fn layer_norm(
&self,
x: &Self::Tensor,
weight: &Self::Tensor,
bias: Option<&Self::Tensor>,
eps: f32,
) -> Result<Self::Tensor>;
fn silu(&self, x: &Self::Tensor) -> Result<Self::Tensor>;
fn gelu(&self, x: &Self::Tensor) -> Result<Self::Tensor>;
fn relu(&self, x: &Self::Tensor) -> Result<Self::Tensor>;
fn softmax(&self, x: &Self::Tensor, dim: i32) -> Result<Self::Tensor>;
fn add(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor>;
fn mul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor>;
fn transpose(&self, x: &Self::Tensor) -> Result<Self::Tensor>;
fn reshape(&self, x: &Self::Tensor, shape: &[usize]) -> Result<Self::Tensor>;
fn to_device(&self, tensor: &Self::Tensor) -> Result<Self::Tensor>;
fn to_cpu(&self, tensor: &Self::Tensor) -> Result<Vec<f32>>;
}
pub mod cpu {
use super::*;
use std::sync::Arc;
use sysinfo::System;
#[derive(Debug, Clone)]
pub struct CpuTensor {
inner: Tensor,
shape_cache: Vec<usize>,
}
impl CpuTensor {
pub fn new(tensor: Tensor) -> Self {
let shape_cache = tensor.dims().to_vec();
Self {
inner: tensor,
shape_cache,
}
}
#[must_use]
pub fn inner(&self) -> &Tensor {
&self.inner
}
#[must_use]
pub fn into_inner(self) -> Tensor {
self.inner
}
}
impl TensorOps for CpuTensor {
fn shape(&self) -> &[usize] {
&self.shape_cache
}
fn dtype(&self) -> DType {
from_candle_dtype(self.inner.dtype())
}
}
#[derive(Debug)]
pub struct CpuDevice {
system: Arc<parking_lot::Mutex<System>>,
}
impl Default for CpuDevice {
fn default() -> Self {
Self::new()
}
}
impl CpuDevice {
#[must_use]
pub fn new() -> Self {
let mut system = System::new_all();
system.refresh_memory();
Self {
system: Arc::new(parking_lot::Mutex::new(system)),
}
}
pub fn refresh(&self) {
self.system.lock().refresh_memory();
}
}
impl DeviceOps for CpuDevice {
fn device_type(&self) -> DeviceType {
DeviceType::Cpu
}
fn total_memory(&self) -> usize {
let system = self.system.lock();
system.total_memory() as usize
}
fn available_memory(&self) -> usize {
let mut system = self.system.lock();
system.refresh_memory();
system.available_memory() as usize
}
fn synchronize(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct CpuBackend {
device: CpuDevice,
candle_device: Device,
}
impl Default for CpuBackend {
fn default() -> Self {
Self::new()
}
}
impl CpuBackend {
#[must_use]
pub fn new() -> Self {
Self {
device: CpuDevice::new(),
candle_device: Device::Cpu,
}
}
#[must_use]
pub fn candle_device(&self) -> &Device {
&self.candle_device
}
fn map_err(e: candle_core::Error) -> infernum_core::Error {
infernum_core::Error::Backend {
backend: "cpu".to_string(),
message: e.to_string(),
}
}
}
#[async_trait]
impl ComputeBackend for CpuBackend {
type Tensor = CpuTensor;
type Device = CpuDevice;
fn device(&self) -> &Self::Device {
&self.device
}
fn allocate(&self, shape: &[usize], dtype: DType) -> Result<Self::Tensor> {
let candle_dtype = to_candle_dtype(dtype);
let tensor =
Tensor::zeros(shape, candle_dtype, &self.candle_device).map_err(Self::map_err)?;
Ok(CpuTensor::new(tensor))
}
fn from_slice(&self, data: &[f32], shape: &[usize]) -> Result<Self::Tensor> {
let tensor =
Tensor::from_slice(data, shape, &self.candle_device).map_err(Self::map_err)?;
Ok(CpuTensor::new(tensor))
}
fn matmul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.matmul(&b.inner).map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn batch_matmul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.matmul(&b.inner).map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn attention(
&self,
q: &Self::Tensor,
k: &Self::Tensor,
v: &Self::Tensor,
mask: Option<&Self::Tensor>,
scale: Option<f32>,
) -> Result<Self::Tensor> {
let head_dim = q.inner.dim(D::Minus1).map_err(Self::map_err)?;
let scale = scale.unwrap_or(1.0 / (head_dim as f32).sqrt());
let k_t = k
.inner
.transpose(D::Minus2, D::Minus1)
.map_err(Self::map_err)?;
let scores = q.inner.matmul(&k_t).map_err(Self::map_err)?;
let scores = (scores * scale as f64).map_err(Self::map_err)?;
let scores = match mask {
Some(m) => scores.broadcast_add(&m.inner).map_err(Self::map_err)?,
None => scores,
};
let attn_weights = candle_nn::ops::softmax_last_dim(&scores).map_err(Self::map_err)?;
let output = attn_weights.matmul(&v.inner).map_err(Self::map_err)?;
Ok(CpuTensor::new(output))
}
fn rms_norm(
&self,
x: &Self::Tensor,
weight: &Self::Tensor,
eps: f32,
) -> Result<Self::Tensor> {
let dtype = x.inner.dtype();
let x_f32 = x
.inner
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?;
let variance = x_f32
.sqr()
.map_err(Self::map_err)?
.mean_keepdim(D::Minus1)
.map_err(Self::map_err)?;
let x_normed = x_f32
.broadcast_div(
&(variance + eps as f64)
.map_err(Self::map_err)?
.sqrt()
.map_err(Self::map_err)?,
)
.map_err(Self::map_err)?;
let result = x_normed
.to_dtype(dtype)
.map_err(Self::map_err)?
.broadcast_mul(&weight.inner)
.map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn layer_norm(
&self,
x: &Self::Tensor,
weight: &Self::Tensor,
bias: Option<&Self::Tensor>,
eps: f32,
) -> Result<Self::Tensor> {
let dtype = x.inner.dtype();
let x_f32 = x
.inner
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?;
let mean = x_f32.mean_keepdim(D::Minus1).map_err(Self::map_err)?;
let x_centered = x_f32.broadcast_sub(&mean).map_err(Self::map_err)?;
let variance = x_centered
.sqr()
.map_err(Self::map_err)?
.mean_keepdim(D::Minus1)
.map_err(Self::map_err)?;
let x_normed = x_centered
.broadcast_div(
&(variance + eps as f64)
.map_err(Self::map_err)?
.sqrt()
.map_err(Self::map_err)?,
)
.map_err(Self::map_err)?;
let mut result = x_normed
.to_dtype(dtype)
.map_err(Self::map_err)?
.broadcast_mul(&weight.inner)
.map_err(Self::map_err)?;
if let Some(b) = bias {
result = result.broadcast_add(&b.inner).map_err(Self::map_err)?;
}
Ok(CpuTensor::new(result))
}
fn silu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = candle_nn::ops::silu(&x.inner).map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn gelu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x.inner.gelu_erf().map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn relu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x.inner.relu().map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn softmax(&self, x: &Self::Tensor, dim: i32) -> Result<Self::Tensor> {
let result = if dim == -1 {
candle_nn::ops::softmax_last_dim(&x.inner).map_err(Self::map_err)?
} else {
candle_nn::ops::softmax(&x.inner, dim as usize).map_err(Self::map_err)?
};
Ok(CpuTensor::new(result))
}
fn add(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.broadcast_add(&b.inner).map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn mul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.broadcast_mul(&b.inner).map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn transpose(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x
.inner
.transpose(D::Minus2, D::Minus1)
.map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn reshape(&self, x: &Self::Tensor, shape: &[usize]) -> Result<Self::Tensor> {
let result = x.inner.reshape(shape).map_err(Self::map_err)?;
Ok(CpuTensor::new(result))
}
fn to_device(&self, tensor: &Self::Tensor) -> Result<Self::Tensor> {
Ok(CpuTensor::new(tensor.inner.clone()))
}
fn to_cpu(&self, tensor: &Self::Tensor) -> Result<Vec<f32>> {
let flat = tensor.inner.flatten_all().map_err(Self::map_err)?;
let data: Vec<f32> = flat
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?
.to_vec1()
.map_err(Self::map_err)?;
Ok(data)
}
}
}
#[cfg(feature = "cuda")]
pub mod cuda {
use super::*;
#[derive(Debug, Clone)]
pub struct CudaTensor {
inner: Tensor,
shape_cache: Vec<usize>,
}
impl CudaTensor {
pub fn new(tensor: Tensor) -> Self {
let shape_cache = tensor.dims().to_vec();
Self {
inner: tensor,
shape_cache,
}
}
#[must_use]
pub fn inner(&self) -> &Tensor {
&self.inner
}
}
impl TensorOps for CudaTensor {
fn shape(&self) -> &[usize] {
&self.shape_cache
}
fn dtype(&self) -> DType {
from_candle_dtype(self.inner.dtype())
}
}
#[derive(Debug)]
pub struct CudaDevice {
device_id: usize,
candle_device: Device,
}
impl CudaDevice {
pub fn new(device_id: usize) -> Result<Self> {
let candle_device =
Device::new_cuda(device_id).map_err(|e| infernum_core::Error::Backend {
backend: "cuda".to_string(),
message: e.to_string(),
})?;
Ok(Self {
device_id,
candle_device,
})
}
}
impl DeviceOps for CudaDevice {
fn device_type(&self) -> DeviceType {
DeviceType::Cuda {
device_id: self.device_id,
}
}
fn total_memory(&self) -> usize {
16 * 1024 * 1024 * 1024 }
fn available_memory(&self) -> usize {
8 * 1024 * 1024 * 1024 }
fn synchronize(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct CudaBackend {
device: CudaDevice,
}
impl CudaBackend {
pub fn new(device_id: usize) -> Result<Self> {
let device = CudaDevice::new(device_id)?;
Ok(Self { device })
}
fn map_err(e: candle_core::Error) -> infernum_core::Error {
infernum_core::Error::Backend {
backend: "cuda".to_string(),
message: e.to_string(),
}
}
}
#[async_trait]
impl ComputeBackend for CudaBackend {
type Tensor = CudaTensor;
type Device = CudaDevice;
fn device(&self) -> &Self::Device {
&self.device
}
fn allocate(&self, shape: &[usize], dtype: DType) -> Result<Self::Tensor> {
let candle_dtype = to_candle_dtype(dtype);
let tensor = Tensor::zeros(shape, candle_dtype, &self.device.candle_device)
.map_err(Self::map_err)?;
Ok(CudaTensor::new(tensor))
}
fn from_slice(&self, data: &[f32], shape: &[usize]) -> Result<Self::Tensor> {
let cpu_tensor =
Tensor::from_slice(data, shape, &Device::Cpu).map_err(Self::map_err)?;
let tensor = cpu_tensor
.to_device(&self.device.candle_device)
.map_err(Self::map_err)?;
Ok(CudaTensor::new(tensor))
}
fn matmul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.matmul(&b.inner).map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn batch_matmul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.matmul(&b.inner).map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn attention(
&self,
q: &Self::Tensor,
k: &Self::Tensor,
v: &Self::Tensor,
mask: Option<&Self::Tensor>,
scale: Option<f32>,
) -> Result<Self::Tensor> {
let head_dim = q.inner.dim(D::Minus1).map_err(Self::map_err)?;
let scale = scale.unwrap_or(1.0 / (head_dim as f32).sqrt());
let k_t = k
.inner
.transpose(D::Minus2, D::Minus1)
.map_err(Self::map_err)?;
let scores = q.inner.matmul(&k_t).map_err(Self::map_err)?;
let scores = (scores * scale as f64).map_err(Self::map_err)?;
let scores = match mask {
Some(m) => scores.broadcast_add(&m.inner).map_err(Self::map_err)?,
None => scores,
};
let attn_weights = candle_nn::ops::softmax_last_dim(&scores).map_err(Self::map_err)?;
let output = attn_weights.matmul(&v.inner).map_err(Self::map_err)?;
Ok(CudaTensor::new(output))
}
fn rms_norm(
&self,
x: &Self::Tensor,
weight: &Self::Tensor,
eps: f32,
) -> Result<Self::Tensor> {
let dtype = x.inner.dtype();
let x_f32 = x
.inner
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?;
let variance = x_f32
.sqr()
.map_err(Self::map_err)?
.mean_keepdim(D::Minus1)
.map_err(Self::map_err)?;
let x_normed = x_f32
.broadcast_div(
&(variance + eps as f64)
.map_err(Self::map_err)?
.sqrt()
.map_err(Self::map_err)?,
)
.map_err(Self::map_err)?;
let result = x_normed
.to_dtype(dtype)
.map_err(Self::map_err)?
.broadcast_mul(&weight.inner)
.map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn layer_norm(
&self,
x: &Self::Tensor,
weight: &Self::Tensor,
bias: Option<&Self::Tensor>,
eps: f32,
) -> Result<Self::Tensor> {
let dtype = x.inner.dtype();
let x_f32 = x
.inner
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?;
let mean = x_f32.mean_keepdim(D::Minus1).map_err(Self::map_err)?;
let x_centered = x_f32.broadcast_sub(&mean).map_err(Self::map_err)?;
let variance = x_centered
.sqr()
.map_err(Self::map_err)?
.mean_keepdim(D::Minus1)
.map_err(Self::map_err)?;
let x_normed = x_centered
.broadcast_div(
&(variance + eps as f64)
.map_err(Self::map_err)?
.sqrt()
.map_err(Self::map_err)?,
)
.map_err(Self::map_err)?;
let mut result = x_normed
.to_dtype(dtype)
.map_err(Self::map_err)?
.broadcast_mul(&weight.inner)
.map_err(Self::map_err)?;
if let Some(b) = bias {
result = result.broadcast_add(&b.inner).map_err(Self::map_err)?;
}
Ok(CudaTensor::new(result))
}
fn silu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = candle_nn::ops::silu(&x.inner).map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn gelu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x.inner.gelu_erf().map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn relu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x.inner.relu().map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn softmax(&self, x: &Self::Tensor, dim: i32) -> Result<Self::Tensor> {
let result = if dim == -1 {
candle_nn::ops::softmax_last_dim(&x.inner).map_err(Self::map_err)?
} else {
candle_nn::ops::softmax(&x.inner, dim as usize).map_err(Self::map_err)?
};
Ok(CudaTensor::new(result))
}
fn add(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.broadcast_add(&b.inner).map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn mul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.broadcast_mul(&b.inner).map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn transpose(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x
.inner
.transpose(D::Minus2, D::Minus1)
.map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn reshape(&self, x: &Self::Tensor, shape: &[usize]) -> Result<Self::Tensor> {
let result = x.inner.reshape(shape).map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn to_device(&self, tensor: &Self::Tensor) -> Result<Self::Tensor> {
let result = tensor
.inner
.to_device(&self.device.candle_device)
.map_err(Self::map_err)?;
Ok(CudaTensor::new(result))
}
fn to_cpu(&self, tensor: &Self::Tensor) -> Result<Vec<f32>> {
let cpu_tensor = tensor
.inner
.to_device(&Device::Cpu)
.map_err(Self::map_err)?;
let flat = cpu_tensor.flatten_all().map_err(Self::map_err)?;
let data: Vec<f32> = flat
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?
.to_vec1()
.map_err(Self::map_err)?;
Ok(data)
}
}
}
#[cfg(feature = "metal")]
pub mod metal {
use super::*;
#[derive(Debug, Clone)]
pub struct MetalTensor {
inner: Tensor,
shape_cache: Vec<usize>,
}
impl MetalTensor {
pub fn new(tensor: Tensor) -> Self {
let shape_cache = tensor.dims().to_vec();
Self {
inner: tensor,
shape_cache,
}
}
#[must_use]
pub fn inner(&self) -> &Tensor {
&self.inner
}
}
impl TensorOps for MetalTensor {
fn shape(&self) -> &[usize] {
&self.shape_cache
}
fn dtype(&self) -> DType {
from_candle_dtype(self.inner.dtype())
}
}
#[derive(Debug)]
pub struct MetalDevice {
device_id: usize,
candle_device: Device,
}
impl MetalDevice {
pub fn new(device_id: usize) -> Result<Self> {
let candle_device =
Device::new_metal(device_id).map_err(|e| infernum_core::Error::Backend {
backend: "metal".to_string(),
message: e.to_string(),
})?;
Ok(Self {
device_id,
candle_device,
})
}
}
impl DeviceOps for MetalDevice {
fn device_type(&self) -> DeviceType {
DeviceType::Metal {
device_id: self.device_id,
}
}
fn total_memory(&self) -> usize {
16 * 1024 * 1024 * 1024 }
fn available_memory(&self) -> usize {
8 * 1024 * 1024 * 1024 }
fn synchronize(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct MetalBackend {
device: MetalDevice,
}
impl MetalBackend {
pub fn new(device_id: usize) -> Result<Self> {
let device = MetalDevice::new(device_id)?;
Ok(Self { device })
}
fn map_err(e: candle_core::Error) -> infernum_core::Error {
infernum_core::Error::Backend {
backend: "metal".to_string(),
message: e.to_string(),
}
}
}
#[async_trait]
impl ComputeBackend for MetalBackend {
type Tensor = MetalTensor;
type Device = MetalDevice;
fn device(&self) -> &Self::Device {
&self.device
}
fn allocate(&self, shape: &[usize], dtype: DType) -> Result<Self::Tensor> {
let candle_dtype = to_candle_dtype(dtype);
let tensor = Tensor::zeros(shape, candle_dtype, &self.device.candle_device)
.map_err(Self::map_err)?;
Ok(MetalTensor::new(tensor))
}
fn from_slice(&self, data: &[f32], shape: &[usize]) -> Result<Self::Tensor> {
let cpu_tensor =
Tensor::from_slice(data, shape, &Device::Cpu).map_err(Self::map_err)?;
let tensor = cpu_tensor
.to_device(&self.device.candle_device)
.map_err(Self::map_err)?;
Ok(MetalTensor::new(tensor))
}
fn matmul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.matmul(&b.inner).map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn batch_matmul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.matmul(&b.inner).map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn attention(
&self,
q: &Self::Tensor,
k: &Self::Tensor,
v: &Self::Tensor,
mask: Option<&Self::Tensor>,
scale: Option<f32>,
) -> Result<Self::Tensor> {
let head_dim = q.inner.dim(D::Minus1).map_err(Self::map_err)?;
let scale = scale.unwrap_or(1.0 / (head_dim as f32).sqrt());
let k_t = k
.inner
.transpose(D::Minus2, D::Minus1)
.map_err(Self::map_err)?;
let scores = q.inner.matmul(&k_t).map_err(Self::map_err)?;
let scores = (scores * scale as f64).map_err(Self::map_err)?;
let scores = match mask {
Some(m) => scores.broadcast_add(&m.inner).map_err(Self::map_err)?,
None => scores,
};
let attn_weights = candle_nn::ops::softmax_last_dim(&scores).map_err(Self::map_err)?;
let output = attn_weights.matmul(&v.inner).map_err(Self::map_err)?;
Ok(MetalTensor::new(output))
}
fn rms_norm(
&self,
x: &Self::Tensor,
weight: &Self::Tensor,
eps: f32,
) -> Result<Self::Tensor> {
let dtype = x.inner.dtype();
let x_f32 = x
.inner
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?;
let variance = x_f32
.sqr()
.map_err(Self::map_err)?
.mean_keepdim(D::Minus1)
.map_err(Self::map_err)?;
let x_normed = x_f32
.broadcast_div(
&(variance + eps as f64)
.map_err(Self::map_err)?
.sqrt()
.map_err(Self::map_err)?,
)
.map_err(Self::map_err)?;
let result = x_normed
.to_dtype(dtype)
.map_err(Self::map_err)?
.broadcast_mul(&weight.inner)
.map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn layer_norm(
&self,
x: &Self::Tensor,
weight: &Self::Tensor,
bias: Option<&Self::Tensor>,
eps: f32,
) -> Result<Self::Tensor> {
let dtype = x.inner.dtype();
let x_f32 = x
.inner
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?;
let mean = x_f32.mean_keepdim(D::Minus1).map_err(Self::map_err)?;
let x_centered = x_f32.broadcast_sub(&mean).map_err(Self::map_err)?;
let variance = x_centered
.sqr()
.map_err(Self::map_err)?
.mean_keepdim(D::Minus1)
.map_err(Self::map_err)?;
let x_normed = x_centered
.broadcast_div(
&(variance + eps as f64)
.map_err(Self::map_err)?
.sqrt()
.map_err(Self::map_err)?,
)
.map_err(Self::map_err)?;
let mut result = x_normed
.to_dtype(dtype)
.map_err(Self::map_err)?
.broadcast_mul(&weight.inner)
.map_err(Self::map_err)?;
if let Some(b) = bias {
result = result.broadcast_add(&b.inner).map_err(Self::map_err)?;
}
Ok(MetalTensor::new(result))
}
fn silu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = candle_nn::ops::silu(&x.inner).map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn gelu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x.inner.gelu_erf().map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn relu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x.inner.relu().map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn softmax(&self, x: &Self::Tensor, dim: i32) -> Result<Self::Tensor> {
let result = if dim == -1 {
candle_nn::ops::softmax_last_dim(&x.inner).map_err(Self::map_err)?
} else {
candle_nn::ops::softmax(&x.inner, dim as usize).map_err(Self::map_err)?
};
Ok(MetalTensor::new(result))
}
fn add(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.broadcast_add(&b.inner).map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn mul(&self, a: &Self::Tensor, b: &Self::Tensor) -> Result<Self::Tensor> {
let result = a.inner.broadcast_mul(&b.inner).map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn transpose(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x
.inner
.transpose(D::Minus2, D::Minus1)
.map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn reshape(&self, x: &Self::Tensor, shape: &[usize]) -> Result<Self::Tensor> {
let result = x.inner.reshape(shape).map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn to_device(&self, tensor: &Self::Tensor) -> Result<Self::Tensor> {
let result = tensor
.inner
.to_device(&self.device.candle_device)
.map_err(Self::map_err)?;
Ok(MetalTensor::new(result))
}
fn to_cpu(&self, tensor: &Self::Tensor) -> Result<Vec<f32>> {
let cpu_tensor = tensor
.inner
.to_device(&Device::Cpu)
.map_err(Self::map_err)?;
let flat = cpu_tensor.flatten_all().map_err(Self::map_err)?;
let data: Vec<f32> = flat
.to_dtype(candle_core::DType::F32)
.map_err(Self::map_err)?
.to_vec1()
.map_err(Self::map_err)?;
Ok(data)
}
}
}
pub mod webgpu {
use super::*;
#[derive(Debug, Clone)]
pub struct WebGpuTensor {
shape: Vec<usize>,
dtype: DType,
}
impl TensorOps for WebGpuTensor {
fn shape(&self) -> &[usize] {
&self.shape
}
fn dtype(&self) -> DType {
self.dtype
}
}
#[derive(Debug, Default)]
pub struct WebGpuDevice;
impl DeviceOps for WebGpuDevice {
fn device_type(&self) -> DeviceType {
DeviceType::WebGpu
}
fn total_memory(&self) -> usize {
4 * 1024 * 1024 * 1024 }
fn available_memory(&self) -> usize {
2 * 1024 * 1024 * 1024 }
fn synchronize(&self) -> Result<()> {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::cpu::*;
use super::*;
#[test]
fn test_cpu_backend_allocate() {
let backend = CpuBackend::new();
let tensor = backend.allocate(&[2, 3, 4], DType::F32).unwrap();
assert_eq!(tensor.shape(), &[2, 3, 4]);
assert_eq!(tensor.numel(), 24);
}
#[test]
fn test_cpu_backend_from_slice() {
let backend = CpuBackend::new();
let data: Vec<f32> = (0..12).map(|x| x as f32).collect();
let tensor = backend.from_slice(&data, &[3, 4]).unwrap();
assert_eq!(tensor.shape(), &[3, 4]);
let result = backend.to_cpu(&tensor).unwrap();
assert_eq!(result, data);
}
#[test]
fn test_cpu_backend_matmul() {
let backend = CpuBackend::new();
let a = backend
.from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3])
.unwrap();
let b = backend
.from_slice(
&[
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
&[3, 4],
)
.unwrap();
let c = backend.matmul(&a, &b).unwrap();
assert_eq!(c.shape(), &[2, 4]);
let result = backend.to_cpu(&c).unwrap();
assert_eq!(
result,
vec![38.0, 44.0, 50.0, 56.0, 83.0, 98.0, 113.0, 128.0]
);
}
#[test]
fn test_cpu_backend_softmax() {
let backend = CpuBackend::new();
let x = backend.from_slice(&[1.0, 2.0, 3.0, 4.0], &[1, 4]).unwrap();
let y = backend.softmax(&x, -1).unwrap();
let result = backend.to_cpu(&y).unwrap();
let sum: f32 = result.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "Softmax should sum to 1");
}
#[test]
fn test_cpu_backend_silu() {
let backend = CpuBackend::new();
let x = backend.from_slice(&[0.0, 1.0, -1.0, 2.0], &[4]).unwrap();
let y = backend.silu(&x).unwrap();
let result = backend.to_cpu(&y).unwrap();
assert!((result[0] - 0.0).abs() < 1e-5);
assert!((result[1] - 0.7311).abs() < 1e-3);
}
#[test]
fn test_cpu_backend_rms_norm() {
let backend = CpuBackend::new();
let x = backend.from_slice(&[1.0, 2.0, 3.0, 4.0], &[1, 4]).unwrap();
let weight = backend.from_slice(&[1.0, 1.0, 1.0, 1.0], &[4]).unwrap();
let y = backend.rms_norm(&x, &weight, 1e-5).unwrap();
let result = backend.to_cpu(&y).unwrap();
assert!(result[0] > 0.0);
assert!(result[3] > result[0]); }
#[test]
fn test_cpu_device_memory() {
let device = CpuDevice::new();
assert!(device.total_memory() > 0);
assert!(device.available_memory() > 0);
assert!(device.available_memory() <= device.total_memory());
}
}