use std::time::Instant;
use anyhow::Result;
use clap::Parser;
use frink_core::bench_profile::{self, Measured};
use frink_core::qstar::BandwidthProfile;
#[derive(Parser, Debug)]
pub struct BenchBwArgs {
#[arg(long, default_value = "q4_k")]
pub format: String,
#[arg(long, default_value_t = 512 * 1024 * 1024)]
pub bytes: usize,
#[arg(long, default_value_t = 5)]
pub reps: usize,
#[arg(long, default_value_t = 1.0)]
pub threshold: f64,
#[arg(long)]
pub out: Option<std::path::PathBuf>,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub allow_debug_build: bool,
}
pub fn run_bench_bw(args: BenchBwArgs) -> Result<()> {
if args.reps == 0 {
anyhow::bail!("--reps must be at least 1");
}
if args.bytes < (1 << 20) {
anyhow::bail!("--bytes must be at least 1 MiB or the number measures cache, not memory");
}
if let Err(why) = writable_build(cfg!(debug_assertions), args.allow_debug_build) {
eprintln!("{why}");
return Ok(());
}
println!(
"measuring the CPU side ({} MiB per pass)…",
args.bytes >> 20
);
let cpu_moe_gbs = Some(measure_cpu_stream(args.bytes, args.reps));
let gather = measure_pcie_gather(args.bytes, args.reps);
let measured = Measured {
cpu_moe_gbs,
pcie_gather_gbs: gather,
cpu_moe_overlap_gbs: None,
pcie_gather_overlap_gbs: None,
};
println!(" cpu-moe {}", fmt_gbs(measured.cpu_moe_gbs));
println!(" pcie-gather {}", fmt_gbs(measured.pcie_gather_gbs));
let entry = match bench_profile::entry_from(&measured, args.threshold) {
Ok(entry) => entry,
Err(why) => {
eprintln!("\nno profile written: {why}");
eprintln!(
"{}",
if cfg!(feature = "cuda") {
"this is a CUDA build, so the device side should have been \
measurable: check that a GPU is visible."
} else {
"the PCIe side needs a CUDA build. Rebuild with \
`--features cuda` on the machine you intend to serve on."
}
);
return Ok(());
}
};
let gpu = detect_gpu();
let path = args
.out
.clone()
.unwrap_or_else(|| bench_profile::default_profile_path(gpu.uuid.as_deref()));
let mut profile = BandwidthProfile {
threshold: Some(args.threshold),
..BandwidthProfile::default()
};
profile.gpu = gpu;
profile
.dtype_kernels
.insert(args.format.clone(), entry.clone());
if let Some(backend) = entry.recommended {
profile.dtypes.insert(args.format.clone(), backend);
}
println!(
" verdict {:?} fetch fraction {}",
entry.recommended,
entry
.fetch_fraction()
.map(|f| format!("{f:.3}"))
.unwrap_or_else(|| "-".to_string()),
);
if args.dry_run {
println!("\ndry run, nothing written (would be {})", path.display());
return Ok(());
}
bench_profile::write_profile(&path, &profile)?;
println!("\nwrote {}", path.display());
Ok(())
}
fn writable_build(debug_build: bool, allow_debug: bool) -> Result<(), String> {
if !debug_build || allow_debug {
return Ok(());
}
Err(
"no profile written: this is an unoptimized build, so the CPU number \
measures code generation rather than the machine -- and because the \
device side is unaffected, it moves the ratio the verdict is made \
of.\nRe-run a release build (`cargo build --release -p frink-cli`), \
or pass --allow-debug-build if you know why you want this one."
.to_string(),
)
}
fn fmt_gbs(v: Option<f64>) -> String {
match v {
Some(v) => format!("{v:.1} GB/s"),
None => "- (not measured in this build)".to_string(),
}
}
fn measure_cpu_stream(bytes: usize, reps: usize) -> f64 {
let len = bytes / std::mem::size_of::<f32>();
let buffer: Vec<f32> = (0..len).map(|i| (i % 251) as f32).collect();
let mut best = 0.0f64;
for _ in 0..reps {
let start = Instant::now();
let mut acc = 0.0f32;
for chunk in buffer.chunks(8192) {
acc += chunk.iter().sum::<f32>();
}
let elapsed = start.elapsed().as_secs_f64();
std::hint::black_box(acc);
if elapsed > 0.0 {
best = best.max(bytes as f64 / elapsed / 1e9);
}
}
best
}
#[cfg(feature = "cuda")]
fn measure_pcie_gather(_bytes: usize, _reps: usize) -> Option<f64> {
None
}
#[cfg(not(feature = "cuda"))]
fn measure_pcie_gather(_bytes: usize, _reps: usize) -> Option<f64> {
None
}
fn detect_gpu() -> frink_core::qstar::ProfileGpu {
frink_core::qstar::ProfileGpu::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_cpu_side_measures_a_positive_bandwidth() {
let gbs = measure_cpu_stream(4 << 20, 2);
assert!(gbs > 0.0, "measured {gbs} GB/s");
assert!(gbs.is_finite());
}
#[test]
fn an_unoptimized_build_refuses_to_write_a_profile() {
assert!(
writable_build(false, false).is_ok(),
"a release build writes"
);
assert!(writable_build(false, true).is_ok());
let refused = writable_build(true, false).expect_err("a debug build refuses");
assert!(refused.contains("--release"), "{refused}");
assert!(refused.contains("--allow-debug-build"), "{refused}");
assert!(
writable_build(true, true).is_ok(),
"the escape hatch exists, and has to be asked for by name"
);
}
#[test]
fn an_unmeasured_side_prints_as_a_dash() {
assert!(fmt_gbs(None).starts_with('-'));
assert_eq!(fmt_gbs(Some(12.34)), "12.3 GB/s");
}
#[test]
fn a_build_that_cannot_reach_a_device_produces_no_profile_entry() {
let measured = Measured {
cpu_moe_gbs: Some(measure_cpu_stream(4 << 20, 1)),
pcie_gather_gbs: measure_pcie_gather(4 << 20, 1),
cpu_moe_overlap_gbs: None,
pcie_gather_overlap_gbs: None,
};
if measured.pcie_gather_gbs.is_none() {
assert_eq!(
bench_profile::entry_from(&measured, 1.0),
Err(bench_profile::NotMeasurable::OnlyOneSide)
);
}
}
}