#[path = "../../tests/support/workspace_root.rs"]
#[allow(dead_code)]
mod workspace_root;
use std::{
hint::black_box,
path::{Path, PathBuf},
time::Instant,
};
use coremlit::{
ComputeUnits,
embeddings::clap::{
AudioEncoder, AudioEncoderOptions, Embedding, TextEncoder, TextEncoderOptions,
audio::{SAMPLE_RATE_HZ, TARGET_SAMPLES},
},
};
const WARM_RUNS: usize = 25;
const UNITS: [ComputeUnits; 4] = [
ComputeUnits::CpuOnly,
ComputeUnits::All,
ComputeUnits::CpuAndGpu,
ComputeUnits::CpuAndNeuralEngine,
];
const TEXT_PROMPT: &str = "a violin playing a slow melody in a concert hall";
fn main() {
let models = models_dir();
let audio_model = models.join("clap_audio.mlmodelc");
let text_model = models.join("clap_text.mlmodelc");
if !audio_model.is_dir() || !text_model.is_dir() {
eprintln!(
"clap_encode bench skipped: clapkit models not found under {} \
(set CLAPKIT_TEST_MODELS, or fetch to Models/clapkit — see the crate README). \
MEASURED NOTHING.",
models.display()
);
return;
}
println!(
"# clap_encode — CLAP dual-tower encode phases (measured, never marketed)\n\
# models: {}\n\
# warm runs: {WARM_RUNS}\n\
# latency in ms; cos = cosine vs CpuOnly reference (same tower/input); \
rss = current process resident MB (a snapshot, not a peak)\n",
models.display(),
);
bench_audio(&audio_model);
bench_text(&text_model);
println!("\n# current process resident memory: {:.1} MB", rss_mb());
println!(
"# note: rss is a single current snapshot (task resident_size at the moment \
each row prints), NOT a peak and NOT a cumulative running total — encoders \
are dropped between configurations, so a row is neither monotonic nor that \
configuration's isolated footprint. A true peak would need sampling across \
load + inference."
);
}
fn bench_audio(model: &Path) {
println!("## audio tower (HTSAT) — {}", model.display());
print_header();
let samples = deterministic_window(TARGET_SAMPLES);
let mut reference: Option<Embedding> = None;
for unit in UNITS {
let first_load_start = Instant::now();
let priming =
AudioEncoder::from_file_with(model, AudioEncoderOptions::new().with_compute(unit))
.unwrap_or_else(|e| panic!("first-observed load audio [{unit}]: {e}"));
let first_load_ms = ms(first_load_start);
drop(priming);
let load_start = Instant::now();
let encoder =
AudioEncoder::from_file_with(model, AudioEncoderOptions::new().with_compute(unit))
.unwrap_or_else(|e| panic!("cached load audio [{unit}]: {e}"));
let cached_load_ms = ms(load_start);
let first_start = Instant::now();
let first = encoder
.embed_window(&samples)
.unwrap_or_else(|e| panic!("first audio embed [{unit}]: {e}"));
let first_ms = ms(first_start);
let mut warm = Vec::with_capacity(WARM_RUNS);
let mut last = first;
for _ in 0..WARM_RUNS {
let t = Instant::now();
last = black_box(
encoder
.embed_window(black_box(&samples))
.unwrap_or_else(|e| panic!("warm audio embed [{unit}]: {e}")),
);
warm.push(ms(t));
}
let cos = record_cosine(unit, &last, &mut reference);
print_row(
unit,
first_load_ms,
cached_load_ms,
first_ms,
&mut warm,
cos,
&last,
);
}
println!();
}
fn bench_text(model: &Path) {
println!("## text tower (RoBERTa) — {}", model.display());
print_header();
let mut reference: Option<Embedding> = None;
for unit in UNITS {
let first_load_start = Instant::now();
let priming =
TextEncoder::from_bundled_tokenizer(model, TextEncoderOptions::new().with_compute(unit))
.unwrap_or_else(|e| panic!("first-observed load text [{unit}]: {e}"));
let first_load_ms = ms(first_load_start);
drop(priming);
let load_start = Instant::now();
let encoder =
TextEncoder::from_bundled_tokenizer(model, TextEncoderOptions::new().with_compute(unit))
.unwrap_or_else(|e| panic!("cached load text [{unit}]: {e}"));
let cached_load_ms = ms(load_start);
let first_start = Instant::now();
let first = encoder
.embed(TEXT_PROMPT)
.unwrap_or_else(|e| panic!("first text embed [{unit}]: {e}"));
let first_ms = ms(first_start);
let mut warm = Vec::with_capacity(WARM_RUNS);
let mut last = first;
for _ in 0..WARM_RUNS {
let t = Instant::now();
last = black_box(
encoder
.embed(black_box(TEXT_PROMPT))
.unwrap_or_else(|e| panic!("warm text embed [{unit}]: {e}")),
);
warm.push(ms(t));
}
let cos = record_cosine(unit, &last, &mut reference);
print_row(
unit,
first_load_ms,
cached_load_ms,
first_ms,
&mut warm,
cos,
&last,
);
}
println!();
}
fn record_cosine(unit: ComputeUnits, emb: &Embedding, reference: &mut Option<Embedding>) -> f32 {
if unit == ComputeUnits::CpuOnly {
*reference = Some(emb.clone());
}
reference
.as_ref()
.map_or_else(|| emb.cosine(emb), |r| emb.cosine(r))
}
fn print_header() {
println!(
"{:<26} {:>12} {:>11} {:>10} {:>11} {:>10} {:>12} {:>9} {:>10}",
"unit",
"first_load",
"cached_load",
"first_inf",
"warm_median",
"warm_p90",
"cos_vs_cpu",
"rss_MB",
"emb_hash"
);
}
#[allow(clippy::too_many_arguments)]
fn print_row(
unit: ComputeUnits,
first_load_ms: f64,
cached_load_ms: f64,
first_ms: f64,
warm: &mut [f64],
cos: f32,
emb: &Embedding,
) {
let (median, p90) = median_p90(warm);
println!(
"{:<26} {:>12.1} {:>11.1} {:>10.2} {:>11.2} {:>10.2} {:>12.6} {:>9.1} {:>10}",
unit.as_str(),
first_load_ms,
cached_load_ms,
first_ms,
median,
p90,
cos,
rss_mb(),
hash8(emb),
);
}
fn ms(start: Instant) -> f64 {
start.elapsed().as_secs_f64() * 1e3
}
fn median_p90(samples: &mut [f64]) -> (f64, f64) {
samples.sort_by(f64::total_cmp);
let n = samples.len();
let median = samples[n / 2];
let p90_idx = ((n as f64 * 0.9).ceil() as usize)
.saturating_sub(1)
.min(n - 1);
(median, samples[p90_idx])
}
fn hash8(emb: &Embedding) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
for &v in emb.as_slice() {
hasher.update(v.to_le_bytes());
}
hasher
.finalize()
.iter()
.take(4)
.map(|b| format!("{b:02x}"))
.collect()
}
fn rss_mb() -> f64 {
let mut info: libc::mach_task_basic_info = unsafe { core::mem::zeroed() };
let mut count = (core::mem::size_of::<libc::mach_task_basic_info>()
/ core::mem::size_of::<libc::natural_t>()) as libc::mach_msg_type_number_t;
let result = unsafe {
libc::task_info(
mach2::traps::mach_task_self(),
libc::MACH_TASK_BASIC_INFO,
(&raw mut info).cast(),
&mut count,
)
};
if result == libc::KERN_SUCCESS {
info.resident_size as f64 / (1024.0 * 1024.0)
} else {
f64::NAN
}
}
fn models_dir() -> PathBuf {
std::env::var_os("CLAPKIT_TEST_MODELS").map_or_else(
|| workspace_root::models_root().join("clapkit"),
PathBuf::from,
)
}
fn deterministic_window(len: usize) -> Vec<f32> {
(0..len)
.map(|i| {
let t = i as f32 / SAMPLE_RATE_HZ as f32;
let two_pi = std::f32::consts::TAU;
0.5 * (two_pi * 220.0 * t).sin()
+ 0.3 * (two_pi * 440.0 * t).sin()
+ 0.2 * (two_pi * 1760.0 * t).sin()
})
.collect()
}