mod backend_kind;
mod capabilities;
mod cpu;
mod error;
mod null;
mod ops;
mod precision;
mod registry;
use std::fmt;
pub use backend_kind::BackendKind;
pub use capabilities::{Capabilities, DeviceInfo, MemoryKind, TileShape, default_tile_for};
pub use cpu::CpuBackend;
pub use error::{BackendError, BackendResult};
pub use null::NullBackend;
pub use ops::{BackendTranspose, BinaryOp, MixedPrecision, ReduceOp, UnaryOp};
pub use precision::{round_to_bf16, round_to_f16};
pub use registry::{BackendEntry, BackendRegistry, OpClass, SelectionRequest};
pub trait ComputeBackend: Send + Sync + fmt::Debug {
fn name(&self) -> &str;
fn init(&mut self) -> BackendResult<()>;
fn is_initialized(&self) -> bool;
fn capabilities(&self) -> Capabilities {
Capabilities::default()
}
fn available_devices(&self) -> BackendResult<Vec<DeviceInfo>> {
Ok(Vec::new())
}
fn recommended_tile_for(&self, m: usize, n: usize, k: usize) -> TileShape {
default_tile_for(m, n, k, &self.capabilities())
}
#[allow(clippy::too_many_arguments)]
fn gemm(
&self,
trans_a: BackendTranspose,
trans_b: BackendTranspose,
m: usize,
n: usize,
k: usize,
alpha: f64,
a_ptr: u64,
lda: usize,
b_ptr: u64,
ldb: usize,
beta: f64,
c_ptr: u64,
ldc: usize,
) -> BackendResult<()>;
#[allow(clippy::too_many_arguments)]
fn conv2d_forward(
&self,
input_ptr: u64,
input_shape: &[usize],
filter_ptr: u64,
filter_shape: &[usize],
output_ptr: u64,
output_shape: &[usize],
stride: &[usize],
padding: &[usize],
) -> BackendResult<()>;
#[allow(clippy::too_many_arguments)]
fn attention(
&self,
q_ptr: u64,
k_ptr: u64,
v_ptr: u64,
o_ptr: u64,
batch: usize,
heads: usize,
seq_q: usize,
seq_kv: usize,
head_dim: usize,
scale: f64,
causal: bool,
) -> BackendResult<()>;
fn reduce(
&self,
op: ReduceOp,
input_ptr: u64,
output_ptr: u64,
shape: &[usize],
axis: usize,
) -> BackendResult<()>;
fn unary(&self, op: UnaryOp, input_ptr: u64, output_ptr: u64, n: usize) -> BackendResult<()>;
fn binary(
&self,
op: BinaryOp,
a_ptr: u64,
b_ptr: u64,
output_ptr: u64,
n: usize,
) -> BackendResult<()>;
#[allow(clippy::too_many_arguments)]
fn gemm_mixed_precision(
&self,
prec: MixedPrecision,
trans_a: BackendTranspose,
trans_b: BackendTranspose,
m: usize,
n: usize,
k: usize,
alpha: f32,
a_ptr: u64,
lda: usize,
b_ptr: u64,
ldb: usize,
beta: f32,
c_ptr: u64,
ldc: usize,
) -> BackendResult<()> {
let _ = (
prec, trans_a, trans_b, m, n, k, alpha, a_ptr, lda, b_ptr, ldb, beta, c_ptr, ldc,
);
Err(BackendError::Unsupported(
"gemm_mixed_precision not implemented by this backend".into(),
))
}
#[allow(clippy::too_many_arguments)]
fn conv2d_backward_data(
&self,
grad_output_ptr: u64,
grad_output_shape: &[usize],
filter_ptr: u64,
filter_shape: &[usize],
grad_input_ptr: u64,
grad_input_shape: &[usize],
stride: &[usize],
padding: &[usize],
) -> BackendResult<()> {
let _ = (
grad_output_ptr,
grad_output_shape,
filter_ptr,
filter_shape,
grad_input_ptr,
grad_input_shape,
stride,
padding,
);
Err(BackendError::Unsupported(
"conv2d_backward_data not implemented by this backend".into(),
))
}
#[allow(clippy::too_many_arguments)]
fn conv2d_backward_filter(
&self,
input_ptr: u64,
input_shape: &[usize],
grad_output_ptr: u64,
grad_output_shape: &[usize],
grad_filter_ptr: u64,
grad_filter_shape: &[usize],
stride: &[usize],
padding: &[usize],
) -> BackendResult<()> {
let _ = (
input_ptr,
input_shape,
grad_output_ptr,
grad_output_shape,
grad_filter_ptr,
grad_filter_shape,
stride,
padding,
);
Err(BackendError::Unsupported(
"conv2d_backward_filter not implemented by this backend".into(),
))
}
fn softmax(
&self,
input_ptr: u64,
output_ptr: u64,
shape: &[usize],
axis: usize,
) -> BackendResult<()> {
let _ = (input_ptr, output_ptr, shape, axis);
Err(BackendError::Unsupported(
"softmax not implemented by this backend".into(),
))
}
fn gather(
&self,
input_ptr: u64,
indices: &[usize],
output_ptr: u64,
rows: usize,
cols: usize,
) -> BackendResult<()> {
let _ = (input_ptr, indices, output_ptr, rows, cols);
Err(BackendError::Unsupported(
"gather not implemented by this backend".into(),
))
}
fn scatter(
&self,
input_ptr: u64,
indices: &[usize],
output_ptr: u64,
rows: usize,
cols: usize,
) -> BackendResult<()> {
let _ = (input_ptr, indices, output_ptr, rows, cols);
Err(BackendError::Unsupported(
"scatter not implemented by this backend".into(),
))
}
#[allow(clippy::too_many_arguments)]
fn batched_gemm(
&self,
trans_a: BackendTranspose,
trans_b: BackendTranspose,
m: usize,
n: usize,
k: usize,
alpha: f64,
a_ptr: u64,
lda: usize,
stride_a: usize,
b_ptr: u64,
ldb: usize,
stride_b: usize,
beta: f64,
c_ptr: u64,
ldc: usize,
stride_c: usize,
batch_count: usize,
) -> BackendResult<()> {
let elem_bytes: u64 = 4; for b in 0..batch_count {
let b64 = b as u64;
self.gemm(
trans_a,
trans_b,
m,
n,
k,
alpha,
a_ptr + b64 * stride_a as u64 * elem_bytes,
lda,
b_ptr + b64 * stride_b as u64 * elem_bytes,
ldb,
beta,
c_ptr + b64 * stride_c as u64 * elem_bytes,
ldc,
)?;
}
Ok(())
}
fn synchronize(&self) -> BackendResult<()>;
fn alloc(&self, bytes: usize) -> BackendResult<u64>;
fn free(&self, ptr: u64) -> BackendResult<()>;
fn copy_htod(&self, dst: u64, src: &[u8]) -> BackendResult<()>;
fn copy_dtoh(&self, dst: &mut [u8], src: u64) -> BackendResult<()>;
}
impl<T: ComputeBackend + ?Sized> ComputeBackend for &mut T {
fn name(&self) -> &str {
(**self).name()
}
fn init(&mut self) -> BackendResult<()> {
(**self).init()
}
fn is_initialized(&self) -> bool {
(**self).is_initialized()
}
fn capabilities(&self) -> Capabilities {
(**self).capabilities()
}
fn available_devices(&self) -> BackendResult<Vec<DeviceInfo>> {
(**self).available_devices()
}
fn recommended_tile_for(&self, m: usize, n: usize, k: usize) -> TileShape {
(**self).recommended_tile_for(m, n, k)
}
fn gemm(
&self,
trans_a: BackendTranspose,
trans_b: BackendTranspose,
m: usize,
n: usize,
k: usize,
alpha: f64,
a_ptr: u64,
lda: usize,
b_ptr: u64,
ldb: usize,
beta: f64,
c_ptr: u64,
ldc: usize,
) -> BackendResult<()> {
(**self).gemm(
trans_a, trans_b, m, n, k, alpha, a_ptr, lda, b_ptr, ldb, beta, c_ptr, ldc,
)
}
fn conv2d_forward(
&self,
input_ptr: u64,
input_shape: &[usize],
filter_ptr: u64,
filter_shape: &[usize],
output_ptr: u64,
output_shape: &[usize],
stride: &[usize],
padding: &[usize],
) -> BackendResult<()> {
(**self).conv2d_forward(
input_ptr,
input_shape,
filter_ptr,
filter_shape,
output_ptr,
output_shape,
stride,
padding,
)
}
fn attention(
&self,
q_ptr: u64,
k_ptr: u64,
v_ptr: u64,
o_ptr: u64,
batch: usize,
heads: usize,
seq_q: usize,
seq_kv: usize,
head_dim: usize,
scale: f64,
causal: bool,
) -> BackendResult<()> {
(**self).attention(
q_ptr, k_ptr, v_ptr, o_ptr, batch, heads, seq_q, seq_kv, head_dim, scale, causal,
)
}
fn reduce(
&self,
op: ReduceOp,
input_ptr: u64,
output_ptr: u64,
shape: &[usize],
axis: usize,
) -> BackendResult<()> {
(**self).reduce(op, input_ptr, output_ptr, shape, axis)
}
fn unary(&self, op: UnaryOp, input_ptr: u64, output_ptr: u64, n: usize) -> BackendResult<()> {
(**self).unary(op, input_ptr, output_ptr, n)
}
fn binary(
&self,
op: BinaryOp,
a_ptr: u64,
b_ptr: u64,
output_ptr: u64,
n: usize,
) -> BackendResult<()> {
(**self).binary(op, a_ptr, b_ptr, output_ptr, n)
}
fn gemm_mixed_precision(
&self,
prec: MixedPrecision,
trans_a: BackendTranspose,
trans_b: BackendTranspose,
m: usize,
n: usize,
k: usize,
alpha: f32,
a_ptr: u64,
lda: usize,
b_ptr: u64,
ldb: usize,
beta: f32,
c_ptr: u64,
ldc: usize,
) -> BackendResult<()> {
(**self).gemm_mixed_precision(
prec, trans_a, trans_b, m, n, k, alpha, a_ptr, lda, b_ptr, ldb, beta, c_ptr, ldc,
)
}
fn conv2d_backward_data(
&self,
grad_output_ptr: u64,
grad_output_shape: &[usize],
filter_ptr: u64,
filter_shape: &[usize],
grad_input_ptr: u64,
grad_input_shape: &[usize],
stride: &[usize],
padding: &[usize],
) -> BackendResult<()> {
(**self).conv2d_backward_data(
grad_output_ptr,
grad_output_shape,
filter_ptr,
filter_shape,
grad_input_ptr,
grad_input_shape,
stride,
padding,
)
}
fn conv2d_backward_filter(
&self,
input_ptr: u64,
input_shape: &[usize],
grad_output_ptr: u64,
grad_output_shape: &[usize],
grad_filter_ptr: u64,
grad_filter_shape: &[usize],
stride: &[usize],
padding: &[usize],
) -> BackendResult<()> {
(**self).conv2d_backward_filter(
input_ptr,
input_shape,
grad_output_ptr,
grad_output_shape,
grad_filter_ptr,
grad_filter_shape,
stride,
padding,
)
}
fn softmax(
&self,
input_ptr: u64,
output_ptr: u64,
shape: &[usize],
axis: usize,
) -> BackendResult<()> {
(**self).softmax(input_ptr, output_ptr, shape, axis)
}
fn gather(
&self,
input_ptr: u64,
indices: &[usize],
output_ptr: u64,
rows: usize,
cols: usize,
) -> BackendResult<()> {
(**self).gather(input_ptr, indices, output_ptr, rows, cols)
}
fn scatter(
&self,
input_ptr: u64,
indices: &[usize],
output_ptr: u64,
rows: usize,
cols: usize,
) -> BackendResult<()> {
(**self).scatter(input_ptr, indices, output_ptr, rows, cols)
}
fn synchronize(&self) -> BackendResult<()> {
(**self).synchronize()
}
fn alloc(&self, bytes: usize) -> BackendResult<u64> {
(**self).alloc(bytes)
}
fn free(&self, ptr: u64) -> BackendResult<()> {
(**self).free(ptr)
}
fn copy_htod(&self, dst: u64, src: &[u8]) -> BackendResult<()> {
(**self).copy_htod(dst, src)
}
fn copy_dtoh(&self, dst: &mut [u8], src: u64) -> BackendResult<()> {
(**self).copy_dtoh(dst, src)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Clone, PartialEq, Eq)]
struct CallRecord {
op: &'static str,
count: usize,
}
#[derive(Debug)]
struct MockBackend {
gemm_call_count: AtomicUsize,
log: Mutex<Vec<CallRecord>>,
}
impl MockBackend {
fn new() -> Self {
Self {
gemm_call_count: AtomicUsize::new(0),
log: Mutex::new(Vec::new()),
}
}
fn record(&self, op: &'static str, count: usize) {
self.log.lock().unwrap().push(CallRecord { op, count });
}
fn calls(&self) -> Vec<CallRecord> {
self.log.lock().unwrap().clone()
}
}
impl ComputeBackend for MockBackend {
fn name(&self) -> &str {
"mock"
}
fn init(&mut self) -> BackendResult<()> {
Ok(())
}
fn is_initialized(&self) -> bool {
true
}
fn gemm(
&self,
_trans_a: BackendTranspose,
_trans_b: BackendTranspose,
_m: usize,
_n: usize,
_k: usize,
_alpha: f64,
_a_ptr: u64,
_lda: usize,
_b_ptr: u64,
_ldb: usize,
_beta: f64,
_c_ptr: u64,
_ldc: usize,
) -> BackendResult<()> {
self.gemm_call_count.fetch_add(1, Ordering::Relaxed);
self.record("gemm", 1);
Ok(())
}
fn conv2d_forward(
&self,
_: u64,
_: &[usize],
_: u64,
_: &[usize],
_: u64,
_: &[usize],
_: &[usize],
_: &[usize],
) -> BackendResult<()> {
self.record("conv2d_forward", 1);
Ok(())
}
fn attention(
&self,
_: u64,
_: u64,
_: u64,
_: u64,
_: usize,
_: usize,
_: usize,
_: usize,
_: usize,
_: f64,
_: bool,
) -> BackendResult<()> {
self.record("attention", 1);
Ok(())
}
fn reduce(&self, _: ReduceOp, _: u64, _: u64, _: &[usize], _: usize) -> BackendResult<()> {
self.record("reduce", 1);
Ok(())
}
fn unary(&self, _: UnaryOp, _: u64, _: u64, n: usize) -> BackendResult<()> {
self.record("unary", n);
Ok(())
}
fn binary(&self, _: BinaryOp, _: u64, _: u64, _: u64, n: usize) -> BackendResult<()> {
self.record("binary", n);
Ok(())
}
fn synchronize(&self) -> BackendResult<()> {
Ok(())
}
fn alloc(&self, bytes: usize) -> BackendResult<u64> {
self.record("alloc", bytes);
Ok(0)
}
fn free(&self, _: u64) -> BackendResult<()> {
Ok(())
}
fn copy_htod(&self, _: u64, src: &[u8]) -> BackendResult<()> {
self.record("copy_htod", src.len());
Ok(())
}
fn copy_dtoh(&self, dst: &mut [u8], _: u64) -> BackendResult<()> {
self.record("copy_dtoh", dst.len());
Ok(())
}
}
#[test]
fn batched_gemm_zero_batch_is_noop() {
let backend = MockBackend::new();
let result = backend.batched_gemm(
BackendTranspose::NoTrans,
BackendTranspose::NoTrans,
4,
4,
4,
1.0,
0,
4,
16,
0,
4,
16,
0.0,
0,
4,
16,
0, );
assert!(result.is_ok());
assert_eq!(backend.gemm_call_count.load(Ordering::Relaxed), 0);
}
#[test]
fn batched_gemm_default_calls_gemm_n_times() {
let backend = MockBackend::new();
let batch_count = 7;
let result = backend.batched_gemm(
BackendTranspose::NoTrans,
BackendTranspose::Trans,
8,
8,
8,
1.0,
1000,
8,
64,
2000,
8,
64,
0.0,
3000,
8,
64,
batch_count,
);
assert!(result.is_ok());
assert_eq!(backend.gemm_call_count.load(Ordering::Relaxed), batch_count);
}
#[test]
fn batched_gemm_single_batch() {
let backend = MockBackend::new();
let result = backend.batched_gemm(
BackendTranspose::NoTrans,
BackendTranspose::NoTrans,
16,
16,
16,
1.0,
0,
16,
256,
0,
16,
256,
1.0,
0,
16,
256,
1,
);
assert!(result.is_ok());
assert_eq!(backend.gemm_call_count.load(Ordering::Relaxed), 1);
}
#[test]
fn mock_records_dispatch_for_consumer_tests() {
let backend = MockBackend::new();
backend.alloc(128).unwrap();
backend.copy_htod(0, &[0u8; 32]).unwrap();
backend.unary(UnaryOp::Relu, 0, 0, 64).unwrap();
backend.binary(BinaryOp::Add, 0, 0, 0, 64).unwrap();
let calls = backend.calls();
assert_eq!(
calls,
vec![
CallRecord {
op: "alloc",
count: 128
},
CallRecord {
op: "copy_htod",
count: 32
},
CallRecord {
op: "unary",
count: 64
},
CallRecord {
op: "binary",
count: 64
},
]
);
}
#[test]
fn default_softmax_gather_scatter_are_unsupported() {
let backend = MockBackend::new();
assert!(matches!(
backend.softmax(0, 0, &[2, 2], 1),
Err(BackendError::Unsupported(_))
));
assert!(matches!(
backend.gather(0, &[0], 0, 1, 1),
Err(BackendError::Unsupported(_))
));
assert!(matches!(
backend.scatter(0, &[0], 0, 1, 1),
Err(BackendError::Unsupported(_))
));
}
#[test]
fn default_mixed_precision_and_conv_backward_are_unsupported() {
let backend = MockBackend::new();
assert!(matches!(
backend.gemm_mixed_precision(
MixedPrecision::Bf16,
BackendTranspose::NoTrans,
BackendTranspose::NoTrans,
2,
2,
2,
1.0,
0,
2,
0,
2,
0.0,
0,
2,
),
Err(BackendError::Unsupported(_))
));
assert!(matches!(
backend.conv2d_backward_data(
0,
&[1, 1, 2, 2],
0,
&[1, 1, 2, 2],
0,
&[1, 1, 3, 3],
&[1, 1],
&[0, 0],
),
Err(BackendError::Unsupported(_))
));
assert!(matches!(
backend.conv2d_backward_filter(
0,
&[1, 1, 3, 3],
0,
&[1, 1, 2, 2],
0,
&[1, 1, 2, 2],
&[1, 1],
&[0, 0],
),
Err(BackendError::Unsupported(_))
));
}
#[test]
fn default_capabilities_and_tile_hint() {
let backend = MockBackend::new();
assert_eq!(backend.capabilities(), Capabilities::cpu());
assert_eq!(
backend.recommended_tile_for(32, 32, 32),
TileShape::new(16, 16, 16)
);
assert!(backend.available_devices().unwrap().is_empty());
}
#[test]
fn mut_ref_blanket_forwards() {
fn run_one_gemm<B: ComputeBackend>(mut be: B) -> BackendResult<()> {
be.init()?;
be.gemm(
BackendTranspose::NoTrans,
BackendTranspose::NoTrans,
2,
2,
2,
1.0,
0,
2,
0,
2,
0.0,
0,
2,
)
}
let mut backend = MockBackend::new();
run_one_gemm(&mut backend).unwrap();
assert_eq!(backend.name(), "mock");
assert_eq!(backend.gemm_call_count.load(Ordering::Relaxed), 1);
}
#[test]
fn object_safety_vec_of_mixed_backends() {
let backends: Vec<Box<dyn ComputeBackend>> = vec![
Box::new(MockBackend::new()),
Box::new(CpuBackend::new()),
Box::new(NullBackend::new()),
];
let names: Vec<&str> = backends.iter().map(|b| b.name()).collect();
assert_eq!(names, vec!["mock", "cpu", "null"]);
for b in &backends {
assert!(b.synchronize().is_ok());
}
}
}
#[cfg(test)]
mod conformance {
use super::*;
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
Self { state: seed }
}
fn next_u32(&mut self) -> u32 {
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.state >> 32) as u32
}
fn next_unit(&mut self) -> f64 {
f64::from(self.next_u32()) / f64::from(u32::MAX) }
}
fn upload_f64(be: &CpuBackend, data: &[f64]) -> u64 {
let ptr = be.alloc(data.len() * 8).unwrap();
let mut bytes = Vec::with_capacity(data.len() * 8);
for &v in data {
bytes.extend_from_slice(&v.to_ne_bytes());
}
be.copy_htod(ptr, &bytes).unwrap();
ptr
}
fn download_f64(be: &CpuBackend, ptr: u64, len: usize) -> Vec<f64> {
let mut bytes = vec![0u8; len * 8];
be.copy_dtoh(&mut bytes, ptr).unwrap();
bytes
.chunks_exact(8)
.map(|c| {
let mut b = [0u8; 8];
b.copy_from_slice(c);
f64::from_ne_bytes(b)
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn ref_gemm(
ta: BackendTranspose,
tb: BackendTranspose,
m: usize,
n: usize,
k: usize,
a: &[f64],
b: &[f64],
) -> Vec<f64> {
let at = |row: usize, col: usize| -> f64 {
match ta {
BackendTranspose::NoTrans => a[col * m + row],
_ => a[row * k + col],
}
};
let bt = |row: usize, col: usize| -> f64 {
match tb {
BackendTranspose::NoTrans => b[col * k + row],
_ => b[row * n + col],
}
};
let mut c = vec![0.0f64; m * n];
for j in 0..n {
for i in 0..m {
let mut acc = 0.0;
for p in 0..k {
acc += at(i, p) * bt(p, j);
}
c[j * m + i] = acc;
}
}
c
}
#[test]
fn gemm_matches_reference_across_all_transpose_combos() {
let be = CpuBackend::new();
let (m, n, k) = (3, 4, 5);
let mut rng = Lcg::new(0xC0FFEE);
for &ta in &[
BackendTranspose::NoTrans,
BackendTranspose::Trans,
BackendTranspose::ConjTrans,
] {
for &tb in &[
BackendTranspose::NoTrans,
BackendTranspose::Trans,
BackendTranspose::ConjTrans,
] {
let a_elems = m * k;
let b_elems = k * n;
let a: Vec<f64> = (0..a_elems).map(|_| rng.next_unit()).collect();
let b: Vec<f64> = (0..b_elems).map(|_| rng.next_unit()).collect();
let (lda, a_cols) = if ta == BackendTranspose::NoTrans {
(m, k)
} else {
(k, m)
};
let (ldb, b_cols) = if tb == BackendTranspose::NoTrans {
(k, n)
} else {
(n, k)
};
assert_eq!(a.len(), lda * a_cols);
assert_eq!(b.len(), ldb * b_cols);
let a_ptr = upload_f64(&be, &a);
let b_ptr = upload_f64(&be, &b);
let c_ptr = upload_f64(&be, &vec![0.0f64; m * n]);
be.gemm(ta, tb, m, n, k, 1.0, a_ptr, lda, b_ptr, ldb, 0.0, c_ptr, m)
.unwrap();
let got = download_f64(&be, c_ptr, m * n);
let want = ref_gemm(ta, tb, m, n, k, &a, &b);
for (g, w) in got.iter().zip(want.iter()) {
assert!((g - w).abs() < 1e-9, "gemm({ta},{tb}) mismatch: {g} vs {w}");
}
be.free(a_ptr).unwrap();
be.free(b_ptr).unwrap();
be.free(c_ptr).unwrap();
}
}
}
#[test]
fn reference_backend_is_always_available_via_registry() {
let reg = BackendRegistry::with_defaults();
let chosen = reg.select_best().unwrap();
assert_eq!(chosen, BackendKind::Cpu);
let be = CpuBackend::new();
let a = upload_f64(&be, &[1.0, 0.0, 0.0, 1.0]);
let b = upload_f64(&be, &[7.0, 8.0, 9.0, 10.0]);
let c = upload_f64(&be, &[0.0; 4]);
be.gemm(
BackendTranspose::NoTrans,
BackendTranspose::NoTrans,
2,
2,
2,
1.0,
a,
2,
b,
2,
0.0,
c,
2,
)
.unwrap();
assert_eq!(download_f64(&be, c, 4), vec![7.0, 8.0, 9.0, 10.0]);
}
}