use std::borrow::Cow;
use onnx_runtime_ir::TensorLayout;
use crate::error::Result;
use crate::tensor::{TensorMut, TensorView};
use crate::weight::WeightHandle;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct Cost {
pub compute_us: f64,
pub memory_us: f64,
pub transfer_us: f64,
pub launch_us: f64,
pub bytes_moved: u64,
}
impl Cost {
pub const ZERO: Cost = Cost {
compute_us: 0.0,
memory_us: 0.0,
transfer_us: 0.0,
launch_us: 0.0,
bytes_moved: 0,
};
pub fn new(compute_us: f64, memory_us: f64, transfer_us: f64) -> Self {
Self {
compute_us,
memory_us,
transfer_us,
..Self::ZERO
}
}
pub fn with_launch_us(mut self, launch_us: f64) -> Self {
self.launch_us = launch_us;
self
}
pub fn with_bytes_moved(mut self, bytes_moved: u64) -> Self {
self.bytes_moved = bytes_moved;
self
}
pub fn total_us(&self) -> f64 {
self.compute_us + self.memory_us + self.transfer_us + self.launch_us
}
}
pub enum KernelMatch {
Supported {
cost: Cost,
required_input_layouts: Option<Vec<TensorLayout>>,
output_layouts: Vec<TensorLayout>,
},
Unsupported {
reason: Cow<'static, str>,
},
}
impl KernelMatch {
pub fn unsupported(reason: impl Into<Cow<'static, str>>) -> Self {
Self::Unsupported {
reason: reason.into(),
}
}
pub fn is_supported(&self) -> bool {
matches!(self, KernelMatch::Supported { .. })
}
pub fn reason(&self) -> Option<&str> {
match self {
Self::Supported { .. } => None,
Self::Unsupported { reason } => Some(reason),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CaptureSupport {
Supported,
Unsupported {
reason: Cow<'static, str>,
},
}
impl CaptureSupport {
pub fn unsupported(reason: impl Into<Cow<'static, str>>) -> Self {
Self::Unsupported {
reason: reason.into(),
}
}
pub fn is_supported(&self) -> bool {
matches!(self, Self::Supported)
}
pub fn reason(&self) -> Option<&str> {
match self {
Self::Supported => None,
Self::Unsupported { reason } => Some(reason),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KernelVariantSelection {
pub variant: &'static str,
pub reason: Cow<'static, str>,
}
impl KernelVariantSelection {
pub fn new(variant: &'static str, reason: impl Into<Cow<'static, str>>) -> Self {
Self {
variant,
reason: reason.into(),
}
}
}
pub const ARG_KERNEL_VARIANT: &str = "kernel_variant";
pub const ARG_KERNEL_VARIANT_REASON: &str = "kernel_variant_reason";
pub const ARG_DEVICE: &str = "device";
pub const ARG_BYTES: &str = "bytes";
pub const ARG_FLOPS: &str = "flops";
pub const CAT_KERNEL_WORKER: &str = "op.worker";
#[inline]
pub fn record_kernel_metrics(
device: &'static str,
inputs: &[TensorView<'_>],
outputs: &[TensorMut<'_>],
flops: impl FnOnce() -> u64,
) {
if !kernel_variant_tracing_enabled() {
return;
}
onnx_runtime_tracer::annotate_current_span_with(|| {
let input_bytes = inputs
.iter()
.filter(|input| !input.is_absent())
.fold(0_u64, |total, input| {
total.saturating_add(input.byte_size() as u64)
});
let bytes = outputs.iter().fold(input_bytes, |total, output| {
total.saturating_add(output.byte_size() as u64)
});
onnx_runtime_tracer::Args::new()
.with(ARG_DEVICE, device)
.with(ARG_BYTES, bytes)
.with(ARG_FLOPS, flops())
});
}
#[must_use]
#[inline]
pub fn kernel_worker_span(label: &'static str) -> Option<onnx_runtime_tracer::SpanGuard> {
let context = onnx_runtime_tracer::global_context()?;
if !context
.verbosity()
.includes(onnx_runtime_tracer::TraceVerbosity::Full)
{
return None;
}
Some(context.span(label, CAT_KERNEL_WORKER))
}
#[must_use]
#[inline]
pub fn kernel_variant_tracing_enabled() -> bool {
onnx_runtime_tracer::tracing_active()
}
#[inline]
pub fn record_kernel_variant_selection(selection: &KernelVariantSelection) {
if !kernel_variant_tracing_enabled() {
return;
}
onnx_runtime_tracer::annotate_current_span_with(|| {
onnx_runtime_tracer::Args::new()
.with(ARG_KERNEL_VARIANT, selection.variant)
.with(ARG_KERNEL_VARIANT_REASON, selection.reason.as_ref())
});
}
#[inline]
pub fn record_kernel_variant_stage_selection(stage: &str, selection: &KernelVariantSelection) {
if !kernel_variant_tracing_enabled() {
return;
}
let variant_key = format!("{ARG_KERNEL_VARIANT}.{stage}");
let reason_key = format!("{ARG_KERNEL_VARIANT_REASON}.{stage}");
onnx_runtime_tracer::annotate_current_span_with(|| {
onnx_runtime_tracer::Args::new()
.with(variant_key, selection.variant)
.with(reason_key, selection.reason.as_ref())
});
}
#[macro_export]
macro_rules! record_kernel_variant {
($variant:expr, $($arg:tt)+) => {{
if $crate::kernel_variant_tracing_enabled() {
let selection = $crate::KernelVariantSelection::new(
$variant,
::std::format!($($arg)+),
);
$crate::record_kernel_variant_selection(&selection);
}
}};
}
#[macro_export]
macro_rules! record_kernel_variant_stage {
($stage:expr, $variant:expr, $($arg:tt)+) => {{
if $crate::kernel_variant_tracing_enabled() {
let selection = $crate::KernelVariantSelection::new(
$variant,
::std::format!($($arg)+),
);
$crate::record_kernel_variant_stage_selection($stage, &selection);
}
}};
}
#[macro_export]
macro_rules! decline_capture {
($($arg:tt)+) => {
return $crate::CaptureSupport::unsupported(format!($($arg)+))
};
}
#[macro_export]
macro_rules! require_capture {
($condition:expr, $($arg:tt)+) => {
if !$condition {
$crate::decline_capture!($($arg)+);
}
};
}
#[macro_export]
macro_rules! deny {
($($arg:tt)+) => {
return $crate::KernelMatch::unsupported(format!($($arg)+))
};
}
#[macro_export]
macro_rules! require {
($condition:expr, $($arg:tt)+) => {
if !$condition {
$crate::deny!($($arg)+);
}
};
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ViewOutput {
pub input_index: usize,
pub shape: Vec<usize>,
pub strides: Vec<i64>,
pub byte_offset: usize,
}
pub enum KernelInput<'a> {
Tensor(TensorView<'a>),
Weight(&'a WeightHandle),
}
impl<'a> KernelInput<'a> {
pub fn tensor(&self) -> Option<&TensorView<'a>> {
match self {
Self::Tensor(view) => Some(view),
Self::Weight(_) => None,
}
}
pub fn weight(&self) -> Option<&WeightHandle> {
match self {
Self::Tensor(_) => None,
Self::Weight(weight) => Some(weight),
}
}
}
pub trait Kernel: Send {
fn set_constant_inputs(&mut self, constant_inputs: &[bool]) {
let _ = constant_inputs;
}
fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()>;
fn execute_with_inputs(
&self,
inputs: &[KernelInput<'_>],
outputs: &mut [TensorMut],
) -> Result<()> {
let views = inputs
.iter()
.map(|input| {
input.tensor().copied().ok_or_else(|| {
crate::EpError::KernelFailed(
"kernel received a lazy WeightHandle without implementing \
execute_with_inputs"
.into(),
)
})
})
.collect::<Result<Vec<_>>>()?;
self.execute(&views, outputs)
}
fn estimated_flops(&self) -> Option<u64> {
None
}
fn view_outputs(&self, inputs: &[TensorView], num_outputs: usize) -> Option<Vec<ViewOutput>> {
let _ = (inputs, num_outputs);
None
}
fn supports_strided_input(&self, input_idx: usize) -> bool {
let _ = input_idx;
false
}
fn preferred_output_layout(&self) -> Option<TensorLayout> {
None
}
fn capture_support(&self) -> CaptureSupport {
CaptureSupport::Supported
}
fn cuda_graph_compatible(&self) -> bool {
self.capture_support().is_supported()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use super::*;
use crate::{DeviceId, DevicePtr};
#[test]
fn cost_zero_and_total() {
assert_eq!(Cost::ZERO.total_us(), 0.0);
let c = Cost::new(10.0, 5.0, 2.0);
assert_eq!(c.total_us(), 17.0);
assert_eq!(c.bytes_moved, 0);
assert_eq!(c.launch_us, 0.0);
}
#[test]
fn cost_builders_are_additive() {
let c = Cost::new(10.0, 5.0, 2.0)
.with_launch_us(3.0)
.with_bytes_moved(4096);
assert_eq!(c.total_us(), 20.0);
assert_eq!(c.bytes_moved, 4096);
}
#[test]
fn kernel_match_reports_support() {
let supported = KernelMatch::Supported {
cost: Cost::new(1.0, 0.0, 0.0),
required_input_layouts: None,
output_layouts: vec![],
};
assert!(supported.is_supported());
assert_eq!(supported.reason(), None);
let unsupported = KernelMatch::unsupported("test EP supports no ops");
assert!(!unsupported.is_supported());
assert_eq!(unsupported.reason(), Some("test EP supports no ops"));
}
fn require_positive(value: i32) -> KernelMatch {
require!(value > 0, "value must be positive, got {value}");
KernelMatch::Supported {
cost: Cost::ZERO,
required_input_layouts: None,
output_layouts: vec![],
}
}
#[test]
fn require_macro_carries_formatted_decline_reason() {
let rejected = require_positive(-2);
assert_eq!(rejected.reason(), Some("value must be positive, got -2"));
assert!(require_positive(2).is_supported());
}
#[test]
fn kernel_variant_selection_carries_winner_and_reason() {
let selection =
KernelVariantSelection::new("gemv", "M==1 decode uses the packed int4 GEMV path");
assert_eq!(selection.variant, "gemv");
assert_eq!(
selection.reason,
"M==1 decode uses the packed int4 GEMV path"
);
}
#[test]
fn record_kernel_variant_without_active_span_skips_formatting() {
let formatted = AtomicBool::new(false);
record_kernel_variant!("gemv", "{}", {
formatted.store(true, Ordering::Relaxed);
"M==1 decode"
});
assert!(!formatted.load(Ordering::Relaxed));
}
#[test]
fn record_kernel_variant_annotates_active_span() {
use onnx_runtime_tracer::TraceContext;
let (trace, events) = TraceContext::in_memory();
{
let _span = trace.span("MatMulNBits", "op");
record_kernel_variant!(
"gemv_f16",
"K={}, N={} chose the vectorized GEMV",
4864,
896
);
}
let events = events.events();
assert_eq!(events.len(), 1);
let args = events[0].args.as_ref().unwrap();
assert_eq!(args[ARG_KERNEL_VARIANT], "gemv_f16");
assert!(
args[ARG_KERNEL_VARIANT_REASON]
.as_str()
.unwrap()
.contains("K=4864, N=896")
);
}
#[test]
fn record_kernel_variant_stage_namespaces_multiple_subdecisions() {
use onnx_runtime_tracer::TraceContext;
let (trace, events) = TraceContext::in_memory();
{
let _span = trace.span("GroupQueryAttention", "op");
record_kernel_variant_stage!("prep", "gqa_prep_fused", "Sq==1, even head_dim");
record_kernel_variant!("attention_gqa_decode_fp16_splitk", "split-K decode");
}
let events = events.events();
assert_eq!(events.len(), 1);
let args = events[0].args.as_ref().unwrap();
assert_eq!(args[ARG_KERNEL_VARIANT], "attention_gqa_decode_fp16_splitk");
assert_eq!(args["kernel_variant.prep"], "gqa_prep_fused");
assert!(
args["kernel_variant_reason.prep"]
.as_str()
.unwrap()
.contains("even head_dim")
);
}
struct DecliningCaptureKernel;
impl Kernel for DecliningCaptureKernel {
fn execute(&self, _inputs: &[TensorView], _outputs: &mut [TensorMut]) -> Result<()> {
Ok(())
}
fn capture_support(&self) -> CaptureSupport {
CaptureSupport::unsupported("per-call workspace allocation is not capturable")
}
}
#[test]
fn cuda_graph_compatible_shim_matches_capture_support() {
let supported = LegacyKernel {
called: Arc::new(AtomicBool::new(false)),
};
assert_eq!(
supported.cuda_graph_compatible(),
supported.capture_support().is_supported()
);
let unsupported = DecliningCaptureKernel;
assert_eq!(
unsupported.cuda_graph_compatible(),
unsupported.capture_support().is_supported()
);
assert_eq!(
unsupported.capture_support().reason(),
Some("per-call workspace allocation is not capturable")
);
}
struct LegacyKernel {
called: Arc<AtomicBool>,
}
impl Kernel for LegacyKernel {
fn execute(&self, inputs: &[TensorView], _outputs: &mut [TensorMut]) -> Result<()> {
assert_eq!(inputs.len(), 1);
assert_eq!(inputs[0].shape, &[4]);
self.called.store(true, Ordering::Relaxed);
Ok(())
}
}
#[test]
fn legacy_kernel_adapter_receives_the_resident_tensor_path() {
let called = Arc::new(AtomicBool::new(false));
let kernel = LegacyKernel {
called: Arc::clone(&called),
};
let bytes = [1u8, 2, 3, 4];
let shape = [4usize];
let strides = [1i64];
let inputs = [KernelInput::Tensor(TensorView::new(
DevicePtr(bytes.as_ptr().cast()),
onnx_runtime_ir::DataType::Uint8,
&shape,
&strides,
DeviceId::cpu(),
))];
kernel.execute_with_inputs(&inputs, &mut []).unwrap();
assert!(called.load(Ordering::Relaxed));
}
}
#[cfg(test)]
mod trace_standard_tests {
use super::*;
use onnx_runtime_tracer::{TraceContext, TraceVerbosity, set_global_context};
#[test]
fn worker_span_is_gated_on_full_verbosity() {
set_global_context(None);
assert!(
kernel_worker_span("probe").is_none(),
"a worker span opened with no ambient context installed"
);
let (context, _collector) = TraceContext::in_memory();
set_global_context(Some(context.with_verbosity(TraceVerbosity::Ops)));
assert!(
kernel_worker_span("probe").is_none(),
"a worker span opened at Ops verbosity, which would put the \
per-worker cost on every ordinary traced run"
);
let (context, collector) = TraceContext::in_memory();
set_global_context(Some(context.with_verbosity(TraceVerbosity::Full)));
{
let span = kernel_worker_span("probe");
assert!(span.is_some(), "no worker span at Full verbosity");
}
let events = collector.events();
assert_eq!(events.len(), 1, "expected exactly one recorded worker span");
assert_eq!(events[0].cat, CAT_KERNEL_WORKER);
assert_eq!(events[0].name, "probe");
set_global_context(None);
}
}