#![allow(clippy::unwrap_used)]
#![allow(dead_code)]
#![allow(clippy::all, clippy::pedantic)]
use std::fs;
use std::time::Instant;
use super::super::super::super::apr_args::AprProfileArgs;
use super::super::super::super::commands::{CliError, CliResult, CommandResult};
use super::super::emit_output;
use super::sweep::run_sweep_threads;
use super::types::{AvgBrickDetail, BrickDetail, ProfileRun, ProfileSummary};
pub(in super::super::super) fn run_profile(
args: &AprProfileArgs,
global: &super::super::super::super::args::Args,
) -> CliResult<CommandResult> {
use crate::WhisperApr;
let thread_count = crate::parallel::configure_thread_pool(args.threads)
.map_err(|e| CliError::InvalidArgument(format!("Thread pool: {e}")))?;
if global.verbose {
eprintln!("[INFO] Using {thread_count} thread(s) for inference");
}
let load_start = Instant::now();
let model_bytes =
fs::read(&args.model).map_err(|e| CliError::InvalidArgument(format!("Model: {e}")))?;
let whisper = WhisperApr::load_from_apr(&model_bytes)
.map_err(|e| CliError::InvalidArgument(format!("Model load: {e}")))?;
let load_ms = load_start.elapsed().as_secs_f64() * 1000.0;
let hw = trueno::HardwareCapability::detect();
if global.verbose {
eprintln!(
"[INFO] Hardware: {} cores, {:?} SIMD, {:.0} GFLOP/s peak, {:.1} GB/s BW, AI balance: {:.1} F/B",
hw.cpu.cores,
hw.cpu.simd,
hw.cpu.peak_gflops,
hw.cpu.memory_bw_gbps,
hw.roofline.cpu_arithmetic_intensity,
);
}
let audio_bytes =
fs::read(&args.audio).map_err(|e| CliError::InvalidArgument(format!("Audio: {e}")))?;
let samples = super::super::super::super::commands::load_audio_samples(
args.audio.as_path(),
&audio_bytes,
)?;
let audio_duration_s = samples.len() as f64 / 16000.0;
if let Some(ref sweep_str) = args.sweep_threads {
return run_sweep_threads(
sweep_str,
&whisper,
&samples,
audio_duration_s,
args,
global,
&hw,
);
}
let run_results = collect_runs(&whisper, &samples, args, audio_duration_s, &hw)?;
let summary = summarize(run_results, load_ms, audio_duration_s, &hw);
emit_summary(&summary, args, global)
}
fn collect_runs(
whisper: &crate::WhisperApr,
samples: &[f32],
args: &AprProfileArgs,
audio_duration_s: f64,
hw: &trueno::HardwareCapability,
) -> CliResult<Vec<ProfileRun>> {
use crate::TranscribeOptions;
let total_runs = args.warmup + args.runs;
let mut run_results: Vec<ProfileRun> = Vec::with_capacity(args.runs);
let mut options = TranscribeOptions::default();
options.profile = true;
for run_idx in 0..total_runs {
let is_warmup = run_idx < args.warmup;
let run_start = Instant::now();
let result = whisper
.transcribe(samples, options.clone())
.map_err(|e| CliError::InvalidArgument(format!("Transcribe: {e}")))?;
let wall_ms = run_start.elapsed().as_secs_f64() * 1000.0;
let (mel_ms, enc_ms, dec_ms, total_ms) = if let Some(ref prof) = result.profiling {
let mel = prof.breakdown.get("mel_ms").copied().unwrap_or(0.0);
let enc = prof.breakdown.get("encoder_ms").copied().unwrap_or(0.0);
let dec = prof.breakdown.get("decoder_ms").copied().unwrap_or(0.0);
(mel, enc, dec, prof.total_ms)
} else {
(0.0, 0.0, 0.0, wall_ms)
};
let brick_detail = result
.profiling
.as_ref()
.and_then(|prof| extract_brick_detail(prof, hw));
let token_count: usize = result.segments.iter().map(|s| s.tokens.len()).sum();
if !is_warmup {
run_results.push(ProfileRun {
mel_ms,
encode_ms: enc_ms,
decode_ms: dec_ms,
total_ms,
rtf: total_ms / 1000.0 / audio_duration_s,
token_count,
text: result.text.clone(),
brick_detail,
trace_json: result.profiling.as_ref().and_then(|p| p.trace_json.clone()),
});
}
}
Ok(run_results)
}
fn summarize<'a>(
run_results: Vec<ProfileRun>,
load_ms: f64,
audio_duration_s: f64,
hw: &trueno::HardwareCapability,
) -> OwnedSummary {
let n = run_results.len().max(1) as f64;
let text = run_results.last().map_or(String::new(), |r| r.text.clone());
let trace_json = run_results.last().and_then(|r| r.trace_json.clone());
let avg_brick_detail = compute_avg_brick_detail(&run_results);
let avg_mel = run_results.iter().map(|r| r.mel_ms).sum::<f64>() / n;
let avg_enc = run_results.iter().map(|r| r.encode_ms).sum::<f64>() / n;
let avg_dec = run_results.iter().map(|r| r.decode_ms).sum::<f64>() / n;
let avg_total = run_results.iter().map(|r| r.total_ms).sum::<f64>() / n;
let avg_rtf = run_results.iter().map(|r| r.rtf).sum::<f64>() / n;
let avg_tokens =
run_results.iter().map(|r| r.token_count).sum::<usize>() / run_results.len().max(1);
OwnedSummary {
load_ms,
avg_mel,
avg_enc,
avg_dec,
avg_total,
avg_rtf,
avg_tokens,
text,
audio_duration_s,
avg_brick_detail,
trace_json,
hw_cores: hw.cpu.cores,
hw_simd: format!("{:?}", hw.cpu.simd),
hw_peak_gflops: hw.cpu.peak_gflops,
hw_bw_gbps: hw.cpu.memory_bw_gbps,
hw_balance_point: hw.roofline.cpu_arithmetic_intensity,
}
}
struct OwnedSummary {
load_ms: f64,
avg_mel: f64,
avg_enc: f64,
avg_dec: f64,
avg_total: f64,
avg_rtf: f64,
avg_tokens: usize,
text: String,
audio_duration_s: f64,
avg_brick_detail: Option<AvgBrickDetail>,
trace_json: Option<String>,
hw_cores: usize,
hw_simd: String,
hw_peak_gflops: f64,
hw_bw_gbps: f64,
hw_balance_point: f64,
}
impl OwnedSummary {
fn as_summary(&self) -> ProfileSummary<'_> {
ProfileSummary {
load_ms: self.load_ms,
avg_mel: self.avg_mel,
avg_enc: self.avg_enc,
avg_dec: self.avg_dec,
avg_total: self.avg_total,
avg_rtf: self.avg_rtf,
avg_tokens: self.avg_tokens,
text: self.text.as_str(),
audio_duration_s: self.audio_duration_s,
avg_brick_detail: self.avg_brick_detail.clone(),
trace_json: self.trace_json.clone(),
hw_cores: self.hw_cores,
hw_simd: self.hw_simd.clone(),
hw_peak_gflops: self.hw_peak_gflops,
hw_bw_gbps: self.hw_bw_gbps,
hw_balance_point: self.hw_balance_point,
}
}
}
fn emit_summary(
summary: &OwnedSummary,
args: &AprProfileArgs,
global: &super::super::super::super::args::Args,
) -> CliResult<CommandResult> {
let view = summary.as_summary();
let output_str = match args.format.as_str() {
"json" => view.format_json(args),
"renacer" => view.format_renacer(args),
_ => {
emit_output(global, || {}, || view.print_table(args));
return Ok(CommandResult::success("Profile complete"));
}
};
if let Some(ref out) = args.output {
fs::write(out, &output_str)
.map_err(|e| CliError::InvalidArgument(format!("Write: {e}")))?;
} else {
println!("{output_str}");
}
Ok(CommandResult::success("Profile complete"))
}
fn classify_roofline(blis_total_gflops: f64, peak_gflops: f64) -> (&'static str, f64) {
let util_pct = if peak_gflops > 0.0 && blis_total_gflops > 0.0 {
blis_total_gflops / peak_gflops * 100.0
} else {
0.0
};
let bound = if blis_total_gflops > 0.0 {
if util_pct > 50.0 {
"compute (efficient)"
} else if util_pct > 10.0 {
"compute (low util)"
} else {
"memory"
}
} else {
"unknown"
};
(bound, util_pct)
}
fn extract_brick_detail(
prof: &crate::ProfilingStats,
hw: &trueno::HardwareCapability,
) -> Option<BrickDetail> {
let get = |key: &str| prof.breakdown.get(key).copied().unwrap_or(0.0);
let norm = get("brick_norm_ms");
let attn = get("brick_attn_ms");
let ffn = get("brick_ffn_ms");
let other = get("brick_other_ms");
if norm == 0.0 && attn == 0.0 && ffn == 0.0 {
return None;
}
let blis_total_gflops = get("blis_total_gflops");
let (roofline_bound, roofline_util_pct) =
classify_roofline(blis_total_gflops, hw.cpu.peak_gflops);
Some(BrickDetail {
norm_ms: norm,
attn_ms: attn,
ffn_ms: ffn,
other_ms: other,
page_faults_minor: get("page_faults_minor") as u64,
page_faults_major: get("page_faults_major") as u64,
ln_bottleneck: get("brick_LayerNorm_bottleneck") as u8,
attn_bottleneck: get("brick_AttentionScore_bottleneck") as u8,
ffn_bottleneck: get("brick_GateProjection_bottleneck") as u8,
ln_cycles_per_elem: get("brick_LayerNorm_cycles_per_elem"),
attn_cycles_per_elem: get("brick_AttentionScore_cycles_per_elem"),
ffn_cycles_per_elem: get("brick_GateProjection_cycles_per_elem"),
blis_total_gflops,
blis_macro_gflops: get("blis_macro_gflops"),
blis_micro_gflops: get("blis_micro_gflops"),
blis_pack_pct: get("blis_pack_pct"),
blis_macro_calls: get("blis_macro_calls") as u64,
roofline_bound,
roofline_util_pct,
})
}
fn compute_avg_brick_detail(runs: &[ProfileRun]) -> Option<AvgBrickDetail> {
if runs.is_empty() || !runs.iter().all(|r| r.brick_detail.is_some()) {
return None;
}
let n = runs.len() as f64;
let avg = |f: fn(&BrickDetail) -> f64| -> f64 {
runs.iter()
.map(|r| f(r.brick_detail.as_ref().unwrap()))
.sum::<f64>()
/ n
};
let last = runs.last().unwrap().brick_detail.as_ref().unwrap();
Some(AvgBrickDetail {
norm_ms: avg(|b| b.norm_ms),
attn_ms: avg(|b| b.attn_ms),
ffn_ms: avg(|b| b.ffn_ms),
other_ms: avg(|b| b.other_ms),
page_faults_minor: last.page_faults_minor,
page_faults_major: last.page_faults_major,
ln_bottleneck: last.ln_bottleneck,
attn_bottleneck: last.attn_bottleneck,
ffn_bottleneck: last.ffn_bottleneck,
ln_cycles_per_elem: avg(|b| b.ln_cycles_per_elem),
attn_cycles_per_elem: avg(|b| b.attn_cycles_per_elem),
ffn_cycles_per_elem: avg(|b| b.ffn_cycles_per_elem),
blis_total_gflops: avg(|b| b.blis_total_gflops),
blis_macro_gflops: avg(|b| b.blis_macro_gflops),
blis_micro_gflops: avg(|b| b.blis_micro_gflops),
blis_pack_pct: avg(|b| b.blis_pack_pct),
blis_macro_calls: last.blis_macro_calls,
roofline_bound: last.roofline_bound,
roofline_util_pct: avg(|b| b.roofline_util_pct),
})
}