use async_trait::async_trait;
use candle_core::{Device, Tensor, D};
use infernum_core::{DType, DeviceType, Result};
pub 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,
_ => DType::F32,
}
}
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, Clone, Copy)]
pub struct GpuCapabilities {
pub compute_major: u32,
pub compute_minor: u32,
pub total_memory: usize,
pub has_tensor_cores: bool,
pub has_bf16: bool,
pub has_fp8: bool,
}
impl GpuCapabilities {
pub fn compute_capability(&self) -> f32 {
self.compute_major as f32 + self.compute_minor as f32 / 10.0
}
pub fn supports_fp16_tensor_cores(&self) -> bool {
self.has_tensor_cores
}
pub fn supports_bf16_tensor_cores(&self) -> bool {
self.has_bf16
}
pub fn recommended_dtype(&self) -> DType {
if self.has_bf16 {
DType::BF16 } else if self.has_tensor_cores {
DType::F16 } else {
DType::F32 }
}
}
#[derive(Debug)]
pub struct CudaDevice {
device_id: usize,
candle_device: Device,
capabilities: GpuCapabilities,
}
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(),
})?;
let capabilities = Self::query_capabilities(device_id)?;
tracing::info!(
device_id = device_id,
compute = %format!("{}.{}", capabilities.compute_major, capabilities.compute_minor),
vram_gb = capabilities.total_memory / (1024 * 1024 * 1024),
tensor_cores = capabilities.has_tensor_cores,
bf16 = capabilities.has_bf16,
"CUDA device initialized"
);
Ok(Self {
device_id,
candle_device,
capabilities,
})
}
fn query_capabilities(device_id: usize) -> Result<GpuCapabilities> {
use cudarc::driver::CudaDevice as CudarcDevice;
let cuda_dev =
CudarcDevice::new(device_id).map_err(|e| infernum_core::Error::Backend {
backend: "cuda".to_string(),
message: format!("Failed to query CUDA device: {}", e),
})?;
let compute_major = cuda_dev
.attribute(cudarc::driver::sys::CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR)
.unwrap_or(7) as u32;
let compute_minor = cuda_dev
.attribute(cudarc::driver::sys::CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR)
.unwrap_or(0) as u32;
let total_memory = cuda_dev
.attribute(cudarc::driver::sys::CUdevice_attribute::CU_DEVICE_ATTRIBUTE_TOTAL_CONSTANT_MEMORY)
.map(|v| v as usize)
.unwrap_or(16 * 1024 * 1024 * 1024);
let total_memory = Self::estimate_vram(compute_major, compute_minor, total_memory);
let has_tensor_cores = compute_major >= 7;
let has_bf16 = compute_major >= 8;
let has_fp8 = compute_major > 8 || (compute_major == 8 && compute_minor >= 9);
Ok(GpuCapabilities {
compute_major,
compute_minor,
total_memory,
has_tensor_cores,
has_bf16,
has_fp8,
})
}
fn estimate_vram(major: u32, minor: u32, _hint: usize) -> usize {
match (major, minor) {
(8, 9) => 24 * 1024 * 1024 * 1024, (8, 6) => 12 * 1024 * 1024 * 1024, (8, 0) => 40 * 1024 * 1024 * 1024, (7, 5) => 8 * 1024 * 1024 * 1024, (7, 0) => 16 * 1024 * 1024 * 1024, _ => 8 * 1024 * 1024 * 1024, }
}
pub fn capabilities(&self) -> &GpuCapabilities {
&self.capabilities
}
pub fn recommended_dtype(&self) -> DType {
self.capabilities.recommended_dtype()
}
}
impl DeviceOps for CudaDevice {
fn device_type(&self) -> DeviceType {
DeviceType::Cuda {
device_id: self.device_id,
}
}
fn total_memory(&self) -> usize {
self.capabilities.total_memory
}
fn available_memory(&self) -> usize {
(self.capabilities.total_memory as f64 * 0.8) as usize
}
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 {
inner: Tensor,
shape_cache: Vec<usize>,
}
impl WebGpuTensor {
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 WebGpuTensor {
fn shape(&self) -> &[usize] {
&self.shape_cache
}
fn dtype(&self) -> DType {
from_candle_dtype(self.inner.dtype())
}
}
#[derive(Debug)]
pub struct WebGpuDevice {
max_buffer_size: usize,
total_memory: usize,
candle_device: Device,
}
impl Default for WebGpuDevice {
fn default() -> Self {
Self::new()
}
}
impl WebGpuDevice {
#[must_use]
pub fn new() -> Self {
Self {
max_buffer_size: 256 * 1024 * 1024,
total_memory: 4 * 1024 * 1024 * 1024,
candle_device: Device::Cpu,
}
}
pub fn with_limits(max_buffer_size: usize, total_memory: usize) -> Self {
Self {
max_buffer_size,
total_memory,
candle_device: Device::Cpu,
}
}
#[must_use]
pub fn max_buffer_size(&self) -> usize {
self.max_buffer_size
}
}
impl DeviceOps for WebGpuDevice {
fn device_type(&self) -> DeviceType {
DeviceType::WebGpu
}
fn total_memory(&self) -> usize {
self.total_memory
}
fn available_memory(&self) -> usize {
self.total_memory / 2
}
fn synchronize(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct WebGpuBackend {
device: WebGpuDevice,
}
impl Default for WebGpuBackend {
fn default() -> Self {
Self::new()
}
}
impl WebGpuBackend {
#[must_use]
pub fn new() -> Self {
Self {
device: WebGpuDevice::new(),
}
}
pub fn with_device(device: WebGpuDevice) -> Self {
Self { device }
}
#[must_use]
pub fn candle_device(&self) -> &Device {
&self.device.candle_device
}
fn map_err(e: candle_core::Error) -> infernum_core::Error {
infernum_core::Error::Backend {
backend: "webgpu".to_string(),
message: e.to_string(),
}
}
fn check_buffer_size(&self, shape: &[usize], dtype: DType) -> Result<()> {
let elem_size = match dtype {
DType::F32 | DType::I8 => 4,
DType::F16 | DType::BF16 => 2,
DType::I4 => 1,
};
let total_size: usize = shape.iter().product::<usize>() * elem_size;
if total_size > self.device.max_buffer_size {
return Err(infernum_core::Error::Backend {
backend: "webgpu".to_string(),
message: format!(
"Tensor size {} exceeds WebGPU max buffer size {}",
total_size, self.device.max_buffer_size
),
});
}
Ok(())
}
}
#[async_trait]
impl ComputeBackend for WebGpuBackend {
type Tensor = WebGpuTensor;
type Device = WebGpuDevice;
fn device(&self) -> &Self::Device {
&self.device
}
fn allocate(&self, shape: &[usize], dtype: DType) -> Result<Self::Tensor> {
self.check_buffer_size(shape, dtype)?;
let candle_dtype = to_candle_dtype(dtype);
let tensor = Tensor::zeros(shape, candle_dtype, &self.device.candle_device)
.map_err(Self::map_err)?;
Ok(WebGpuTensor::new(tensor))
}
fn from_slice(&self, data: &[f32], shape: &[usize]) -> Result<Self::Tensor> {
self.check_buffer_size(shape, DType::F32)?;
let tensor = Tensor::from_slice(data, shape, &self.device.candle_device)
.map_err(Self::map_err)?;
Ok(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::new(result))
}
fn gelu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x.inner.gelu_erf().map_err(Self::map_err)?;
Ok(WebGpuTensor::new(result))
}
fn relu(&self, x: &Self::Tensor) -> Result<Self::Tensor> {
let result = x.inner.relu().map_err(Self::map_err)?;
Ok(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::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(WebGpuTensor::new(result))
}
fn to_device(&self, tensor: &Self::Tensor) -> Result<Self::Tensor> {
Ok(WebGpuTensor::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(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());
}
}