1use crate::analysis::roofline::{Precision, RooflineModel};
6use anyhow::Result;
7use serde::Serialize;
8
9#[derive(Debug, Clone, Serialize)]
11pub struct BackendResult {
12 pub name: String,
13 pub wall_time_us: f64,
14 pub tflops: f64,
15 pub bandwidth_gbps: f64,
16 pub available: bool,
17 #[serde(skip_serializing_if = "std::ops::Not::not")]
19 pub measured: bool,
20}
21
22fn gemm_tflops(size: u32, time_us: f64) -> f64 {
24 if time_us <= 0.0 {
25 return 0.0;
26 }
27 let flops = 2.0 * (size as f64).powi(3);
28 flops / (time_us * 1e-6) / 1e12
29}
30
31fn get_actual_gemm_timing(size: u32) -> Option<(f64, f64)> {
34 let stdout = run_benchmark_suite()?;
35 let pattern = format!("Matrix Multiplication ({size}x{size}x{size})");
36 stdout
37 .lines()
38 .find(|line| line.contains(&pattern))
39 .and_then(parse_benchmark_line)
40}
41
42pub(crate) fn cargo_target_dir() -> Option<String> {
49 let output =
50 std::process::Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
51 .args(["metadata", "--no-deps", "--format-version", "1"])
52 .output()
53 .ok()?;
54 if !output.status.success() {
55 return None;
56 }
57 let stdout = String::from_utf8_lossy(&output.stdout);
58 let json: serde_json::Value = serde_json::from_str(&stdout).ok()?;
59 json.get("target_directory")?.as_str().map(str::to_string)
60}
61
62fn run_benchmark_suite() -> Option<String> {
64 let target_dir = cargo_target_dir();
65 let owned_candidate =
66 target_dir.map(|d| format!("{d}/release/examples/benchmark_matrix_suite"));
67 let candidates: Vec<&str> = owned_candidate
68 .as_deref()
69 .into_iter()
70 .chain(["./target/release/examples/benchmark_matrix_suite"])
71 .collect();
72 let binary_path = candidates
73 .iter()
74 .find(|p| std::path::Path::new(p).exists())?;
75 let output = std::process::Command::new(*binary_path)
76 .stdout(std::process::Stdio::piped())
77 .stderr(std::process::Stdio::piped())
78 .output()
79 .ok()?;
80 if !output.status.success() {
81 return None;
82 }
83 Some(String::from_utf8_lossy(&output.stdout).into_owned())
84}
85
86fn parse_benchmark_line(line: &str) -> Option<(f64, f64)> {
89 let after_dots = line.split("...").nth(1)?;
90 let time_ms = after_dots.split("ms").next()?.trim().parse::<f64>().ok()?;
91 let gflops = after_dots
92 .split('(')
93 .nth(1)?
94 .split(" GFLOPS")
95 .next()?
96 .trim()
97 .parse::<f64>()
98 .ok()?;
99 Some((time_ms * 1000.0, gflops))
100}
101
102fn estimate_scalar_time_us(size: u32) -> f64 {
105 let ratio = (size as f64 / 256.0).powi(3);
107 11_700.0 * ratio
108}
109
110fn estimate_avx2_time_us(size: u32) -> f64 {
113 let flops = 2.0 * (size as f64).powi(3);
115 let gflops = 72.0; flops / (gflops * 1e9) * 1e6
117}
118
119fn estimate_avx512_time_us(size: u32) -> f64 {
122 let flops = 2.0 * (size as f64).powi(3);
123 let gflops = 80.0; flops / (gflops * 1e9) * 1e6
125}
126
127fn estimate_cuda_time_us(size: u32) -> f64 {
130 let ratio = (size as f64 / 512.0).powi(3);
131 23.2 * ratio
132}
133
134fn estimate_cublas_time_us(size: u32) -> f64 {
137 estimate_cuda_time_us(size) / 3.0
138}
139
140#[cfg(feature = "cuda")]
143fn measure_cublas_gemm(size: u32) -> Option<(f64, f64)> {
144 use trueno_gpu::driver::{CublasHandle, CudaContext, CudaStream, GemmOp, GpuBuffer};
145
146 let ctx = CudaContext::new(0).ok()?;
147 let stream = CudaStream::new(&ctx).ok()?;
148 let handle = CublasHandle::new(&ctx).ok()?;
149 handle.set_stream(&stream).ok()?;
150
151 let n = size as usize;
152 let a_data = vec![0x3C00u16; n * n]; let b_data = vec![0x3C00u16; n * n];
154 let c_data = vec![0u16; n * n];
155
156 let a_buf = GpuBuffer::from_host(&ctx, &a_data).ok()?;
157 let b_buf = GpuBuffer::from_host(&ctx, &b_data).ok()?;
158 let c_buf = GpuBuffer::from_host(&ctx, &c_data).ok()?;
159
160 for _ in 0..5 {
162 let _ = handle.gemm_f16(
163 GemmOp::NoTrans,
164 GemmOp::NoTrans,
165 n as i32,
166 n as i32,
167 n as i32,
168 1.0,
169 a_buf.as_ptr(),
170 n as i32,
171 b_buf.as_ptr(),
172 n as i32,
173 0.0,
174 c_buf.as_ptr(),
175 n as i32,
176 );
177 }
178 stream.synchronize().ok()?;
179
180 let iters: u32 = if n <= 512 {
181 200
182 } else if n <= 1024 {
183 100
184 } else {
185 30
186 };
187 let start = std::time::Instant::now();
188 for _ in 0..iters {
189 let _ = handle.gemm_f16(
190 GemmOp::NoTrans,
191 GemmOp::NoTrans,
192 n as i32,
193 n as i32,
194 n as i32,
195 1.0,
196 a_buf.as_ptr(),
197 n as i32,
198 b_buf.as_ptr(),
199 n as i32,
200 0.0,
201 c_buf.as_ptr(),
202 n as i32,
203 );
204 }
205 stream.synchronize().ok()?;
206 let elapsed = start.elapsed();
207
208 let per_call_us = elapsed.as_micros() as f64 / iters as f64;
209 let flops = 2.0 * (n as f64).powi(3);
210 let tflops = flops / (per_call_us * 1e6);
211
212 Some((per_call_us, tflops))
213}
214
215#[cfg(feature = "cuda")]
218fn measure_ptx_gemm(size: u32) -> Option<(f64, f64)> {
219 use std::ffi::c_void;
220 use trueno_gpu::driver::{CudaContext, CudaModule, CudaStream, GpuBuffer, LaunchConfig};
221 use trueno_gpu::kernels::build_cta64x128_mma_pipeline_fp16;
222 use trueno_gpu::ptx::PtxModule;
223
224 let ctx = CudaContext::new(0).ok()?;
225 let stream = CudaStream::new(&ctx).ok()?;
226
227 let n = size as usize;
228 let a16 = vec![0x3C00u16; n * n];
229 let b16 = vec![0x3C00u16; n * n];
230 let c32 = vec![0.0f32; n * n];
231
232 let a_buf = GpuBuffer::from_host(&ctx, &a16).ok()?;
233 let b_buf = GpuBuffer::from_host(&ctx, &b16).ok()?;
234 let c_buf = GpuBuffer::from_host(&ctx, &c32).ok()?;
235
236 let kernel = build_cta64x128_mma_pipeline_fp16(n as u32, n as u32, n as u32);
237 let ptx = PtxModule::new().target("sm_80").add_kernel(kernel).emit();
238 let mut module = CudaModule::from_ptx(&ctx, &ptx).ok()?;
239
240 let cfg = LaunchConfig {
241 grid: (((n + 127) / 128) as u32, ((n + 63) / 64) as u32, 1),
242 block: (512, 1, 1),
243 shared_mem: 18432,
244 };
245
246 let mut a_ptr = a_buf.as_ptr();
247 let mut b_ptr = b_buf.as_ptr();
248 let mut c_ptr = c_buf.as_ptr();
249 let mut m_v = n as u32;
250 let mut n_v = n as u32;
251 let mut k_v = n as u32;
252 let mut args: Vec<*mut c_void> = vec![
253 &mut a_ptr as *mut _ as *mut c_void,
254 &mut b_ptr as *mut _ as *mut c_void,
255 &mut c_ptr as *mut _ as *mut c_void,
256 &mut m_v as *mut _ as *mut c_void,
257 &mut n_v as *mut _ as *mut c_void,
258 &mut k_v as *mut _ as *mut c_void,
259 ];
260
261 for _ in 0..5 {
263 unsafe {
264 stream
265 .launch_kernel(
266 &mut module,
267 "gemm_cta64x128_mma_pipeline_fp16",
268 &cfg,
269 &mut args,
270 )
271 .ok()?;
272 }
273 }
274 stream.synchronize().ok()?;
275
276 let iters: u32 = if n <= 512 {
277 100
278 } else if n <= 1024 {
279 50
280 } else {
281 20
282 };
283 let start = std::time::Instant::now();
284 for _ in 0..iters {
285 unsafe {
286 stream
287 .launch_kernel(
288 &mut module,
289 "gemm_cta64x128_mma_pipeline_fp16",
290 &cfg,
291 &mut args,
292 )
293 .ok()?;
294 }
295 }
296 stream.synchronize().ok()?;
297 let per_call_us = start.elapsed().as_micros() as f64 / iters as f64;
298 let flops = 2.0 * (n as f64).powi(3);
299 let tflops = flops / (per_call_us * 1e6);
300
301 Some((per_call_us, tflops))
302}
303
304pub fn run_compare(kernel: &str, size: u32, backends_str: &str, json: bool) -> Result<()> {
306 let backends: Vec<&str> = backends_str.split(',').map(|s| s.trim()).collect();
307
308 if !json {
309 println!("\n=== CGP Cross-Backend Comparison: {kernel} ({size}x{size}x{size}) ===\n");
310 }
311
312 let actual = get_actual_gemm_timing(size);
313 let mut results = collect_backend_results(&backends, size, actual);
314 results.sort_by(|a, b| {
315 a.wall_time_us
316 .partial_cmp(&b.wall_time_us)
317 .unwrap_or(std::cmp::Ordering::Equal)
318 });
319
320 if json {
321 println!("{}", serde_json::to_string_pretty(&results)?);
322 return Ok(());
323 }
324
325 render_comparison_table(&results);
326 render_source_legend(&results);
327 render_best_summary(&results);
328 render_cpu_gpu_gap(&results);
329
330 println!();
331 Ok(())
332}
333
334fn collect_backend_results(
336 backends: &[&str],
337 size: u32,
338 actual: Option<(f64, f64)>,
339) -> Vec<BackendResult> {
340 let mut results: Vec<BackendResult> = Vec::new();
341 for backend in backends {
342 let Some((time_us, available, measured)) = measure_backend(backend, size, actual) else {
343 continue;
344 };
345 results.push(BackendResult {
346 name: (*backend).to_string(),
347 wall_time_us: time_us,
348 tflops: gemm_tflops(size, time_us),
349 bandwidth_gbps: 0.0,
350 available,
351 measured,
352 });
353 }
354 results
355}
356
357fn measure_backend(
359 backend: &str,
360 size: u32,
361 actual: Option<(f64, f64)>,
362) -> Option<(f64, bool, bool)> {
363 match backend {
364 "scalar" => Some((estimate_scalar_time_us(size), true, false)),
365 "avx2" => Some(measure_avx_backend(size, actual, false)),
366 "avx512" => Some(measure_avx_backend(size, actual, true)),
367 "neon" => Some((
368 estimate_scalar_time_us(size) / 4.0,
369 cfg!(target_arch = "aarch64"),
370 false,
371 )),
372 "cuda" => Some(measure_cuda_backend(size)),
373 "cublas" => Some(measure_cublas_backend(size)),
374 "wgpu" => Some((
375 estimate_cuda_time_us(size) * 2.0,
376 which::which("nvidia-smi").is_ok(),
377 false,
378 )),
379 other => {
380 eprintln!(" Warning: unknown backend '{other}', skipping");
381 None
382 }
383 }
384}
385
386fn measure_avx_backend(size: u32, actual: Option<(f64, f64)>, avx512: bool) -> (f64, bool, bool) {
388 #[cfg(target_arch = "x86_64")]
389 let avail = if avx512 {
390 std::arch::is_x86_feature_detected!("avx512f")
391 } else {
392 std::arch::is_x86_feature_detected!("avx2")
393 };
394 #[cfg(not(target_arch = "x86_64"))]
395 let avail = false;
396
397 if let Some((actual_us, _)) = actual {
398 return (actual_us, avail, true);
399 }
400 if avx512 {
401 (estimate_avx512_time_us(size), avail, false)
402 } else {
403 (estimate_avx2_time_us(size), avail, false)
404 }
405}
406
407fn measure_cuda_backend(size: u32) -> (f64, bool, bool) {
409 let avail = which::which("nvidia-smi").is_ok();
410 if avail {
411 if let Some((time_us, _)) = try_measure_ptx(size) {
412 return (time_us, true, true);
413 }
414 }
415 (estimate_cuda_time_us(size), avail, false)
416}
417
418fn measure_cublas_backend(size: u32) -> (f64, bool, bool) {
420 let avail = which::which("nvidia-smi").is_ok();
421 if avail {
422 if let Some((time_us, _)) = try_measure_cublas(size) {
423 return (time_us, true, true);
424 }
425 }
426 (estimate_cublas_time_us(size), avail, false)
427}
428
429#[cfg(feature = "cuda")]
431fn try_measure_ptx(size: u32) -> Option<(f64, f64)> {
432 measure_ptx_gemm(size)
433}
434
435#[cfg(not(feature = "cuda"))]
436fn try_measure_ptx(_size: u32) -> Option<(f64, f64)> {
437 None
438}
439
440#[cfg(feature = "cuda")]
442fn try_measure_cublas(size: u32) -> Option<(f64, f64)> {
443 measure_cublas_gemm(size)
444}
445
446#[cfg(not(feature = "cuda"))]
447fn try_measure_cublas(_size: u32) -> Option<(f64, f64)> {
448 None
449}
450
451fn render_comparison_table(results: &[BackendResult]) {
453 let best_time = results.first().map_or(1.0, |r| r.wall_time_us);
454 println!(
455 " {:12} {:>12} {:>12} {:>10} {:>10} {:>8} {:>5}",
456 "Backend", "Time (us)", "TFLOP/s", "Efficiency", "vs Best", "Avail", "Src"
457 );
458 println!(" {}", "-".repeat(75));
459 let (cpu_peak, gpu_peak) = peak_performance_limits();
460 for r in results {
461 render_comparison_row(r, best_time, cpu_peak, gpu_peak);
462 }
463}
464
465fn render_comparison_row(r: &BackendResult, best_time: f64, cpu_peak: f64, gpu_peak: f64) {
467 let peak_tflops = if r.name.contains("cuda") || r.name.contains("cublas") || r.name == "wgpu" {
468 gpu_peak / 1e12
469 } else {
470 cpu_peak / 1e12
471 };
472 let efficiency = if peak_tflops > 0.0 {
473 r.tflops / peak_tflops * 100.0
474 } else {
475 0.0
476 };
477 let ratio = format!("{:.2}x", r.wall_time_us / best_time);
478 let avail = if r.available { "yes" } else { "no" };
479 let time_str = if r.wall_time_us >= 1000.0 {
480 format!("{:.1} ms", r.wall_time_us / 1000.0)
481 } else {
482 format!("{:.1}", r.wall_time_us)
483 };
484 let src = if r.measured { "M" } else { "E" };
485 println!(
486 " {:12} {:>12} {:>12.1} {:>9.1}% {:>10} {:>8} {:>5}",
487 r.name, time_str, r.tflops, efficiency, ratio, avail, src
488 );
489}
490
491fn peak_performance_limits() -> (f64, f64) {
493 let model = RooflineModel::rtx_4090();
494 let gpu_peak = model
495 .peak_compute
496 .get(&Precision::Fp16)
497 .copied()
498 .unwrap_or(330.0e12);
499 let cores = num_cpus::get_physical();
500 #[allow(clippy::cast_precision_loss)]
501 let cpu_peak = 2.0 * 8.0 * 2.0 * 3.5e9 * cores as f64; (cpu_peak, gpu_peak)
503}
504
505fn render_source_legend(results: &[BackendResult]) {
507 let has_measured = results.iter().any(|r| r.measured);
508 let has_estimated = results.iter().any(|r| !r.measured);
509 if !(has_measured || has_estimated) {
510 return;
511 }
512 print!(" Src: ");
513 if has_measured {
514 print!("M=measured ");
515 }
516 if has_estimated {
517 print!("E=estimated ");
518 }
519 println!();
520}
521
522fn render_best_summary(results: &[BackendResult]) {
524 let Some(best) = results.first() else {
525 return;
526 };
527 let Some(worst) = results.last() else {
528 return;
529 };
530 let speedup = worst.wall_time_us / best.wall_time_us;
531 println!(
532 "\n Best: {} ({:.1}x faster than {})",
533 best.name, speedup, worst.name
534 );
535}
536
537fn render_cpu_gpu_gap(results: &[BackendResult]) {
539 let has_cpu = results
540 .iter()
541 .any(|r| matches!(r.name.as_str(), "scalar" | "avx2" | "avx512"));
542 let has_gpu = results
543 .iter()
544 .any(|r| matches!(r.name.as_str(), "cuda" | "cublas" | "wgpu"));
545 if !(has_cpu && has_gpu) {
546 return;
547 }
548 let best_cpu = results
549 .iter()
550 .filter(|r| matches!(r.name.as_str(), "scalar" | "avx2" | "avx512"))
551 .map(|r| r.wall_time_us)
552 .fold(f64::INFINITY, f64::min);
553 let best_gpu = results
554 .iter()
555 .filter(|r| matches!(r.name.as_str(), "cuda" | "cublas" | "wgpu"))
556 .map(|r| r.wall_time_us)
557 .fold(f64::INFINITY, f64::min);
558 if best_gpu > 0.0 {
559 println!(
560 " CPU→GPU gap: {:.0}x (expected for large GEMM)",
561 best_cpu / best_gpu
562 );
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
571 fn test_gemm_tflops() {
572 let tflops = gemm_tflops(512, 23.2);
574 assert!(
575 (tflops - 11.56).abs() < 0.1,
576 "Expected ~11.6 TFLOP/s, got {tflops:.2}"
577 );
578 }
579
580 #[test]
581 fn test_scalar_slower_than_avx2() {
582 let scalar = estimate_scalar_time_us(512);
583 let avx2 = estimate_avx2_time_us(512);
584 assert!(scalar > avx2 * 3.0, "Scalar should be >3x slower than AVX2");
585 }
586
587 #[test]
588 fn test_cuda_faster_than_cpu() {
589 let cpu = estimate_avx2_time_us(4096);
590 let cuda = estimate_cuda_time_us(4096);
591 assert!(
592 cpu > cuda * 10.0,
593 "CPU should be >10x slower than CUDA for 4096"
594 );
595 }
596
597 #[test]
599 fn test_cuda_faster_than_scalar_at_256() {
600 let scalar = estimate_scalar_time_us(256);
601 let cuda = estimate_cuda_time_us(256);
602 assert!(cuda < scalar, "CUDA should be faster than scalar at 256");
603 }
604
605 #[test]
607 fn test_simd_faster_than_scalar() {
608 let scalar = estimate_scalar_time_us(1024);
609 let avx2 = estimate_avx2_time_us(1024);
610 assert!(
611 scalar / avx2 >= 3.0,
612 "AVX2 speedup {:.1}x should be >= 3x",
613 scalar / avx2
614 );
615 }
616
617 #[test]
619 fn test_cublas_faster_than_ptx() {
620 let ptx = estimate_cuda_time_us(4096);
621 let cublas = estimate_cublas_time_us(4096);
622 assert!(cublas < ptx, "cuBLAS should be faster than PTX at 4096");
623 }
624
625 #[test]
626 fn test_run_compare_basic() {
627 let result = run_compare("gemm", 256, "scalar,avx2", false);
628 assert!(result.is_ok());
629 }
630
631 #[test]
632 fn test_run_compare_json() {
633 let result = run_compare("gemm", 256, "scalar,avx2", true);
634 assert!(result.is_ok());
635 }
636
637 #[test]
639 fn test_get_actual_gemm_timing() {
640 if let Some((time_us, gflops)) = get_actual_gemm_timing(1024) {
641 assert!(time_us > 0.0, "time should be positive");
642 assert!(gflops > 10.0, "GFLOPS should be > 10 for 1024 GEMM");
643 assert!(gflops < 2000.0, "GFLOPS should be < 2000");
644 eprintln!(
645 "Actual GEMM 1024: {:.1} us = {:.0} GFLOPS [MEASURED]",
646 time_us, gflops
647 );
648 } else {
649 eprintln!("benchmark_matrix_suite binary not found — actual data unavailable");
650 }
651 }
652}