mod codegen;
mod gpu;
use std::any::Any;
use std::sync::Arc;
use crate::array::{Array, Buf, Data, Owner};
use crate::dtype::DType;
use crate::fuse::{FusedKernel, Yield};
pub use codegen::Precision;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeviceInfo {
pub name: String,
pub backend: String,
pub kind: String,
pub f64: bool,
}
pub fn available() -> Vec<DeviceInfo> {
gpu::enumerate()
}
#[derive(Clone)]
pub struct Device {
at: Where,
precision: Precision,
}
#[derive(Clone)]
enum Where {
Cpu,
Gpu(Arc<dyn Backend>),
}
impl std::fmt::Debug for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.at {
Where::Cpu => write!(f, "Device(cpu)"),
Where::Gpu(g) => {
write!(f, "Device({}, {:?})", g.info().name, self.precision)
}
}
}
}
impl Device {
pub fn cpu() -> Device {
Device { at: Where::Cpu, precision: Precision::F64 }
}
pub fn default_gpu() -> Option<Device> {
Some(Device { at: Where::Gpu(gpu::shared()?), precision: Precision::F64 })
}
pub fn with_precision(&self, p: Precision) -> Device {
Device { at: self.at.clone(), precision: p }
}
pub fn precision(&self) -> Precision {
self.precision
}
pub fn is_gpu(&self) -> bool {
matches!(self.at, Where::Gpu(_))
}
pub fn info(&self) -> Option<&DeviceInfo> {
match &self.at {
Where::Cpu => None,
Where::Gpu(g) => Some(g.info()),
}
}
fn backend(&self) -> Option<&Arc<dyn Backend>> {
match &self.at {
Where::Cpu => None,
Where::Gpu(g) => Some(g),
}
}
pub fn upload(&self, y: &Array) -> Result<Array, DeviceError> {
let Some(backend) = self.backend() else { return Ok(y.clone()) };
let laid_out;
let y = if y.is_row_major() {
y
} else {
laid_out = y.to_row_major();
&laid_out
};
let host = match &y.data {
Data::F64(_) => Host::Same(y.data.clone()),
Data::I64(v) => Host::Made(v.iter().map(|&x| x as f64).collect()),
Data::Bool(v) => Host::Made(v.iter().map(|&x| x as f64).collect()),
_ => {
return Err(DeviceError(
"only boolean, integer and float arrays can be uploaded".into(),
))
}
};
let handle = backend.upload(host.values(), self.precision)?;
let resident = Arc::new(Resident {
device: Arc::as_ptr(backend) as *const () as usize,
precision: self.precision,
elems: host.values().len(),
handle,
host,
});
let values = resident.host.values();
let (ptr, len) = (values.as_ptr(), values.len());
let owner: Owner = resident;
Ok(Array::new(y.shape.clone(), Data::F64(unsafe { Buf::foreign(ptr, len, owner) })))
}
pub fn holds(&self, y: &Array) -> bool {
self.backend().is_some_and(|b| resident_on(y, b, self.precision).is_some())
}
}
enum Host {
Same(Data),
Made(Vec<f64>),
}
impl Host {
fn values(&self) -> &[f64] {
match self {
Host::Same(Data::F64(v)) => v.as_slice(),
Host::Same(_) => &[],
Host::Made(v) => v,
}
}
}
struct Resident {
device: usize,
precision: Precision,
elems: usize,
handle: Handle,
host: Host,
}
fn resident_on<'a>(
y: &'a Array,
backend: &Arc<dyn Backend>,
precision: Precision,
) -> Option<&'a Handle> {
let owner = y.data.owner()?;
let r: &Resident = owner.downcast_ref()?;
let same = r.device == Arc::as_ptr(backend) as *const () as usize
&& r.precision == precision
&& r.elems == y.data.len();
same.then_some(&r.handle)
}
#[derive(Clone, Debug)]
pub struct DeviceError(pub String);
impl std::fmt::Display for DeviceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for DeviceError {}
pub(crate) struct Handle(pub Arc<dyn Any + Send + Sync>);
pub(crate) struct Plan<'a> {
pub source: &'a str,
pub entry: &'a str,
pub inputs: &'a [&'a Handle],
pub out_elems: usize,
pub elem_size: usize,
pub n: u32,
pub stride: u32,
pub groups: u32,
}
pub(crate) trait Backend: Send + Sync + 'static {
fn info(&self) -> &DeviceInfo;
fn upload(&self, values: &[f64], p: Precision) -> Result<Handle, DeviceError>;
fn dispatch(&self, plan: &Plan<'_>) -> Result<Vec<u8>, DeviceError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Refusal {
Integer,
NotFloat,
NoF64,
Unsupported(&'static str),
Declined,
TooSmall,
Failed(String),
}
impl Refusal {
pub fn reason(&self) -> String {
match self {
Refusal::Integer => "the chain computes in 64-bit integers".into(),
Refusal::NotFloat => "the chain's result is not a float array".into(),
Refusal::NoF64 => {
"this adapter has no f64 in shaders; pass precision=\"f32\" to run anyway".into()
}
Refusal::Unsupported(op) => format!("`{op}` has no shader form here"),
Refusal::Declined => "the fused kernel declined these inputs".into(),
Refusal::TooSmall => "there is too little data to pay for a dispatch".into(),
Refusal::Failed(e) => format!("the device refused: {e}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Placement {
Default,
Gpu,
Cpu(Refusal),
}
impl std::fmt::Display for Placement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Placement::Default => Ok(()),
Placement::Gpu => write!(f, "device: gpu"),
Placement::Cpu(why) => write!(f, "device: cpu ({})", why.reason()),
}
}
}
pub const MIN_ELEMS: usize = 1 << 19;
pub(crate) fn try_run(
device: &Device,
k: &FusedKernel,
inputs: &[Array],
) -> Result<Array, Refusal> {
let backend = device.backend().ok_or(Refusal::Declined)?;
let precision = device.precision;
if precision == Precision::F64 && !backend.info().f64 {
return Err(Refusal::NoF64);
}
if k.yields() == Yield::Tally {
return Err(Refusal::Declined);
}
let Some(Some(shape)) = crate::fuse::common_shape(inputs) else {
return Err(Refusal::Declined);
};
let n: usize = shape.iter().product();
if n < MIN_ELEMS {
return Err(Refusal::TooSmall);
}
let reducing = k.reduce().is_some();
if reducing && shape.len() != 1 {
return Err(Refusal::Declined);
}
let (working, root) = crate::fuse::working_type(k, inputs).ok_or(Refusal::Declined)?;
if working != DType::F64 {
return Err(Refusal::Integer);
}
if root != DType::F64 {
return Err(Refusal::NotFloat);
}
let splat: Vec<bool> = inputs.iter().map(|a| a.rank() == 0).collect();
let source = codegen::wgsl(k, &splat, precision).map_err(Refusal::Unsupported)?;
let mut temporaries: Vec<Handle> = Vec::new();
let mut slots: Vec<Option<&Handle>> = Vec::with_capacity(inputs.len());
for a in inputs {
match resident_on(a, backend, precision) {
Some(h) => slots.push(Some(h)),
None => {
let h = match &a.data {
Data::F64(v) => backend.upload(v.as_slice(), precision),
_ => backend.upload(&as_f64_vec(a), precision),
}
.map_err(|e| Refusal::Failed(e.0))?;
temporaries.push(h);
slots.push(None);
}
}
}
let mut next = 0usize;
let buffers: Vec<&Handle> = slots
.iter()
.map(|s| match s {
Some(h) => *h,
None => {
let h = &temporaries[next];
next += 1;
h
}
})
.collect();
let elem_size = precision.size();
let out = if reducing {
let groups = codegen::groups_for(n);
let plan = Plan {
source: &source,
entry: codegen::REDUCE,
inputs: &buffers,
out_elems: groups,
elem_size,
n: n as u32,
stride: (groups * codegen::WORKGROUP) as u32,
groups: groups as u32,
};
let bytes = backend.dispatch(&plan).map_err(|e| Refusal::Failed(e.0))?;
let partials = codegen::from_bytes(&bytes, precision, groups);
let op = k.reduce().expect("reducing");
let mut acc = *partials.last().ok_or(Refusal::Declined)?;
for &v in partials[..partials.len() - 1].iter().rev() {
acc = crate::fuse::step(op, v, acc).ok_or(Refusal::Declined)?;
}
Array::scalar_f64(acc)
} else {
let plan = Plan {
source: &source,
entry: codegen::MAP,
inputs: &buffers,
out_elems: n,
elem_size,
n: n as u32,
stride: 0,
groups: n.div_ceil(codegen::WORKGROUP) as u32,
};
let bytes = backend.dispatch(&plan).map_err(|e| Refusal::Failed(e.0))?;
let values = codegen::from_bytes(&bytes, precision, n);
Array::new(shape, Data::F64(values.into()))
};
Ok(out)
}
fn as_f64_vec(a: &Array) -> Vec<f64> {
match &a.data {
Data::F64(v) => v.as_slice().to_vec(),
Data::I64(v) => v.iter().map(|&x| x as f64).collect(),
Data::Bool(v) => v.iter().map(|&x| x as f64).collect(),
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_cpu_is_always_a_device() {
let d = Device::cpu();
assert!(!d.is_gpu());
assert!(d.info().is_none());
let a = Array::from_f64(vec![1.0, 2.0]);
assert_eq!(d.upload(&a).expect("cpu upload"), a);
}
#[test]
fn every_refusal_says_something() {
for r in [
Refusal::Integer,
Refusal::NotFloat,
Refusal::NoF64,
Refusal::Unsupported("^"),
Refusal::Declined,
Refusal::TooSmall,
Refusal::Failed("no adapter".into()),
] {
assert!(!r.reason().is_empty());
}
}
}