#![allow(clippy::upper_case_acronyms)]
#![allow(clippy::needless_pass_by_ref_mut)]
use super::{
DTypeCapability, Device, DeviceId, DeviceInfo, DeviceProgramId, Event, MemoryPool, PoolBufferId, PoolId, ProgramId,
host::HostEvent,
};
use crate::{
DType, Set,
error::{BackendError, ErrorStatus},
graph::{ClassId, Graph, Node, NodeData},
kernel::Kernel,
runtime::ShapeId,
shape::Dim,
slab::{Slab, SlabId},
};
use libloading::Library;
use nanoserde::DeJson;
use std::collections::BTreeSet;
type SgemmFn = unsafe extern "C" fn(
order: i32,
transa: i32,
transb: i32,
m: i32,
n: i32,
k: i32,
alpha: f32,
a: *const f32,
lda: i32,
b: *const f32,
ldb: i32,
beta: f32,
c: *mut f32,
ldc: i32,
);
const CBLAS_ROW_MAJOR: i32 = 101;
const CBLAS_NO_TRANS: i32 = 111;
const OPENBLAS_PATH: &str = "/usr/lib/x86_64-linux-gnu/libopenblas.so";
#[derive(Debug, DeJson)]
#[nserde(default)]
pub struct CblasConfig {
pub enabled: bool,
}
impl Default for CblasConfig {
fn default() -> Self {
Self { enabled: true }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CblasKernelId(u32);
impl From<usize> for CblasKernelId {
fn from(value: usize) -> Self {
CblasKernelId(u32::try_from(value).unwrap())
}
}
impl From<CblasKernelId> for usize {
fn from(value: CblasKernelId) -> Self {
value.0 as usize
}
}
impl SlabId for CblasKernelId {
const ZERO: Self = Self(0);
const NULL: Self = Self(u32::MAX);
fn inc(&mut self) {
self.0 += 1;
}
}
#[derive(Debug)]
pub struct CblasKernel {
sgemm: SgemmFn,
}
#[derive(Debug)]
pub struct CblasProgram {
kernel: CblasKernelId,
m: Dim,
n: Dim,
k: Dim,
}
#[derive(Debug)]
pub struct CblasDevice {
device_info: DeviceInfo,
device_id: DeviceId,
memory_pool_id: PoolId,
#[allow(dead_code)]
lib: Library,
kernels: Slab<CblasKernelId, CblasKernel>,
programs: Slab<DeviceProgramId, CblasProgram>,
}
pub(super) fn initialize_device(
config: &CblasConfig,
memory_pools: &mut Slab<PoolId, MemoryPool>,
devices: &mut Slab<DeviceId, Device>,
debug_dev: bool,
) -> Result<(), BackendError> {
if !config.enabled {
if debug_dev {
println!("[cblas] configured out");
}
return Ok(());
}
if memory_pools.is_empty() {
return Err(BackendError {
status: ErrorStatus::Initialization,
context: "cblas backend requires HostMemoryPool to be initialized first.".into(),
});
}
let lib = unsafe { Library::new(OPENBLAS_PATH) }?;
let sgemm: SgemmFn = *unsafe { lib.get(b"cblas_sgemm") }?;
let mut kernels = Slab::new();
kernels.push(CblasKernel { sgemm });
let device_id = devices.push(Device::Cblas(CblasDevice {
device_info: DeviceInfo {
compute: 1,
max_global_work_dims: vec![Dim::from(0u64); 3],
max_local_threads: 1,
max_local_work_dims: vec![1, 1, 1],
preferred_vector_size: 8,
local_mem_size: 0,
max_register_bytes: 0,
tensor_cores: false,
warp_size: 1,
dtype_capability: [DTypeCapability::none(); DType::N_DTYPES],
has_native_exp2: false,
supported_vec_lens: vec![],
},
device_id: DeviceId::NULL,
memory_pool_id: PoolId::from(0),
lib,
kernels,
programs: Slab::new(),
}));
if let Device::Cblas(dev) = &mut devices[device_id] {
dev.device_id = device_id;
}
if debug_dev {
println!("[cblas] initialized from {OPENBLAS_PATH}");
}
Ok(())
}
impl CblasDevice {
pub const fn deinitialize(&mut self) {}
pub const fn info(&self) -> &DeviceInfo {
&self.device_info
}
pub const fn memory_pool_id(&self) -> PoolId {
self.memory_pool_id
}
pub const fn free_compute(&self) -> u128 {
self.device_info.compute
}
pub fn release(&mut self, program_id: DeviceProgramId) {
self.programs.remove(program_id);
}
pub fn compile(&mut self, _kernel: &Kernel, _debug_asm: bool) -> Result<DeviceProgramId, BackendError> {
Err(BackendError {
status: ErrorStatus::KernelCompilation,
context: "cblas device only runs AOT matmul kernels, it does not compile generic kernels.".into(),
})
}
pub fn match_graph(&mut self, graph: &mut Graph, outputs: &BTreeSet<ClassId>, shapes: &Slab<ShapeId, Vec<Dim>>) {
let order = graph.topo_sort_classes_without_kernels(&Set::default(), outputs, None);
for &cid in &order {
let Some(mm) = graph.match_matmul(cid, shapes) else {
continue;
};
if mm.in_dtype != DType::F32 || mm.acc_dtype != DType::F32 {
continue;
}
println!("[cblas] matched matmul m={}, n={}, k={}", mm.m, mm.n, mm.k);
let program_id = self.programs.push(CblasProgram { kernel: CblasKernelId::ZERO, m: mm.m, n: mm.n, k: mm.k });
let nid = graph.nodes.push(NodeData {
node: Node::Kernel {
inputs: Box::new([mm.a, mm.b]),
outputs: Box::new([mm.out]),
program_id: ProgramId { device: self.device_id, program: program_id },
time: 1,
},
class_of: mm.out,
});
graph.classes[mm.out].nodes.push(nid);
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn launch(
&mut self,
program_id: DeviceProgramId,
memory_pool: &mut super::host::HostMemoryPool,
args: &[PoolBufferId],
event_wait_list: Vec<Event>,
) -> Result<Event, BackendError> {
let _ = event_wait_list;
let program = &self.programs[program_id];
let kernel = &self.kernels[program.kernel];
let m: i32 = i32::try_from(program.m)
.map_err(|_| BackendError { status: ErrorStatus::IncorrectKernelArg, context: "m exceeds i32 range".into() })?;
let n: i32 = i32::try_from(program.n)
.map_err(|_| BackendError { status: ErrorStatus::IncorrectKernelArg, context: "n exceeds i32 range".into() })?;
let k: i32 = i32::try_from(program.k)
.map_err(|_| BackendError { status: ErrorStatus::IncorrectKernelArg, context: "k exceeds i32 range".into() })?;
let a = memory_pool.buffer_ptr_mut(args[0]) as *mut f32;
let b = memory_pool.buffer_ptr_mut(args[1]) as *mut f32;
let c = memory_pool.buffer_ptr_mut(args[2]) as *mut f32;
unsafe {
(kernel.sgemm)(CBLAS_ROW_MAJOR, CBLAS_NO_TRANS, CBLAS_NO_TRANS, m, n, k, 1.0, a, k, b, n, 0.0, c, n);
}
Ok(Event::Host(HostEvent))
}
}