#![allow(clippy::needless_pass_by_ref_mut)]
#![allow(clippy::upper_case_acronyms)]
use crate::{
DebugMask,
dtype::{Constant, DType},
error::{BackendError, ErrorStatus},
graph::Graph,
kernel::{BOp, Kernel, MMADims, Op, OpId, ParamKind, RangeKind, UOp},
shape::Dim,
slab::SlabId,
};
use crate::{Map, hashers::FHasher};
use nanoserde::{DeBin, DeJson, SerBin};
use std::sync::Mutex;
use std::{collections::BTreeSet, hash::BuildHasherDefault, sync::Arc};
mod c;
mod cblas;
mod cuda;
mod disk;
mod dummy;
mod host;
mod opencl;
#[cfg(feature = "tenstorrent")]
mod tenstorrent;
mod vulkan;
#[cfg(feature = "wgpu")]
mod wgpu;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PoolBufferId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Pool {
Host,
Disk,
Cuda(u16),
OpenCL(u16),
Vulkan(u16),
#[cfg(feature = "tenstorrent")]
TT(u16),
#[cfg(feature = "wgpu")]
WGPU(u16),
Dummy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Dev {
Auto,
C,
Cblas,
Cuda(u16),
#[cfg(feature = "tenstorrent")]
TT(u16),
Vulkan(u16),
OpenCL(u16),
#[cfg(feature = "wgpu")]
WGPU(u16),
Dummy,
}
pub(super) fn lock<'a, T>(pool: Pool, mutex: &'a Mutex<T>) -> std::sync::MutexGuard<'a, T> {
mutex.lock().unwrap_or_else(|_| panic!("{pool:?} pool lock poisoned by a panicking holder"))
}
pub(super) fn dlock<'a, T>(dev: Dev, mutex: &'a Mutex<T>) -> std::sync::MutexGuard<'a, T> {
mutex.lock().unwrap_or_else(|_| panic!("{dev:?} device lock poisoned by a panicking holder"))
}
impl Dev {
#[must_use]
pub fn all() -> Vec<Dev> {
let mut out = Vec::new();
if c::device().is_ok() {
out.push(Dev::C);
}
if cblas::device().is_ok() {
out.push(Dev::Cblas);
}
for i in 0..cuda::device_count() {
out.push(Dev::Cuda(i));
}
#[cfg(feature = "tenstorrent")]
for i in 0..tenstorrent::device_count() {
out.push(Dev::TT(i));
}
for i in 0..vulkan::device_count() {
out.push(Dev::Vulkan(i));
}
for i in 0..opencl::device_count() {
out.push(Dev::OpenCL(i));
}
#[cfg(feature = "wgpu")]
for i in 0..wgpu::device_count() {
out.push(Dev::WGPU(i));
}
if dummy::device().is_ok() {
out.push(Dev::Dummy);
}
out
}
#[must_use]
pub fn pool(self) -> Pool {
match self {
Dev::Auto => panic!("Dev::Auto has no pool; resolve it with Dev::auto() first"),
Dev::C | Dev::Cblas => Pool::Host,
Dev::Cuda(i) => Pool::Cuda(i),
#[cfg(feature = "tenstorrent")]
Dev::TT(i) => Pool::TT(i),
Dev::Vulkan(i) => Pool::Vulkan(i),
Dev::OpenCL(i) => Pool::OpenCL(i),
#[cfg(feature = "wgpu")]
Dev::WGPU(i) => Pool::WGPU(i),
Dev::Dummy => Pool::Dummy,
}
}
pub fn info(self) -> Arc<DeviceInfo> {
match self {
Dev::Auto => panic!("Dev::Auto has no info; resolve it with Dev::auto() first"),
Dev::C => c::device().expect("C device unavailable").lock().unwrap().info(),
Dev::Cblas => cblas::device().expect("CBLAS device unavailable").lock().unwrap().info(),
Dev::Cuda(id) => dlock(self, &cuda::device(id).expect("CUDA device unavailable")).info(),
Dev::OpenCL(id) => dlock(self, &opencl::device(id).expect("OpenCL device unavailable")).info(),
#[cfg(feature = "tenstorrent")]
Dev::TT(id) => dlock(self, &tenstorrent::device(id).expect("TT device unavailable")).info(),
Dev::Vulkan(id) => dlock(self, &vulkan::device(id).expect("Vulkan device unavailable")).info(),
#[cfg(feature = "wgpu")]
Dev::WGPU(id) => dlock(self, &wgpu::device(id).expect("WGPU device unavailable")).info(),
Dev::Dummy => dummy::device().expect("dummy device unavailable").lock().unwrap().info(),
}
}
pub fn free_compute(self) -> u128 {
match self {
Dev::Auto => panic!("Dev::Auto has no compute; resolve it with Dev::auto() first"),
Dev::C => c::device().expect("C device unavailable").lock().unwrap().free_compute(),
Dev::Cblas => cblas::device().expect("CBLAS device unavailable").lock().unwrap().free_compute(),
Dev::Cuda(id) => dlock(self, &cuda::device(id).expect("CUDA device unavailable")).free_compute(),
Dev::OpenCL(id) => dlock(self, &opencl::device(id).expect("OpenCL device unavailable")).free_compute(),
#[cfg(feature = "tenstorrent")]
Dev::TT(id) => dlock(self, &tenstorrent::device(id).expect("TT device unavailable")).free_compute(),
Dev::Vulkan(id) => dlock(self, &vulkan::device(id).expect("Vulkan device unavailable")).free_compute(),
#[cfg(feature = "wgpu")]
Dev::WGPU(id) => dlock(self, &wgpu::device(id).expect("WGPU device unavailable")).free_compute(),
Dev::Dummy => dummy::device().expect("dummy device unavailable").lock().unwrap().free_compute(),
}
}
#[must_use]
pub const fn aot_only(self) -> bool {
matches!(self, Self::Cblas)
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Dev::Auto => "Auto",
Dev::C => "C",
Dev::Cblas => "CBLAS",
Dev::Dummy => "Dummy",
Dev::Cuda(_) => "CUDA",
Dev::OpenCL(_) => "OpenCL",
#[cfg(feature = "tenstorrent")]
Dev::TT(_) => "Tenstorrent",
Dev::Vulkan(_) => "Vulkan",
#[cfg(feature = "wgpu")]
Dev::WGPU(_) => "WGPU",
}
}
pub fn compile(self, kernel: &Kernel, debug_asm: bool) -> Result<DeviceProgramId, BackendError> {
let result = match self {
Dev::Auto => panic!("Dev::Auto cannot compile; resolve it with Dev::auto() first"),
Dev::C => c::device().expect("C device unavailable").lock().unwrap().compile(kernel, debug_asm),
Dev::Cblas => cblas::device().expect("CBLAS device unavailable").lock().unwrap().compile(kernel, debug_asm),
Dev::Dummy => dummy::device().expect("dummy device unavailable").lock().unwrap().compile(kernel, debug_asm),
Dev::Cuda(id) => dlock(self, &cuda::device(id).expect("CUDA device unavailable")).compile(kernel, debug_asm),
Dev::OpenCL(id) => dlock(self, &opencl::device(id).expect("OpenCL device unavailable")).compile(kernel, debug_asm),
#[cfg(feature = "tenstorrent")]
Dev::TT(id) => dlock(self, &tenstorrent::device(id).expect("TT device unavailable")).compile(kernel, debug_asm),
Dev::Vulkan(id) => dlock(self, &vulkan::device(id).expect("Vulkan device unavailable")).compile(kernel, debug_asm),
#[cfg(feature = "wgpu")]
Dev::WGPU(id) => dlock(self, &wgpu::device(id).expect("WGPU device unavailable")).compile(kernel, debug_asm),
};
if let Ok(x) = std::env::var("ZYX_DEBUG")
&& let Ok(x) = x.parse::<u32>()
&& DebugMask(x).compile()
{
println!("[{}] compile kernel", self.name());
}
result
}
pub fn release(self, program_id: DeviceProgramId) {
match self {
Dev::Auto => panic!("Dev::Auto cannot release; resolve it with Dev::auto() first"),
Dev::C => c::device().expect("C device unavailable").lock().unwrap().release(program_id),
Dev::Cblas => cblas::device().expect("CBLAS device unavailable").lock().unwrap().release(program_id),
Dev::Dummy => dummy::device().expect("dummy device unavailable").lock().unwrap().release(program_id),
Dev::Cuda(id) => dlock(self, &cuda::device(id).expect("CUDA device unavailable")).release(program_id),
Dev::OpenCL(id) => dlock(self, &opencl::device(id).expect("OpenCL device unavailable")).release(program_id),
#[cfg(feature = "tenstorrent")]
Dev::TT(id) => dlock(self, &tenstorrent::device(id).expect("TT device unavailable")).release(program_id),
Dev::Vulkan(id) => dlock(self, &vulkan::device(id).expect("Vulkan device unavailable")).release(program_id),
#[cfg(feature = "wgpu")]
Dev::WGPU(id) => dlock(self, &wgpu::device(id).expect("WGPU device unavailable")).release(program_id),
}
}
pub fn match_graph(self, graph: &mut Graph, outputs: &BTreeSet<OpId>) {
match self {
Dev::Cblas => cblas::device().expect("CBLAS device unavailable").lock().unwrap().match_graph(graph, outputs),
Dev::Cuda(id) => dlock(self, &cuda::device(id).expect("CUDA device unavailable")).match_graph(graph, outputs),
_ => {}
}
graph.verify();
}
pub fn launch(self, program_id: DeviceProgramId, args: &[LaunchArg]) -> Result<(), BackendError> {
debug_assert!(!args.is_empty(), "launch with empty args: buffer binding failed upstream");
let pool = self.pool();
match self {
Dev::Auto => panic!("Dev::Auto cannot launch; resolve it with Dev::auto() first"),
Dev::C => c::device().expect("C device unavailable").lock().unwrap().launch(program_id, pool, args),
Dev::Cblas => cblas::device().expect("CBLAS device unavailable").lock().unwrap().launch(program_id, pool, args),
Dev::Dummy => dummy::device().expect("dummy device unavailable").lock().unwrap().launch(program_id, pool, args),
Dev::Cuda(id) => dlock(self, &cuda::device(id).expect("CUDA device unavailable")).launch(program_id, pool, args),
Dev::OpenCL(id) => {
dlock(self, &opencl::device(id).expect("OpenCL device unavailable")).launch(program_id, pool, args)
}
#[cfg(feature = "tenstorrent")]
Dev::TT(id) => dlock(self, &tenstorrent::device(id).expect("TT device unavailable")).launch(program_id, pool, args),
Dev::Vulkan(id) => {
dlock(self, &vulkan::device(id).expect("Vulkan device unavailable")).launch(program_id, pool, args)
}
#[cfg(feature = "wgpu")]
Dev::WGPU(id) => dlock(self, &wgpu::device(id).expect("WGPU device unavailable")).launch(program_id, pool, args),
}
}
pub fn launch_timed(self, program_id: DeviceProgramId, args: &[LaunchArg]) -> Result<u64, BackendError> {
debug_assert!(!args.is_empty(), "launch_timed with empty args: buffer binding failed upstream");
match self {
Dev::Auto => panic!("Dev::Auto cannot launch; resolve it with Dev::auto() first"),
Dev::Cuda(id) => dlock(self, &cuda::device(id).expect("CUDA device unavailable")).launch_timed(program_id, args),
Dev::OpenCL(id) => {
dlock(self, &opencl::device(id).expect("OpenCL device unavailable")).launch_timed(program_id, args)
}
Dev::C => {
let pool = self.pool();
c::device().expect("C device unavailable").lock().unwrap().launch_timed(program_id, pool, args)
}
Dev::Cblas => todo!("launch_timed not yet ported to the CBLAS device"),
Dev::Dummy => todo!("launch_timed not yet ported to the dummy device"),
Dev::Vulkan(id) => {
dlock(self, &vulkan::device(id).expect("Vulkan device unavailable")).launch_timed(program_id, args)
}
#[cfg(feature = "tenstorrent")]
Dev::TT(id) => todo!("launch_timed not yet ported to the TT device ({id})"),
#[cfg(feature = "wgpu")]
Dev::WGPU(id) => dlock(self, &wgpu::device(id).expect("WGPU device unavailable")).launch_timed(program_id, args),
}
}
}
impl Pool {
#[must_use]
pub fn all() -> Vec<Pool> {
let mut out = vec![Pool::Host, Pool::Disk];
if dummy::pool().is_ok() {
out.push(Pool::Dummy);
}
for i in 0..cuda::pool_count() {
out.push(Pool::Cuda(i));
}
for i in 0..opencl::pool_count() {
out.push(Pool::OpenCL(i));
}
for i in 0..vulkan::pool_count() {
out.push(Pool::Vulkan(i));
}
#[cfg(feature = "tenstorrent")]
for i in 0..tenstorrent::pool_count() {
out.push(Pool::TT(i));
}
#[cfg(feature = "wgpu")]
for i in 0..wgpu::pool_count() {
out.push(Pool::WGPU(i));
}
out
}
pub fn allocate(self, bytes: Dim) -> Result<PoolBufferId, BackendError> {
let bytes = bytes + 8; let free = self.free_bytes();
let (result, name) = match self {
Pool::Host => (lock(self, host::pool()).allocate(bytes), "host"),
Pool::Disk => todo!("disk is not allocatable"),
Pool::Cuda(id) => (lock(self, cuda::pool(id)?).allocate(bytes), "cuda"),
Pool::OpenCL(id) => (lock(self, opencl::pool(id)?).allocate(bytes), "opencl"),
Pool::Vulkan(id) => (lock(self, vulkan::pool(id)?).allocate(bytes), "vulkan"),
#[cfg(feature = "tenstorrent")]
Pool::TT(id) => (lock(self, tenstorrent::pool(id)?).allocate(bytes), "tenstorrent"),
#[cfg(feature = "wgpu")]
Pool::WGPU(id) => (lock(self, wgpu::pool(id)?).allocate(bytes), "wgpu"),
Pool::Dummy => (lock(self, dummy::pool()?).allocate(bytes), "dummy"),
};
if result.is_ok() {
if let Ok(x) = std::env::var("ZYX_DEBUG")
&& let Ok(x) = x.parse::<u32>()
&& DebugMask::new(x).dev()
{
println!("[{name}] allocate {bytes} -> free {free} B");
}
} else {
eprintln!("[{name}] allocate FAILED {bytes} -> free {free} B");
}
result
}
pub fn insert_host(self, buf: Box<[u8]>) -> PoolBufferId {
match self {
Pool::Host => lock(self, host::pool()).insert(buf),
_ => unreachable!("Pool::insert is only valid for the host pool, got {self:?}"),
}
}
pub fn disk_buffer_from_path(self, bytes: Dim, path: &std::path::Path, offset_bytes: u64) -> PoolBufferId {
match self {
Pool::Disk => lock(self, disk::pool()).buffer_from_path(bytes, path, offset_bytes),
_ => unreachable!("Pool::disk_buffer_from_path is only valid for the disk pool, got {self:?}"),
}
}
pub fn retain(self, buffer_id: PoolBufferId) {
match self {
Pool::Cuda(id) => {
if let Ok(pool) = cuda::pool(id) {
lock(self, &pool).retain(buffer_id);
}
}
Pool::Host => lock(self, host::pool()).retain(buffer_id),
Pool::Disk => {}
Pool::OpenCL(id) => {
if let Ok(pool) = opencl::pool(id) {
lock(self, &pool).retain(buffer_id);
}
}
Pool::Vulkan(id) => {
if let Ok(pool) = vulkan::pool(id) {
lock(self, &pool).retain(buffer_id);
}
}
Pool::Dummy => {
if let Ok(pool) = dummy::pool() {
lock(self, &pool).retain(buffer_id);
}
}
#[cfg(feature = "tenstorrent")]
Pool::TT(id) => {
if let Ok(pool) = tenstorrent::pool(id) {
lock(self, &pool).retain(buffer_id);
}
}
#[cfg(feature = "wgpu")]
Pool::WGPU(id) => {
if let Ok(pool) = wgpu::pool(id) {
lock(self, &pool).retain(buffer_id);
}
}
}
}
pub fn release(self, buffer_id: PoolBufferId) {
match self {
Pool::Cuda(id) => {
if let Ok(pool) = cuda::pool(id) {
lock(self, &pool).release(buffer_id);
}
}
Pool::Host => lock(self, host::pool()).release(buffer_id),
Pool::Disk => {}
Pool::OpenCL(id) => {
if let Ok(pool) = opencl::pool(id) {
lock(self, &pool).release(buffer_id);
}
}
Pool::Vulkan(id) => {
if let Ok(pool) = vulkan::pool(id) {
lock(self, &pool).release(buffer_id);
}
}
Pool::Dummy => {
if let Ok(pool) = dummy::pool() {
lock(self, &pool).release(buffer_id);
}
}
#[cfg(feature = "tenstorrent")]
Pool::TT(id) => {
if let Ok(pool) = tenstorrent::pool(id) {
lock(self, &pool).release(buffer_id);
}
}
#[cfg(feature = "wgpu")]
Pool::WGPU(id) => {
if wgpu::flush_pending(id).is_ok()
&& let Ok(pool) = wgpu::pool(id)
{
lock(self, &pool).release(buffer_id);
}
}
}
}
pub fn free_bytes(self) -> Dim {
match self {
Pool::Host => lock(self, host::pool()).free_bytes(),
Pool::Disk => lock(self, disk::pool()).free_bytes(),
Pool::Cuda(id) => cuda::pool(id).map(|p| lock(self, &p).free_bytes()).unwrap_or(0),
Pool::OpenCL(id) => opencl::pool(id).map(|p| lock(self, &p).free_bytes()).unwrap_or(0),
Pool::Vulkan(id) => vulkan::pool(id).map(|p| lock(self, &p).free_bytes()).unwrap_or(0),
#[cfg(feature = "tenstorrent")]
Pool::TT(id) => tenstorrent::pool(id).map(|p| lock(self, &p).free_bytes()).unwrap_or(0),
#[cfg(feature = "wgpu")]
Pool::WGPU(id) => wgpu::pool(id).map(|p| lock(self, &p).free_bytes()).unwrap_or(0),
Pool::Dummy => dummy::pool().map(|p| lock(self, &p).free_bytes()).unwrap_or(0),
}
}
pub fn pool_to_host(self, src: PoolBufferId, dst: &mut [u8]) -> Result<(), BackendError> {
match self {
Pool::Host => lock(self, host::pool()).pool_to_host(src, dst),
Pool::Disk => lock(self, disk::pool()).pool_to_host(src, dst),
Pool::Cuda(id) => lock(self, cuda::pool(id)?).pool_to_host(src, dst),
Pool::OpenCL(id) => lock(self, opencl::pool(id)?).pool_to_host(src, dst),
Pool::Vulkan(id) => lock(self, vulkan::pool(id)?).pool_to_host(src, dst),
#[cfg(feature = "tenstorrent")]
Pool::TT(id) => lock(self, tenstorrent::pool(id)?).pool_to_host(src, dst),
#[cfg(feature = "wgpu")]
Pool::WGPU(id) => {
wgpu::flush_pending(id)?;
lock(self, wgpu::pool(id)?).pool_to_host(src, dst)
}
Pool::Dummy => lock(self, dummy::pool()?).pool_to_host(src, dst),
}
}
pub fn pool_to_pool(self, src: Pool, src_buf: PoolBufferId, dst_buf: PoolBufferId) -> Result<(), BackendError> {
match self {
Pool::Host => {
let Pool::Cuda(id) = src else {
return lock(self, host::pool()).pool_to_pool(src, src_buf, dst_buf);
};
let pool = host::pool();
let (dst_ptr, bytes) = {
let mut p = lock(self, &pool);
(p.buffer_ptr_mut(dst_buf), p.get_buffer(dst_buf).len())
};
let src_pool = cuda::pool(id)?;
lock(src, &src_pool).pool_to_host_ptr(src_buf, dst_ptr, bytes as i64)
}
Pool::Disk => todo!("copies into disk pool"),
Pool::Cuda(id) => lock(self, cuda::pool(id)?).pool_to_pool(src, src_buf, dst_buf),
Pool::OpenCL(id) => lock(self, opencl::pool(id)?).pool_to_pool(src, src_buf, dst_buf),
Pool::Vulkan(id) => lock(self, vulkan::pool(id)?).pool_to_pool(src, src_buf, dst_buf),
#[cfg(feature = "tenstorrent")]
Pool::TT(id) => lock(self, tenstorrent::pool(id)?).pool_to_pool(src, src_buf, dst_buf),
#[cfg(feature = "wgpu")]
Pool::WGPU(id) => {
wgpu::flush_pending(id)?;
lock(self, wgpu::pool(id)?).pool_to_pool(src, src_buf, dst_buf)
}
Pool::Dummy => lock(self, dummy::pool()?).pool_to_pool(src, src_buf, dst_buf),
}
}
pub fn buffer_ptr_mut(self, buffer_id: PoolBufferId) -> *mut u8 {
match self {
Pool::Host => lock(self, host::pool()).buffer_ptr_mut(buffer_id),
Pool::Disk => todo!("disk buffers have no staging pointer"),
Pool::Cuda(_) => todo!("cuda buffers have no staging pointer"),
Pool::OpenCL(_) => todo!("opencl buffers have no staging pointer"),
Pool::Vulkan(_) => todo!("vulkan buffers have no staging pointer"),
#[cfg(feature = "tenstorrent")]
Pool::TT(_) => todo!("TT buffers have no staging pointer"),
#[cfg(feature = "wgpu")]
Pool::WGPU(_) => todo!("wgpu buffers have no staging pointer"),
Pool::Dummy => todo!("dummy buffers have no staging pointer"),
}
}
}
pub(crate) fn config() -> &'static Config {
static CONFIG: std::sync::OnceLock<Config> = std::sync::OnceLock::new();
CONFIG.get_or_init(load_config_file)
}
fn load_config_file() -> Config {
use std::path::PathBuf;
let debug = debug_backends();
let config_file = std::env::var_os("XDG_CONFIG_HOME")
.and_then(|path| {
let path = PathBuf::from(path);
if path.is_absolute() { Some(path) } else { None }
})
.or_else(|| std::env::home_dir().map(|home| home.join(".config")))
.map(|path| path.join("zyx/config.json"))
.and_then(|path| std::fs::read_to_string(&path).ok());
config_file
.and_then(|file| {
DeJson::deserialize_json(&file)
.map_err(|e| {
if debug {
println!("Failed to parse config.json, {e}");
}
})
.ok()
})
.inspect(|_| {
if debug {
println!("Device config successfully read and parsed.");
}
})
.unwrap_or_else(|| {
if debug {
println!("Failed to get device config, using defaults.");
}
Config::default()
})
}
pub(crate) fn debug_backends() -> bool {
std::env::var("ZYX_DEBUG").ok().and_then(|x| x.parse::<u32>().ok()).is_some_and(|x| DebugMask::new(x).dev())
}
pub(crate) fn autotune_config() -> crate::kernel::autotune::BeamSearch {
config().autotune.clone()
}
#[derive(Debug, Clone)]
pub enum LaunchArg {
Buffer(PoolBufferId),
Variable(Constant),
}
#[derive(Debug, Clone, PartialEq)]
pub enum GwsDim {
Const(Dim),
Param(usize),
Unary { x: Box<GwsDim>, uop: UOp },
Binary { x: Box<GwsDim>, y: Box<GwsDim>, bop: BOp },
Cast { x: Box<GwsDim>, dtype: DType },
}
impl GwsDim {
#[must_use]
pub fn eval(&self, param: &mut dyn FnMut(usize) -> Dim) -> Dim {
match self {
GwsDim::Const(d) => *d,
GwsDim::Param(ordinal) => param(*ordinal),
GwsDim::Unary { x, uop } => Constant::unary(Constant::idx(x.eval(param)), *uop)
.as_dim()
.expect("gws expression evaluated to a non-integer dim"),
GwsDim::Binary { x, y, bop } => {
let xv = Constant::idx(x.eval(param));
let yv = Constant::idx(y.eval(param));
Constant::binary(xv, yv, *bop).as_dim().expect("gws expression evaluated to a non-integer dim")
}
GwsDim::Cast { x, dtype } => {
Constant::idx(x.eval(param)).cast(*dtype).as_dim().expect("cast gws expression evaluated to a non-integer dim")
}
}
}
}
pub(crate) fn gws_from_kernel(kernel: &Kernel, max_grid_dims: &[Dim]) -> Result<Vec<GwsDim>, BackendError> {
let mut param_ordinal: Map<OpId, usize> = Map::with_hasher(BuildHasherDefault::<FHasher>::new());
let mut param_idx = 0usize;
let mut op_id = kernel.head;
while !op_id.is_null() {
if matches!(kernel.ops[op_id].op, Op::Param { .. }) {
param_ordinal.insert(op_id, param_idx);
param_idx += 1;
}
op_id = kernel.next_op(op_id);
}
fn conv(kernel: &Kernel, len: OpId, ordinals: &Map<OpId, usize>) -> GwsDim {
match &kernel.ops[len].op {
Op::Const(c) => GwsDim::Const(c.as_dim().unwrap()),
Op::Param { kind: ParamKind::Variable, .. } => GwsDim::Param(ordinals[&len]),
Op::Unary { x, uop } => GwsDim::Unary { x: Box::new(conv(kernel, *x, ordinals)), uop: *uop },
Op::Binary { x, y, bop } => {
GwsDim::Binary { x: Box::new(conv(kernel, *x, ordinals)), y: Box::new(conv(kernel, *y, ordinals)), bop: *bop }
}
Op::Load { src, .. } => match &kernel.ops[*src].op {
Op::Param { kind: ParamKind::Variable, .. } => GwsDim::Param(ordinals[src]),
ref op => unreachable!("group length load from non-variable storage, got {op:?}"),
},
Op::Cast { x, dtype } => GwsDim::Cast { x: Box::new(conv(kernel, *x, ordinals)), dtype: *dtype },
ref op => unreachable!("group length must be a dim over Const/Param Variable, got {op:?}"),
}
}
let mut gws = Vec::new();
let mut op_id = kernel.head;
let mut steps_op_id = 0usize;
while !op_id.is_null() {
steps_op_id += 1;
if steps_op_id > 10_000 {
panic!("gws_from_kernel did not finish in 10000 steps");
}
if let Op::Range { axis, kind: RangeKind::Group(len) } = kernel.ops[op_id].op {
let gdim = conv(kernel, len, ¶m_ordinal);
let axis = axis as usize;
if let GwsDim::Const(c) = gdim
&& let Some(&max) = max_grid_dims.get(axis)
&& c > max
{
return Err(BackendError {
status: ErrorStatus::KernelCompilation,
context: format!("grid dim {axis} {c} exceeds device max {max}").into(),
});
}
if gws.len() <= axis {
gws.resize(axis + 1, GwsDim::Const(1));
}
gws[axis] = gdim;
}
op_id = kernel.next_op(op_id);
}
Ok(gws)
}
impl From<usize> for PoolBufferId {
fn from(value: usize) -> Self {
PoolBufferId(u32::try_from(value).unwrap())
}
}
impl From<PoolBufferId> for usize {
fn from(value: PoolBufferId) -> Self {
value.0 as usize
}
}
impl SlabId for PoolBufferId {
const ZERO: Self = Self(0);
const NULL: Self = Self(u32::MAX);
fn inc(&mut self) {
self.0 += 1;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, DeBin, SerBin)]
pub struct DeviceProgramId(u32);
impl From<usize> for DeviceProgramId {
fn from(value: usize) -> Self {
DeviceProgramId(u32::try_from(value).unwrap())
}
}
impl From<DeviceProgramId> for usize {
fn from(value: DeviceProgramId) -> Self {
value.0 as usize
}
}
impl SlabId for DeviceProgramId {
const ZERO: Self = Self(0);
const NULL: Self = Self(u32::MAX);
fn inc(&mut self) {
self.0 += 1;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Buffer {
pub pool: Pool,
pub buffer_id: PoolBufferId,
}
impl Buffer {
pub const NULL: Self = Self { pool: Pool::Host, buffer_id: PoolBufferId(u32::MAX) };
}
impl From<Buffer> for usize {
fn from(value: Buffer) -> Self {
value.buffer_id.0 as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgramId {
pub dev: Dev,
pub program_id: DeviceProgramId,
}
impl ProgramId {
pub const NULL: Self = Self { dev: Dev::Auto, program_id: DeviceProgramId(u32::MAX) };
}
impl From<ProgramId> for usize {
fn from(value: ProgramId) -> Self {
value.program_id.0 as usize
}
}
impl From<libloading::Error> for BackendError {
fn from(value: libloading::Error) -> Self {
BackendError { status: ErrorStatus::Initialization, context: value.to_string().into() }
}
}
#[cfg_attr(feature = "py", pyo3::pyclass)]
#[derive(DeJson, Debug, Default)]
#[nserde(default)]
pub struct Config {
pub autotune: crate::kernel::autotune::BeamSearch,
pub c: c::CConfig,
pub cblas: cblas::CblasConfig,
pub dummy: dummy::DummyConfig,
pub cuda: cuda::CUDAConfig,
pub opencl: opencl::OpenCLConfig,
#[cfg(feature = "tenstorrent")]
pub tenstorrent: tenstorrent::TTConfig,
pub vulkan: vulkan::VulkanConfig,
#[cfg(feature = "wgpu")]
pub wgpu: wgpu::WGPUConfig,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, SerBin, DeBin)]
pub struct DTypeCapability(u32);
impl DTypeCapability {
pub const ZERO: Self = Self(0);
}
macro_rules! op_cap {
($name:ident, $bit:expr, $method:ident) => {
pub const $name: Self = Self(1 << $bit);
pub fn $method(&self) -> bool {
self.0 & Self::$name.0 != 0
}
};
}
impl std::ops::BitOr for DTypeCapability {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl DTypeCapability {
op_cap!(NEG, 0, neg);
op_cap!(BITNOT, 1, bitnot);
op_cap!(EXP, 2, exp);
op_cap!(EXP2, 3, exp2);
op_cap!(LN, 4, ln);
op_cap!(LOG2, 5, log2);
op_cap!(RECIPROCAL, 6, reciprocal);
op_cap!(SQRT, 7, sqrt);
op_cap!(SIN, 8, sin);
op_cap!(COS, 9, cos);
op_cap!(FLOOR, 10, floor);
op_cap!(TRUNC, 11, trunc);
op_cap!(ABS, 12, abs);
op_cap!(ADD, 13, add);
op_cap!(SUB, 14, sub);
op_cap!(MUL, 15, mul);
op_cap!(DIV, 16, div);
op_cap!(POW, 17, pow);
op_cap!(MOD, 18, r#mod);
op_cap!(CMPLT, 19, cmplt);
op_cap!(CMPGT, 20, cmpgt);
op_cap!(MAX, 21, max);
op_cap!(OR, 22, or);
op_cap!(AND, 23, and);
op_cap!(BITXOR, 24, bitxor);
op_cap!(BITOR, 25, bitor);
op_cap!(BITAND, 26, bitand);
op_cap!(BITSHIFTLEFT, 27, bitshiftleft);
op_cap!(BITSHIFTRIGHT, 28, bitshiftright);
op_cap!(NOTEQ, 29, noteq);
op_cap!(EQ, 30, eq);
#[must_use]
pub const fn all() -> Self {
Self(u32::MAX)
}
#[must_use]
pub const fn none() -> Self {
Self(0)
}
#[must_use]
pub fn any(&self) -> bool {
self.0 != 0
}
#[must_use]
pub fn invert(&self) -> Self {
Self(!self.0)
}
#[must_use]
pub fn exclude(&self, capability: DTypeCapability) -> Self {
Self(self.0 & !capability.0)
}
#[must_use]
pub fn include(&self, capability: DTypeCapability) -> Self {
Self(self.0 | capability.0)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, SerBin, DeBin)]
pub struct DeviceInfo {
pub compute: u128,
pub max_global_work_dims: Vec<Dim>,
pub max_local_threads: u32,
pub max_local_work_dims: Vec<u32>,
pub preferred_vector_size: u8,
pub local_mem_size: Dim,
pub max_register_bytes: Dim,
pub tensor_cores: bool,
pub warp_size: u16,
pub cc: [i32; 2],
pub dtype_capability: [DTypeCapability; DType::N_DTYPES],
pub has_native_exp2: bool,
pub supported_vec_lens: Vec<u8>,
pub tenstorrent: bool,
pub tile: [Dim; 2],
pub tile_sizes: Vec<[Dim; 2]>,
pub wmma_layouts: Vec<MMADims>,
pub num_circular_buffers: u32,
pub has_openmp: bool,
}
impl DeviceInfo {
pub const fn supports_dtype(&self, dtype: DType) -> DTypeCapability {
self.dtype_capability[dtype as usize]
}
}