use std::sync::{Arc, RwLock};
#[cfg_attr(not(feature = "cuda"), allow(unused_imports))]
use fuel_ir::Error;
use fuel_ir::dispatch::OpKind;
use fuel_ir::probe::BackendId;
use fuel_ir::{DType, Layout, Result};
use crate::kernel::{KernelBindingTable, KernelCaps, KernelDTypes, KernelRef, OpParams};
use fuel_graph::{Graph, NodeId};
use fuel_memory::Storage;
#[derive(Debug, Clone)]
pub struct CompiledNode {
pub op: OpKind,
pub dtypes: KernelDTypes,
pub backend: BackendId,
pub kernel: KernelRef,
pub caps: KernelCaps,
pub op_params: OpParams,
}
impl CompiledNode {
pub fn output_dtype(&self) -> DType {
*self
.dtypes
.last()
.expect("CompiledNode::dtypes must have at least one entry (the output)")
}
}
pub fn compile_node(
op: OpKind,
dtypes: &[DType],
backend: BackendId,
op_params: OpParams,
bindings: &KernelBindingTable,
) -> Result<CompiledNode> {
let (kernel, caps) = bindings.lookup_with_caps(op, dtypes, backend)?;
Ok(CompiledNode {
op,
dtypes: KernelDTypes::from_slice(dtypes),
backend,
kernel,
caps,
op_params,
})
}
pub fn dispatched_kernel_ident(
graph: &Graph,
node: NodeId,
bindings: &KernelBindingTable,
) -> Option<(&'static str, u64)> {
let n = graph.node(node);
let op = crate::pipelined::op_to_op_kind(&n.op)?;
let dtypes = crate::pipelined::build_lookup_dtypes(graph, n);
let backend = graph.target_backend(node)?;
bindings
.lookup_alternatives(op, &dtypes, backend)
.iter()
.find(|e| !e.caps.requires_broadcast)
.map(|e| (e.kernel_source, e.kernel_revision_hash))
}
pub fn execute_compiled(
compiled: &CompiledNode,
inputs: &[Arc<RwLock<Storage>>],
outputs: &mut [Arc<RwLock<Storage>>],
layouts: &[Layout],
) -> Result<CompletionHandle> {
execute_compiled_with_wait_hint(compiled, inputs, outputs, layouts, true)
}
pub fn execute_compiled_with_wait_hint(
compiled: &CompiledNode,
inputs: &[Arc<RwLock<Storage>>],
outputs: &mut [Arc<RwLock<Storage>>],
layouts: &[Layout],
will_be_waited: bool,
) -> Result<CompletionHandle> {
(compiled.kernel)(inputs, outputs, layouts, &compiled.op_params)?;
produce_pending(compiled.backend, outputs, will_be_waited)
}
fn produce_pending(
_backend: BackendId,
outputs: &[Arc<RwLock<Storage>>],
will_be_waited: bool,
) -> Result<CompletionHandle> {
#[cfg(not(feature = "cuda"))]
let _ = (outputs, will_be_waited);
#[cfg(feature = "cuda")]
{
if will_be_waited {
if let Some(out0) = outputs.first() {
let guard = out0.read().map_err(|_| {
Error::Msg("output storage lock poisoned recording completion".into()).bt()
})?;
if let fuel_memory::BackendStorage::Cuda(cuda_bytes) = &guard.inner {
let loc = cuda_bytes.device().location();
let ev = cuda_bytes.record_completion_event()?;
return Ok(CompletionHandle::Pending(Box::new(CudaCompletion::new(
ev, loc,
))));
}
}
}
}
Ok(CompletionHandle::Ready)
}
#[cfg(feature = "cuda")]
struct CudaCompletion {
ev: fuel_cuda_backend::Event,
loc: fuel_ir::DeviceLocation,
}
#[cfg(feature = "cuda")]
impl CudaCompletion {
fn new(ev: fuel_cuda_backend::Event, loc: fuel_ir::DeviceLocation) -> Self {
crate::dispatch::inflight_inc(loc);
Self { ev, loc }
}
}
#[cfg(feature = "cuda")]
impl Drop for CudaCompletion {
fn drop(&mut self) {
crate::dispatch::inflight_dec(self.loc);
}
}
#[cfg(feature = "cuda")]
impl Completion for CudaCompletion {
fn wait(self: Box<Self>) -> Result<()> {
use fuel_cuda_backend::WrapErr;
self.ev.synchronize().w()
}
}
#[must_use = "a CompletionHandle must be waited on (or threaded to the executor) — dropping a Pending handle leaks async work"]
pub enum CompletionHandle {
Ready,
Pending(Box<dyn Completion>),
}
impl CompletionHandle {
pub fn wait(self) -> Result<()> {
match self {
CompletionHandle::Ready => Ok(()),
CompletionHandle::Pending(c) => c.wait(),
}
}
}
pub trait Completion: Send {
fn wait(self: Box<Self>) -> Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dispatch::register_cpu_kernels;
#[test]
fn compile_then_execute_add_f32() {
let mut bindings = KernelBindingTable::new();
register_cpu_kernels(&mut bindings);
let compiled = compile_node(
OpKind::AddElementwise,
&[DType::F32, DType::F32, DType::F32],
BackendId::Cpu,
OpParams::None,
&bindings,
)
.expect("compile");
assert_eq!(compiled.op, OpKind::AddElementwise);
assert_eq!(compiled.output_dtype(), DType::F32);
assert_eq!(compiled.backend, BackendId::Cpu);
let lhs = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0, 7.0]);
let rhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
let out = fuel_memory::alloc_cpu_zeroed(DType::F32, 3).unwrap();
let inputs = vec![Arc::new(RwLock::new(lhs)), Arc::new(RwLock::new(rhs))];
let mut outputs = vec![Arc::new(RwLock::new(out))];
let layout_3 = Layout::contiguous(fuel_ir::Shape::from(vec![3]));
let layouts = vec![layout_3.clone(), layout_3.clone(), layout_3];
execute_compiled(&compiled, &inputs, &mut outputs, &layouts)
.expect("execute")
.wait()
.expect("wait");
let out_guard = outputs[0].read().unwrap();
if let fuel_memory::BackendStorage::Cpu(c) = &out_guard.inner {
let typed: &[f32] = c.as_slice().unwrap();
assert_eq!(typed, &[6.0, 8.0, 10.0]);
}
}
#[test]
fn execute_compiled_cpu_is_ready_not_pending() {
let mut bindings = KernelBindingTable::new();
register_cpu_kernels(&mut bindings);
let compiled = compile_node(
OpKind::AddElementwise,
&[DType::F32, DType::F32, DType::F32],
BackendId::Cpu,
OpParams::None,
&bindings,
)
.expect("compile");
let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
let rhs = fuel_memory::from_slice_cpu(&[3.0_f32, 4.0]);
let out = fuel_memory::alloc_cpu_zeroed(DType::F32, 2).unwrap();
let inputs = vec![Arc::new(RwLock::new(lhs)), Arc::new(RwLock::new(rhs))];
let mut outputs = vec![Arc::new(RwLock::new(out))];
let l = Layout::contiguous(fuel_ir::Shape::from(vec![2]));
let layouts = vec![l.clone(), l.clone(), l];
let handle = execute_compiled(&compiled, &inputs, &mut outputs, &layouts).expect("execute");
assert!(
matches!(handle, CompletionHandle::Ready),
"CPU execute_compiled must return Ready (A4b-1 behavior-preserving)",
);
handle.wait().expect("wait");
}
#[test]
fn execute_compiled_with_wait_hint_cpu_ready_and_correct_under_both_hints() {
let mut bindings = KernelBindingTable::new();
register_cpu_kernels(&mut bindings);
let compiled = compile_node(
OpKind::AddElementwise,
&[DType::F32, DType::F32, DType::F32],
BackendId::Cpu,
OpParams::None,
&bindings,
)
.expect("compile");
for hint in [true, false] {
let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
let rhs = fuel_memory::from_slice_cpu(&[3.0_f32, 4.0]);
let out = fuel_memory::alloc_cpu_zeroed(DType::F32, 2).unwrap();
let inputs = vec![Arc::new(RwLock::new(lhs)), Arc::new(RwLock::new(rhs))];
let mut outputs = vec![Arc::new(RwLock::new(out))];
let l = Layout::contiguous(fuel_ir::Shape::from(vec![2]));
let layouts = vec![l.clone(), l.clone(), l];
let handle =
execute_compiled_with_wait_hint(&compiled, &inputs, &mut outputs, &layouts, hint)
.expect("execute");
assert!(
matches!(handle, CompletionHandle::Ready),
"CPU path returns Ready regardless of the wait hint ({hint})",
);
handle.wait().expect("wait");
let out_guard = outputs[0].read().unwrap();
if let fuel_memory::BackendStorage::Cpu(c) = &out_guard.inner {
let typed: &[f32] = c.as_slice().unwrap();
assert_eq!(typed, &[4.0, 6.0], "bytes identical under hint={hint}");
}
}
}
#[test]
fn completion_handle_ready_is_noop_pending_delegates() {
use std::sync::Arc as StdArc;
use std::sync::atomic::{AtomicBool, Ordering};
CompletionHandle::Ready.wait().expect("Ready.wait() is Ok");
struct Flag(StdArc<AtomicBool>);
impl Completion for Flag {
fn wait(self: Box<Self>) -> Result<()> {
self.0.store(true, Ordering::SeqCst);
Ok(())
}
}
let waited = StdArc::new(AtomicBool::new(false));
CompletionHandle::Pending(Box::new(Flag(waited.clone())))
.wait()
.expect("Pending.wait() is Ok");
assert!(
waited.load(Ordering::SeqCst),
"Pending.wait() must run the boxed Completion",
);
}
#[test]
fn compile_node_errors_on_missing_binding() {
let bindings = KernelBindingTable::new(); let result = compile_node(
OpKind::AddElementwise,
&[DType::F32, DType::F32, DType::F32],
BackendId::Cpu,
OpParams::None,
&bindings,
);
assert!(result.is_err());
}
}