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 mod op_profile {
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use fuel_ir::dispatch::OpKind;
use fuel_ir::probe::BackendId;
static ENABLED: AtomicBool = AtomicBool::new(false);
static PHASES: Mutex<Option<HashMap<&'static str, (u64, Duration)>>> = Mutex::new(None);
pub struct PhaseTimer(Option<(&'static str, std::time::Instant)>);
impl PhaseTimer {
pub fn start(name: &'static str) -> Self {
Self(enabled().then(|| (name, std::time::Instant::now())))
}
}
impl Drop for PhaseTimer {
fn drop(&mut self) {
if let Some((name, t0)) = self.0.take() {
let mut g = PHASES.lock().unwrap_or_else(|p| p.into_inner());
let e = g
.get_or_insert_with(HashMap::new)
.entry(name)
.or_insert((0, Duration::ZERO));
e.0 += 1;
e.1 += t0.elapsed();
}
}
}
pub fn take_phases() -> Vec<(&'static str, u64, Duration)> {
let mut g = PHASES.lock().unwrap_or_else(|p| p.into_inner());
let mut v: Vec<_> = g
.take()
.unwrap_or_default()
.into_iter()
.map(|(k, (n, d))| (k, n, d))
.collect();
v.sort_by_key(|row| std::cmp::Reverse(row.2));
v
}
static TRACE: AtomicBool = AtomicBool::new(false);
static TRACE_ROWS: Mutex<Vec<LaunchRow>> = Mutex::new(Vec::new());
#[derive(Clone, Debug)]
pub struct LaunchRow {
pub op: String,
pub backend: String,
pub src: &'static str,
pub dst: &'static str,
pub bytes: usize,
pub dt: Duration,
}
fn kind(s: &std::sync::Arc<std::sync::RwLock<fuel_memory::Storage>>) -> (&'static str, usize) {
let Ok(g) = s.try_read() else {
return ("?", 0);
};
let k = match &g.inner {
fuel_memory::BackendStorage::Cpu(_) => "Cpu",
#[cfg(feature = "cuda")]
fuel_memory::BackendStorage::Cuda(_) => "Cuda",
#[cfg(feature = "vulkan")]
fuel_memory::BackendStorage::Vulkan(_) => "Vulkan",
#[allow(unreachable_patterns)]
_ => "Other",
};
(k, g.len_bytes())
}
type Totals = HashMap<(String, String), (u64, Duration)>;
static TOTALS: Mutex<Option<Totals>> = Mutex::new(None);
pub fn set_enabled(on: bool) {
ENABLED.store(on, Ordering::Relaxed);
}
pub fn set_trace(on: bool) {
TRACE.store(on, Ordering::Relaxed);
}
pub fn take_trace() -> Vec<LaunchRow> {
std::mem::take(&mut *TRACE_ROWS.lock().unwrap_or_else(|p| p.into_inner()))
}
pub fn enabled() -> bool {
ENABLED.load(Ordering::Relaxed) || TRACE.load(Ordering::Relaxed)
}
pub(super) fn record_launch(
compiled: &super::CompiledNode,
inputs: &[std::sync::Arc<std::sync::RwLock<fuel_memory::Storage>>],
outputs: &[std::sync::Arc<std::sync::RwLock<fuel_memory::Storage>>],
dt: Duration,
) {
record(compiled.op, compiled.backend, dt);
if TRACE.load(Ordering::Relaxed) {
let (src, bytes) = inputs.first().map_or(("?", 0), kind);
let dst = outputs.first().map_or("?", |o| kind(o).0);
TRACE_ROWS
.lock()
.unwrap_or_else(|p| p.into_inner())
.push(LaunchRow {
op: format!("{:?}", compiled.op),
backend: format!("{:?}", compiled.backend),
src,
dst,
bytes,
dt,
});
}
}
pub(super) fn record(op: OpKind, backend: BackendId, dt: Duration) {
let mut g = TOTALS.lock().unwrap_or_else(|p| p.into_inner());
let e = g
.get_or_insert_with(HashMap::new)
.entry((format!("{op:?}"), format!("{backend:?}")))
.or_insert((0, Duration::ZERO));
e.0 += 1;
e.1 += dt;
}
pub fn take() -> Vec<(String, String, u64, Duration)> {
let mut g = TOTALS.lock().unwrap_or_else(|p| p.into_inner());
let mut v: Vec<_> = g
.take()
.unwrap_or_default()
.into_iter()
.map(|((op, be), (n, d))| (op, be, n, d))
.collect();
v.sort_by_key(|row| std::cmp::Reverse(row.3));
v
}
}
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> {
let profile_start = op_profile::enabled().then(std::time::Instant::now);
let launched = (compiled.kernel)(inputs, outputs, layouts, &compiled.op_params);
if let Some(t0) = profile_start {
op_profile::record_launch(compiled, inputs, outputs, t0.elapsed());
}
launched?;
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 op_profile_tests {
use super::op_profile;
use fuel_ir::dispatch::OpKind;
use fuel_ir::probe::BackendId;
use std::time::Duration;
#[test]
fn totals_sum_per_key_sort_longest_first_and_drain() {
let _ = op_profile::take();
op_profile::record(
OpKind::AddElementwise,
BackendId::Cpu,
Duration::from_millis(2),
);
op_profile::record(
OpKind::AddElementwise,
BackendId::Cpu,
Duration::from_millis(3),
);
op_profile::record(OpKind::MatMul, BackendId::Cpu, Duration::from_millis(1));
op_profile::record(OpKind::MatMul, BackendId::Vulkan, Duration::from_millis(9));
let rows = op_profile::take();
let got: Vec<(&str, &str, u64, u128)> = rows
.iter()
.map(|(o, b, n, d)| (o.as_str(), b.as_str(), *n, d.as_millis()))
.collect();
assert_eq!(
got,
vec![
("MatMul", "Vulkan", 1, 9),
("AddElementwise", "Cpu", 2, 5),
("MatMul", "Cpu", 1, 1),
]
);
assert!(op_profile::take().is_empty(), "take() drains");
}
}
#[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());
}
}