use std::cell::Cell;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use onnx_runtime_tracer::{Args, MemoryCollector, SpanGuard, TraceContext};
use std::sync::Arc;
pub enum PathMark {
Fast(&'static str),
Composed(String),
}
pub const TRACE_ENV: &str = "ONNX_GENAI_MLX_TRACE";
pub const SIGNPOST_ENV: &str = "ONNX_GENAI_MLX_SIGNPOST";
pub const GPU_CAPTURE_ENV: &str = "ONNX_GENAI_MLX_GPU_CAPTURE";
pub const GPU_CAPTURE_EVAL_ENV: &str = "ONNX_GENAI_MLX_GPU_CAPTURE_EVAL";
pub const VERBOSE_ENV: &str = "ONNX_GENAI_MLX_VERBOSE";
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ComputePath {
Decode,
Prefill,
General,
Eager,
}
impl ComputePath {
pub fn as_str(self) -> &'static str {
match self {
ComputePath::Decode => "decode",
ComputePath::Prefill => "prefill",
ComputePath::General => "general",
ComputePath::Eager => "eager",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CacheState {
Hit,
Miss,
Retrace,
Na,
}
impl CacheState {
pub fn as_str(self) -> &'static str {
match self {
CacheState::Hit => "HIT",
CacheState::Miss => "MISS",
CacheState::Retrace => "RETRACE",
CacheState::Na => "n/a",
}
}
}
#[derive(Default)]
struct Summary {
getcap_calls: u64,
claimed_nodes: u64,
total_nodes: u64,
fused_subgraphs: u64,
rejected: std::collections::BTreeMap<String, (u64, String)>,
path_counts: [u64; 4],
cache_hit: u64,
cache_miss: u64,
cache_retrace: u64,
seen_shape_keys: HashMap<&'static str, std::collections::HashSet<String>>,
managed_wrap_count: u64,
managed_wrap_bytes: u64,
managed_wrap_aligned: u64,
copy_wrap_count: u64,
copy_wrap_bytes: u64,
delta_copyout_count: u64,
delta_copyout_bytes: u64,
full_copyout_count: u64,
full_copyout_bytes: u64,
phase_us: std::collections::BTreeMap<&'static str, (u64, u64)>,
}
static TRACER: OnceLock<MlxTracer> = OnceLock::new();
pub fn tracer() -> &'static MlxTracer {
TRACER.get_or_init(MlxTracer::new)
}
thread_local! {
static THREAD_NAMED: Cell<bool> = const { Cell::new(false) };
}
struct CounterSample {
track: String,
key: String,
value: f64,
ts: u64,
}
pub struct MlxTracer {
ctx: TraceContext,
mem: Option<Arc<MemoryCollector>>,
path: Option<PathBuf>,
counters: Mutex<Vec<CounterSample>>,
composed_counts: Mutex<HashMap<String, u64>>,
signpost_log: usize,
device: usize,
capture_path: Option<PathBuf>,
capture_eval_target: u64,
eval_seq: std::sync::atomic::AtomicU64,
capture_done: AtomicBool,
op_times: Mutex<HashMap<String, (u64, u64)>>,
gpu_util: Mutex<Option<ioreport::GpuUtil>>,
summary: Mutex<Summary>,
verbose: bool,
}
unsafe impl Send for MlxTracer {}
unsafe impl Sync for MlxTracer {}
impl MlxTracer {
fn new() -> Self {
let path = std::env::var(TRACE_ENV).ok().filter(|s| !s.is_empty());
let trace_on = path.is_some();
let (ctx, mem) = if trace_on {
let (ctx, mem) = TraceContext::in_memory();
ctx.set_process_name("onnxruntime-mlx-ep");
(ctx, Some(mem))
} else {
(TraceContext::noop(), None)
};
let capture_path = std::env::var(GPU_CAPTURE_ENV)
.ok()
.filter(|s| !s.is_empty())
.map(|s| {
if s == "1" {
PathBuf::from("mlx_capture.gputrace")
} else {
PathBuf::from(s)
}
});
let capture_eval_target = std::env::var(GPU_CAPTURE_EVAL_ENV)
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.unwrap_or(0);
let signpost_on = trace_on
|| std::env::var(SIGNPOST_ENV)
.map(|v| v == "1")
.unwrap_or(false);
let signpost_log = if signpost_on {
signpost::create_log()
} else {
0
};
let device = if trace_on { gpu::default_device() } else { 0 };
let gpu_util = if trace_on { ioreport::GpuUtil::new() } else { None };
let verbose = trace_on
|| std::env::var(VERBOSE_ENV)
.map(|v| v == "1")
.unwrap_or(false);
MlxTracer {
ctx,
mem,
path: path.map(PathBuf::from),
counters: Mutex::new(Vec::new()),
composed_counts: Mutex::new(HashMap::new()),
signpost_log,
device,
capture_path,
capture_eval_target,
eval_seq: std::sync::atomic::AtomicU64::new(0),
capture_done: AtomicBool::new(false),
op_times: Mutex::new(HashMap::new()),
gpu_util: Mutex::new(gpu_util),
summary: Mutex::new(Summary::default()),
verbose,
}
}
#[inline]
pub fn is_enabled(&self) -> bool {
self.ctx.is_enabled()
}
#[inline]
pub fn active(&self) -> bool {
self.is_enabled() || self.verbose
}
fn push_counter(&self, track: &str, key: &str, value: f64) {
let ts = self.ctx.clock().now_micros();
let mut c = match self.counters.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
c.push(CounterSample {
track: track.to_string(),
key: key.to_string(),
value,
ts,
});
}
pub fn record_claim(
&self,
claimed: usize,
total: usize,
subgraphs: usize,
rejected: &[(String, usize, String)],
) {
if !self.active() {
return;
}
let unclaimed = total.saturating_sub(claimed);
{
let mut s = match self.summary.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
s.getcap_calls += 1;
s.claimed_nodes += claimed as u64;
s.total_nodes += total as u64;
s.fused_subgraphs += subgraphs as u64;
for (op, n, reason) in rejected {
let e = s.rejected.entry(op.clone()).or_insert((0, String::new()));
e.0 += *n as u64;
if !reason.is_empty() {
e.1 = reason.clone();
}
}
}
if self.is_enabled() {
let mut args = Args::new()
.with("claimed", claimed as u64)
.with("total", total as u64)
.with("unclaimed", unclaimed as u64)
.with("fused_subgraphs", subgraphs as u64);
for (op, n, reason) in rejected {
args = args.with(
format!("fallback_{op}"),
format!("x{n}: {reason}"),
);
}
self.ctx.instant("mlx.getcapability", "ep.claim", Some(args));
self.push_counter("mlx.claimed_nodes", "nodes", claimed as f64);
self.push_counter("mlx.unclaimed_nodes", "nodes", unclaimed as f64);
self.push_counter("mlx.fused_subgraphs", "subgraphs", subgraphs as f64);
}
}
pub fn record_compute_path(
&self,
path: ComputePath,
cache: CacheState,
shape_key: &str,
node_count: usize,
) -> CacheState {
if !self.active() {
return cache;
}
let tag = path.as_str();
let resolved = {
let mut s = match self.summary.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let idx = match path {
ComputePath::Decode => 0,
ComputePath::Prefill => 1,
ComputePath::General => 2,
ComputePath::Eager => 3,
};
s.path_counts[idx] += 1;
let resolved = match cache {
CacheState::Hit if !shape_key.is_empty() => {
let seen = s
.seen_shape_keys
.get(tag)
.map(|set| set.contains(shape_key))
.unwrap_or(false);
if seen {
CacheState::Hit
} else {
CacheState::Retrace
}
}
other => other,
};
if !shape_key.is_empty() {
s.seen_shape_keys.entry(tag).or_default().insert(shape_key.to_string());
}
match resolved {
CacheState::Hit => s.cache_hit += 1,
CacheState::Miss => s.cache_miss += 1,
CacheState::Retrace => s.cache_retrace += 1,
CacheState::Na => {}
}
resolved
};
if self.is_enabled() {
let mut args = Args::new()
.with("path", tag)
.with("cache", resolved.as_str())
.with("nodes", node_count as u64);
if !shape_key.is_empty() {
args = args.with("shape_key", shape_key.to_string());
}
self.ctx.instant(format!("mlx.compute[{tag}]"), "ep.path", Some(args));
self.push_counter("mlx.compute_path", tag, 1.0);
}
resolved
}
pub fn record_managed_wrap(&self, bytes: u64, aligned: bool) {
if !self.active() {
return;
}
{
let mut s = match self.summary.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
s.managed_wrap_count += 1;
s.managed_wrap_bytes += bytes;
if aligned {
s.managed_wrap_aligned += 1;
}
}
if self.is_enabled() {
self.push_counter("mlx.mem_wrap_bytes", "bytes", bytes as f64);
}
}
pub fn record_copy_wrap(&self, bytes: u64) {
if !self.active() {
return;
}
let mut s = match self.summary.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
s.copy_wrap_count += 1;
s.copy_wrap_bytes += bytes;
}
pub fn record_copyout(&self, delta: bool, bytes: u64, dur: Duration) {
if !self.active() {
return;
}
{
let mut s = match self.summary.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if delta {
s.delta_copyout_count += 1;
s.delta_copyout_bytes += bytes;
} else {
s.full_copyout_count += 1;
s.full_copyout_bytes += bytes;
}
let e = s.phase_us.entry("copy").or_insert((0, 0));
e.0 += dur.as_micros() as u64;
e.1 += 1;
}
if self.is_enabled() {
self.push_counter("mlx.copyout_bytes", if delta { "delta" } else { "full" }, bytes as f64);
}
}
pub fn record_phase(&self, phase: &'static str, dur: Duration) {
if !self.active() {
return;
}
let mut s = match self.summary.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let e = s.phase_us.entry(phase).or_insert((0, 0));
e.0 += dur.as_micros() as u64;
e.1 += 1;
}
#[inline]
pub fn phase(&self, phase: &'static str) -> Option<PhaseGuard> {
if !self.active() {
return None;
}
let span = self.ctx.span(format!("mlx.{phase}"), "ep.phase");
Some(PhaseGuard {
phase,
start: Instant::now(),
_span: span,
})
}
pub fn log_summary(&self) {
if !self.active() {
return;
}
let s = match self.summary.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if s.getcap_calls == 0 && s.path_counts.iter().all(|&n| n == 0) {
return;
}
let mut out = String::new();
out.push_str("[rust-mlx-ep] ===== MLX EP session summary =====\n");
let claim_pct = if s.total_nodes > 0 {
(s.claimed_nodes as f64 / s.total_nodes as f64) * 100.0
} else {
0.0
};
out.push_str(&format!(
" claim: {}/{} nodes claimed ({claim_pct:.1}%) across {} fused subgraph(s), {} GetCapability call(s)\n",
s.claimed_nodes, s.total_nodes, s.fused_subgraphs, s.getcap_calls
));
if !s.rejected.is_empty() {
let mut items: Vec<(&String, &(u64, String))> = s.rejected.iter().collect();
items.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
out.push_str(" unclaimed (→ CPU):\n");
for (op, (n, reason)) in items.iter().take(8) {
let why = if reason.is_empty() { "no MLX handler / opset" } else { reason };
out.push_str(&format!(" - {op} x{n}: {why}\n"));
}
}
out.push_str(&format!(
" compute: decode={} prefill={} general={} eager={} (cache: {} HIT / {} MISS / {} RETRACE)\n",
s.path_counts[0], s.path_counts[1], s.path_counts[2], s.path_counts[3],
s.cache_hit, s.cache_miss, s.cache_retrace
));
out.push_str(&format!(
" memory: managed-wrap {} ({} zero-copy aligned), {:.2} MiB borrowed; copy-wrap {}, {:.2} MiB\n",
s.managed_wrap_count, s.managed_wrap_aligned,
s.managed_wrap_bytes as f64 / (1024.0 * 1024.0),
s.copy_wrap_count, s.copy_wrap_bytes as f64 / (1024.0 * 1024.0)
));
out.push_str(&format!(
" copy-out: delta {} ({:.2} MiB) vs full {} ({:.2} MiB)\n",
s.delta_copyout_count, s.delta_copyout_bytes as f64 / (1024.0 * 1024.0),
s.full_copyout_count, s.full_copyout_bytes as f64 / (1024.0 * 1024.0)
));
if !s.phase_us.is_empty() {
out.push_str(" timing: ");
let mut parts: Vec<String> = Vec::new();
for (phase, (us, calls)) in s.phase_us.iter() {
parts.push(format!("{phase}={}us (x{calls})", us));
}
out.push_str(&parts.join(", "));
out.push('\n');
}
out.push_str("[rust-mlx-ep] ===================================\n");
eprint!("{out}");
if self.is_enabled() {
let args = Args::new()
.with("claimed_nodes", s.claimed_nodes)
.with("total_nodes", s.total_nodes)
.with("claim_pct", format!("{claim_pct:.1}"))
.with("fused_subgraphs", s.fused_subgraphs)
.with("path_decode", s.path_counts[0])
.with("path_prefill", s.path_counts[1])
.with("path_general", s.path_counts[2])
.with("path_eager", s.path_counts[3])
.with("cache_hit", s.cache_hit)
.with("cache_miss", s.cache_miss)
.with("cache_retrace", s.cache_retrace)
.with("managed_wrap_bytes", s.managed_wrap_bytes)
.with("managed_wrap_aligned", s.managed_wrap_aligned)
.with("delta_copyout_bytes", s.delta_copyout_bytes)
.with("full_copyout_bytes", s.full_copyout_bytes);
self.ctx.instant("mlx.session_summary", "summary", Some(args));
}
}
pub fn note_thread(&self, name: &str) {
if !self.is_enabled() {
return;
}
THREAD_NAMED.with(|n| {
if !n.get() {
self.ctx.set_thread_name(name);
n.set(true);
}
});
}
pub fn subgraph_region(&self, node_count: usize) -> Region {
let span = self
.ctx
.span("mlx.subgraph", "ep")
.with_args(Args::new().with("nodes", node_count as u64));
let sp = signpost::interval_begin(self.signpost_log, SP_SUBGRAPH);
Region { _span: span, signpost: sp }
}
pub fn eval_region(&self) -> Region {
let span = self
.ctx
.span("mlx.eval", "gpu")
.with_args(Args::new().device("gpu"));
let sp = signpost::interval_begin(self.signpost_log, SP_EVAL);
Region { _span: span, signpost: sp }
}
pub fn op_span(&self, op_type: &str, num_inputs: usize, num_outputs: usize) -> SpanGuard {
if !self.is_enabled() {
return self.ctx.span(op_type, "op"); }
self.ctx.span(op_type.to_string(), "op").with_args(
Args::new()
.with("op_type", op_type.to_string())
.with("inputs", num_inputs as u64)
.with("outputs", num_outputs as u64),
)
}
#[inline]
pub fn op_timer_start(&self) -> Option<Instant> {
if self.is_enabled() {
Some(Instant::now())
} else {
None
}
}
pub fn record_op_path(&self, op_type: &str, start: Option<Instant>, mark: Option<PathMark>) {
if !self.is_enabled() {
return;
}
let Some(mark) = mark else {
return;
};
match mark {
PathMark::Fast(kernel) => {
if let Some(start) = start {
self.ctx.complete(
format!("{op_type} [fast]"),
"op.fast",
start,
start.elapsed(),
Some(
Args::new()
.with("op_type", op_type.to_string())
.with("optimized", true)
.with("kernel", kernel),
),
);
}
}
PathMark::Composed(reason) => {
if let Some(start) = start {
self.ctx.complete(
format!("{op_type} [composed]"),
"op.composed",
start,
start.elapsed(),
Some(
Args::new()
.with("op_type", op_type.to_string())
.with("optimized", false)
.with("reason", reason.clone()),
),
);
}
self.ctx.instant(
format!("⚠ composed-path: {op_type} ({reason})"),
"op.composed",
Some(
Args::new()
.with("op_type", op_type.to_string())
.with("optimized", false)
.with("reason", reason),
),
);
self.bump_composed_counter(op_type);
}
}
}
fn bump_composed_counter(&self, op_type: &str) {
let ts = self.ctx.clock().now_micros();
let value = {
let mut counts = match self.composed_counts.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let n = counts.entry(op_type.to_string()).or_insert(0);
*n += 1;
*n as f64
};
let mut c = match self.counters.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
c.push(CounterSample {
track: "mlx.composed_path_count".to_string(),
key: op_type.to_string(),
value,
ts,
});
}
#[allow(clippy::too_many_arguments)]
pub fn record_op_meta(
&self,
op_type: &str,
start: Instant,
dur: Duration,
out_shapes: &str,
in_shapes: &str,
dtype: &str,
elements: u64,
bytes: u64,
) {
if !self.is_enabled() {
return;
}
self.ctx.complete(
op_type.to_string(),
"op",
start,
dur,
Some(
Args::new()
.with("op_type", op_type.to_string())
.with("output_shapes", out_shapes.to_string())
.with("input_shapes", in_shapes.to_string())
.with("dtype", dtype.to_string())
.with("elements", elements)
.with("bytes", bytes),
),
);
self.record_op_time(op_type, dur.as_micros() as u64);
}
fn record_op_time(&self, op_type: &str, us: u64) {
let mut m = match self.op_times.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(e) = m.get_mut(op_type) {
e.0 += us;
e.1 += 1;
} else {
m.insert(op_type.to_string(), (us, 1));
}
}
pub fn log_slowest_ops(&self) {
if !self.is_enabled() {
return;
}
let snapshot: Vec<(String, u64, u64)> = {
let m = match self.op_times.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
m.iter().map(|(k, v)| (k.clone(), v.0, v.1)).collect()
};
if snapshot.is_empty() {
return;
}
let total: u64 = snapshot.iter().map(|(_, us, _)| *us).sum();
let mut ranked = snapshot;
ranked.sort_by(|a, b| b.1.cmp(&a.1));
ranked.truncate(10);
let kind = "build-time (fusion intact; per-kernel GPU detail: ONNX_GENAI_MLX_GPU_CAPTURE)";
let denom = total.max(1) as f64;
let mut lines = String::new();
lines.push_str(&format!(
"[rust-mlx-ep] slowest ops ({kind}), total {total} us across {} op-type(s):\n",
ranked.len()
));
let mut args = Args::new().with("timing_kind", kind).with("total_us", total);
for (i, (op, us, calls)) in ranked.iter().enumerate() {
let pct = (*us as f64 / denom) * 100.0;
lines.push_str(&format!(
" {:>2}. {:<20} {:>10} us {:>5.1}% ({} call(s))\n",
i + 1,
op,
us,
pct,
calls
));
args = args.with(
format!("{:02}_{op}", i + 1),
format!("{us}us {pct:.1}% x{calls}"),
);
}
eprint!("{lines}");
self.ctx.instant("mlx.slowest_ops", "summary", Some(args));
}
#[must_use]
pub fn begin_gpu_capture(&self) -> Option<CaptureGuard> {
let path = self.capture_path.as_ref()?;
let seq = self.eval_seq.fetch_add(1, Ordering::SeqCst);
if seq != self.capture_eval_target {
return None;
}
if self
.capture_done
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return None;
}
if !metal_capture::is_available() {
eprintln!(
"[rust-mlx-ep] GPU capture requested but Metal is unavailable (mlx_metal_is_available=false); skipping"
);
return None;
}
let capture_layer_on = std::env::var("MTL_CAPTURE_ENABLED")
.map(|v| v == "1")
.unwrap_or(false);
if !capture_layer_on {
eprintln!(
"[rust-mlx-ep] GPU capture requires MTL_CAPTURE_ENABLED=1 to be exported before the \
process starts (the Metal capture layer must be inserted at device creation); \
skipping capture. Re-run with: MTL_CAPTURE_ENABLED=1 ONNX_GENAI_MLX_GPU_CAPTURE={} ...",
path.to_string_lossy()
);
return None;
}
let path_str = path.to_string_lossy().to_string();
if metal_capture::start(&path_str) {
eprintln!(
"[rust-mlx-ep] Metal GPU capture STARTED → {path_str} (eval #{})",
self.capture_eval_target
);
Some(CaptureGuard { path: path_str })
} else {
eprintln!(
"[rust-mlx-ep] GPU capture start FAILED for {path_str} \
(capture requires MTL_CAPTURE_ENABLED=1 in the environment and a path ending in .gputrace)"
);
None
}
}
pub fn sample_gpu_counters(&self) {
if !self.is_enabled() || self.device == 0 {
return;
}
let dev = self.device;
let allocated = gpu::msg_u64(dev, b"currentAllocatedSize\0") as f64;
let recommended = gpu::msg_u64(dev, b"recommendedMaxWorkingSetSize\0") as f64;
let ts = self.ctx.clock().now_micros();
let mut c = match self.counters.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
c.push(CounterSample {
track: "mlx.gpu_mem_bytes".to_string(),
key: "bytes".to_string(),
value: allocated,
ts,
});
if recommended > 0.0 {
c.push(CounterSample {
track: "mlx.gpu_mem_pct".to_string(),
key: "pct".to_string(),
value: (allocated / recommended) * 100.0,
ts,
});
}
drop(c);
if let Ok(mut util) = self.gpu_util.lock() {
if let Some(sampler) = util.as_mut() {
if let Some(reading) = sampler.sample() {
let mut c = match self.counters.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
c.push(CounterSample {
track: "mlx.gpu_util_pct".to_string(),
key: "pct".to_string(),
value: reading.active_pct,
ts,
});
if let Some(mhz) = reading.freq_mhz {
c.push(CounterSample {
track: "mlx.gpu_freq_mhz".to_string(),
key: "mhz".to_string(),
value: mhz,
ts,
});
}
}
}
}
}
pub fn export(&self) {
if !self.is_enabled() {
return;
}
let (Some(mem), Some(path)) = (&self.mem, &self.path) else {
return;
};
let mut out = mem.to_chrome_json();
let pid = self.ctx.pid();
let counters = match self.counters.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let mut tail = String::new();
for c in counters.iter() {
tail.push_str(&format!(
",{{\"name\":\"{}\",\"cat\":\"gpu_counter\",\"ph\":\"C\",\"ts\":{},\
\"pid\":{},\"tid\":0,\"args\":{{\"{}\":{}}}}}",
c.track, c.ts, pid, c.key, c.value
));
}
if out.ends_with(']') {
out.pop();
let had_events = out.len() > 1; if !tail.is_empty() {
if had_events {
out.push_str(&tail);
} else {
out.push_str(&tail[1..]); }
}
out.push(']');
}
match std::fs::write(path, &out) {
Ok(()) => eprintln!(
"[rust-mlx-ep] wrote MLX trace ({} span event(s), {} counter sample(s)) to {}",
mem.len(),
counters.len(),
path.display()
),
Err(e) => eprintln!("[rust-mlx-ep] trace export to {} failed: {e}", path.display()),
}
}
}
#[must_use = "a Region records its span/interval only while alive; drop it at the end of the region"]
pub struct Region {
_span: SpanGuard,
signpost: Option<signpost::Interval>,
}
impl Drop for Region {
fn drop(&mut self) {
if let Some(iv) = self.signpost.take() {
iv.end();
}
}
}
#[must_use = "a PhaseGuard times its region only while alive; drop it at the end of the phase"]
pub struct PhaseGuard {
phase: &'static str,
start: Instant,
_span: SpanGuard,
}
impl Drop for PhaseGuard {
fn drop(&mut self) {
tracer().record_phase(self.phase, self.start.elapsed());
}
}
#[must_use = "the GPU capture only covers the region this guard is alive for"]
pub struct CaptureGuard {
path: String,
}
impl Drop for CaptureGuard {
fn drop(&mut self) {
metal_capture::stop();
eprintln!(
"[rust-mlx-ep] Metal GPU capture STOPPED → wrote {} (open in Xcode: `open {}`)",
self.path, self.path
);
}
}
const SP_SUBGRAPH: &[u8] = b"mlx.subgraph\0";
const SP_EVAL: &[u8] = b"mlx.eval\0";
mod gpu {
use std::os::raw::{c_char, c_void};
#[allow(non_camel_case_types)]
type SEL = *const c_void;
unsafe extern "C" {
fn MTLCreateSystemDefaultDevice() -> *mut c_void;
fn sel_registerName(name: *const c_char) -> SEL;
fn objc_msgSend();
}
pub fn default_device() -> usize {
unsafe { MTLCreateSystemDefaultDevice() as usize }
}
pub fn msg_u64(obj: usize, sel_name: &[u8]) -> u64 {
if obj == 0 {
return 0;
}
unsafe {
let sel = sel_registerName(sel_name.as_ptr() as *const c_char);
let send: extern "C" fn(*mut c_void, SEL) -> u64 =
std::mem::transmute(objc_msgSend as *const c_void);
send(obj as *mut c_void, sel)
}
}
}
mod signpost {
use std::os::raw::{c_char, c_void};
type OsLog = *mut c_void;
const OS_SIGNPOST_INTERVAL_BEGIN: u8 = 1;
const OS_SIGNPOST_INTERVAL_END: u8 = 2;
unsafe extern "C" {
fn os_log_create(subsystem: *const c_char, category: *const c_char) -> OsLog;
fn os_signpost_id_generate(log: OsLog) -> u64;
fn _os_signpost_emit_with_name_impl(
dso: *mut c_void,
log: OsLog,
ty: u8,
spid: u64,
name: *const c_char,
format: *const c_char,
buf: *mut u8,
size: u32,
);
static __dso_handle: c_void;
}
pub fn create_log() -> usize {
let subsystem = b"com.onnxruntime.mlx\0";
let category = b"MLXExecutionProvider\0";
unsafe {
os_log_create(
subsystem.as_ptr() as *const c_char,
category.as_ptr() as *const c_char,
) as usize
}
}
pub struct Interval {
log: usize,
id: u64,
name: *const c_char,
}
impl Interval {
pub fn end(self) {
let mut buf: [u8; 2] = [0, 0];
unsafe {
_os_signpost_emit_with_name_impl(
&__dso_handle as *const c_void as *mut c_void,
self.log as OsLog,
OS_SIGNPOST_INTERVAL_END,
self.id,
self.name,
b"\0".as_ptr() as *const c_char,
buf.as_mut_ptr(),
buf.len() as u32,
);
}
}
}
pub fn interval_begin(log: usize, name: &'static [u8]) -> Option<Interval> {
if log == 0 {
return None;
}
let name_ptr = name.as_ptr() as *const c_char;
let mut buf: [u8; 2] = [0, 0];
unsafe {
let id = os_signpost_id_generate(log as OsLog);
_os_signpost_emit_with_name_impl(
&__dso_handle as *const c_void as *mut c_void,
log as OsLog,
OS_SIGNPOST_INTERVAL_BEGIN,
id,
name_ptr,
b"\0".as_ptr() as *const c_char,
buf.as_mut_ptr(),
buf.len() as u32,
);
Some(Interval { log, id, name: name_ptr })
}
}
}
mod metal_capture {
use crate::sys::mlx;
use std::ffi::CString;
pub fn is_available() -> bool {
let mut res = false;
let rc = unsafe { mlx::mlx_metal_is_available(&mut res as *mut bool) };
rc == 0 && res
}
pub fn start(path: &str) -> bool {
let Ok(c) = CString::new(path) else {
return false;
};
let rc = unsafe { mlx::mlx_metal_start_capture(c.as_ptr()) };
rc == 0
}
pub fn stop() {
unsafe {
mlx::mlx_metal_stop_capture();
}
}
}
mod ioreport {
use std::os::raw::{c_char, c_int, c_longlong, c_void};
type CFTypeRef = *const c_void;
type CFDictionaryRef = *const c_void;
type CFMutableDictionaryRef = *mut c_void;
type CFStringRef = *const c_void;
type IOReportSubscriptionRef = *const c_void;
type IOReportSampleRef = *const c_void;
const RTLD_NOW: c_int = 2;
const CF_UTF8: u32 = 0x0800_0100;
const K_IO_REPORT_ITER_OK: c_int = 0;
unsafe extern "C" {
fn dlopen(path: *const c_char, mode: c_int) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}
type CFStringCreateWithCStringFn =
unsafe extern "C" fn(CFTypeRef, *const c_char, u32) -> CFStringRef;
type CFStringGetCStringFn =
unsafe extern "C" fn(CFStringRef, *mut c_char, c_longlong, u32) -> bool;
type CFReleaseFn = unsafe extern "C" fn(CFTypeRef);
type IOReportCopyChannelsInGroupFn = unsafe extern "C" fn(
CFStringRef,
CFStringRef,
u64,
u64,
u64,
) -> CFMutableDictionaryRef;
type IOReportCreateSubscriptionFn = unsafe extern "C" fn(
*mut c_void,
CFMutableDictionaryRef,
*mut CFMutableDictionaryRef,
u64,
CFTypeRef,
) -> IOReportSubscriptionRef;
type IOReportCreateSamplesFn = unsafe extern "C" fn(
IOReportSubscriptionRef,
CFMutableDictionaryRef,
CFTypeRef,
) -> CFDictionaryRef;
type IOReportCreateSamplesDeltaFn =
unsafe extern "C" fn(CFDictionaryRef, CFDictionaryRef, CFTypeRef) -> CFDictionaryRef;
type IOReportIterateFn = unsafe extern "C" fn(CFDictionaryRef, *const c_void);
type IOReportChannelGetGroupFn = unsafe extern "C" fn(IOReportSampleRef) -> CFStringRef;
type IOReportChannelGetChannelNameFn =
unsafe extern "C" fn(IOReportSampleRef) -> CFStringRef;
type IOReportStateGetCountFn = unsafe extern "C" fn(IOReportSampleRef) -> c_int;
type IOReportStateGetNameForIndexFn =
unsafe extern "C" fn(IOReportSampleRef, c_int) -> CFStringRef;
type IOReportStateGetResidencyFn =
unsafe extern "C" fn(IOReportSampleRef, c_int) -> c_longlong;
pub struct Reading {
pub active_pct: f64,
pub freq_mhz: Option<f64>,
}
pub struct GpuUtil {
sub: IOReportSubscriptionRef,
channels: CFMutableDictionaryRef,
prev: CFDictionaryRef,
cf_release: CFReleaseFn,
create_samples: IOReportCreateSamplesFn,
create_delta: IOReportCreateSamplesDeltaFn,
iterate: IOReportIterateFn,
}
unsafe impl Send for GpuUtil {}
thread_local! {
static ACC: std::cell::Cell<(i64, i64)> = const { std::cell::Cell::new((0, 0)) };
}
#[repr(C)]
struct BlockDescriptor {
reserved: u64,
size: u64,
}
#[repr(C)]
struct Block {
isa: *const c_void,
flags: c_int,
reserved: c_int,
invoke: extern "C" fn(*mut Block, IOReportSampleRef) -> c_int,
descriptor: *const BlockDescriptor,
}
unsafe impl Sync for Block {}
unsafe extern "C" {
static _NSConcreteGlobalBlock: [*const c_void; 32];
}
static BLOCK_DESCRIPTOR: BlockDescriptor = BlockDescriptor {
reserved: 0,
size: std::mem::size_of::<Block>() as u64,
};
const BLOCK_IS_GLOBAL: c_int = 1 << 28;
struct StateAccessors {
get_group: IOReportChannelGetGroupFn,
get_channel: IOReportChannelGetChannelNameFn,
state_count: IOReportStateGetCountFn,
state_name: IOReportStateGetNameForIndexFn,
state_resid: IOReportStateGetResidencyFn,
get_cstring: CFStringGetCStringFn,
cf_release: CFReleaseFn,
}
static mut ACCESSORS: Option<StateAccessors> = None;
fn cfstr_to_string(get_cstring: CFStringGetCStringFn, s: CFStringRef) -> String {
if s.is_null() {
return String::new();
}
let mut buf = [0i8; 128];
let ok = unsafe { get_cstring(s, buf.as_mut_ptr(), buf.len() as c_longlong, CF_UTF8) };
if !ok {
return String::new();
}
let cstr = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
cstr.to_string_lossy().into_owned()
}
extern "C" fn iterate_block(_blk: *mut Block, ch: IOReportSampleRef) -> c_int {
let acc = unsafe {
let ptr = std::ptr::addr_of!(ACCESSORS);
match &*ptr {
Some(a) => a,
None => return K_IO_REPORT_ITER_OK,
}
};
let group = cfstr_to_string(acc.get_cstring, unsafe { (acc.get_group)(ch) });
let channel = cfstr_to_string(acc.get_cstring, unsafe { (acc.get_channel)(ch) });
let n = unsafe { (acc.state_count)(ch) };
if group != "GPU Stats" || channel != "GPUPH" || n <= 0 {
return K_IO_REPORT_ITER_OK;
}
let (mut idle, mut total) = (0i64, 0i64);
for i in 0..n {
let name_ref = unsafe { (acc.state_name)(ch, i) };
let name = cfstr_to_string(acc.get_cstring, name_ref);
unsafe { (acc.cf_release)(name_ref) };
let resid = unsafe { (acc.state_resid)(ch, i) };
total += resid;
let up = name.to_ascii_uppercase();
if up.contains("IDLE") || up.contains("OFF") || up.contains("DOWN") {
idle += resid;
}
}
ACC.with(|c| {
let (pi, pt) = c.get();
c.set((pi + idle, pt + total));
});
K_IO_REPORT_ITER_OK
}
static ITER_BLOCK: Block = Block {
isa: unsafe { _NSConcreteGlobalBlock.as_ptr() as *const c_void },
flags: BLOCK_IS_GLOBAL,
reserved: 0,
invoke: iterate_block,
descriptor: &BLOCK_DESCRIPTOR,
};
unsafe fn sym<T>(handle: *mut c_void, name: &[u8]) -> Option<T> {
let p = unsafe { dlsym(handle, name.as_ptr() as *const c_char) };
if p.is_null() {
None
} else {
Some(unsafe { std::mem::transmute_copy::<*mut c_void, T>(&p) })
}
}
impl GpuUtil {
pub fn new() -> Option<GpuUtil> {
unsafe {
let cf = dlopen(
b"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation\0"
.as_ptr() as *const c_char,
RTLD_NOW,
);
let mut ior = dlopen(
b"/usr/lib/libIOReport.dylib\0".as_ptr() as *const c_char,
RTLD_NOW,
);
if ior.is_null() {
ior = dlopen(
b"/System/Library/PrivateFrameworks/IOReport.framework/IOReport\0"
.as_ptr() as *const c_char,
RTLD_NOW,
);
}
if cf.is_null() || ior.is_null() {
return None;
}
let cfstr_create: CFStringCreateWithCStringFn =
sym(cf, b"CFStringCreateWithCString\0")?;
let cf_get_cstring: CFStringGetCStringFn = sym(cf, b"CFStringGetCString\0")?;
let cf_release: CFReleaseFn = sym(cf, b"CFRelease\0")?;
let copy_channels: IOReportCopyChannelsInGroupFn =
sym(ior, b"IOReportCopyChannelsInGroup\0")?;
let create_sub: IOReportCreateSubscriptionFn =
sym(ior, b"IOReportCreateSubscription\0")?;
let create_samples: IOReportCreateSamplesFn =
sym(ior, b"IOReportCreateSamples\0")?;
let create_delta: IOReportCreateSamplesDeltaFn =
sym(ior, b"IOReportCreateSamplesDelta\0")?;
let iterate: IOReportIterateFn = sym(ior, b"IOReportIterate\0")?;
let get_group: IOReportChannelGetGroupFn =
sym(ior, b"IOReportChannelGetGroup\0")?;
let get_channel: IOReportChannelGetChannelNameFn =
sym(ior, b"IOReportChannelGetChannelName\0")?;
let state_count: IOReportStateGetCountFn =
sym(ior, b"IOReportStateGetCount\0")?;
let state_name: IOReportStateGetNameForIndexFn =
sym(ior, b"IOReportStateGetNameForIndex\0")?;
let state_resid: IOReportStateGetResidencyFn =
sym(ior, b"IOReportStateGetResidency\0")?;
let group = cfstr_create(
std::ptr::null(),
b"GPU Stats\0".as_ptr() as *const c_char,
CF_UTF8,
);
if group.is_null() {
return None;
}
let channels = copy_channels(group, std::ptr::null(), 0, 0, 0);
cf_release(group);
if channels.is_null() {
return None;
}
let mut subbed: CFMutableDictionaryRef = std::ptr::null_mut();
let sub = create_sub(
std::ptr::null_mut(),
channels,
&mut subbed as *mut CFMutableDictionaryRef,
0,
std::ptr::null(),
);
if sub.is_null() {
cf_release(channels);
return None;
}
let ptr = std::ptr::addr_of_mut!(ACCESSORS);
*ptr = Some(StateAccessors {
get_group,
get_channel,
state_count,
state_name,
state_resid,
get_cstring: cf_get_cstring,
cf_release,
});
let prev = create_samples(sub, channels, std::ptr::null());
if prev.is_null() {
cf_release(channels);
return None;
}
Some(GpuUtil {
sub,
channels,
prev,
cf_release,
create_samples,
create_delta,
iterate,
})
}
}
pub fn sample(&mut self) -> Option<Reading> {
unsafe {
let cur = (self.create_samples)(self.sub, self.channels, std::ptr::null());
if cur.is_null() {
return None;
}
let delta = (self.create_delta)(self.prev, cur, std::ptr::null());
(self.cf_release)(self.prev);
self.prev = cur;
if delta.is_null() {
return None;
}
ACC.with(|c| c.set((0, 0)));
(self.iterate)(delta, &ITER_BLOCK as *const Block as *const c_void);
(self.cf_release)(delta);
let (idle, total) = ACC.with(|c| c.get());
if total <= 0 {
return None;
}
let active = (total - idle).max(0) as f64;
Some(Reading {
active_pct: (active / total as f64) * 100.0,
freq_mhz: None,
})
}
}
}
impl Drop for GpuUtil {
fn drop(&mut self) {
unsafe {
if !self.prev.is_null() {
(self.cf_release)(self.prev);
}
if !self.channels.is_null() {
(self.cf_release)(self.channels as CFTypeRef);
}
if !self.sub.is_null() {
(self.cf_release)(self.sub);
}
}
}
}
}