Skip to main content

rusty_stack/benchmark_runners/
mod.rs

1//! Native Rust benchmark runner module.
2//!
3//! Dispatches to the existing `benchmarks/` module functions.
4//! Provides the CLI-facing interface for running individual benchmarks,
5//! the full suite, and JSON output.
6//!
7//! Ports functionality from:
8//! - `scripts/run_rocm_benchmarks.sh` → ROCm benchmarks
9//! - `scripts/run_all_benchmarks_suite.sh` → full suite
10//! - `scripts/run_benchmarks.sh` → individual benchmark dispatch
11
12use crate::benchmarks;
13use serde::{Deserialize, Serialize};
14
15// ---------------------------------------------------------------------------
16// Public types
17// ---------------------------------------------------------------------------
18
19/// Combined benchmark result for CLI output.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct BenchmarkOutput {
22    pub name: String,
23    pub success: bool,
24    pub execution_time_ms: u128,
25    pub results: serde_json::Value,
26    pub errors: Vec<String>,
27}
28
29// ---------------------------------------------------------------------------
30// Benchmark name registry
31// ---------------------------------------------------------------------------
32
33/// Returns all available benchmark names in canonical order.
34pub fn available_benchmarks() -> Vec<&'static str> {
35    vec![
36        "gpu-capability",
37        "memory-bandwidth",
38        "tensor-core",
39        "gemm",
40        "pytorch",
41        "llama-cpp",
42        "flash-attention",
43        "flash-attention-ck",
44        "vllm",
45        "deepspeed",
46        "megatron",
47        "onnx",
48        "all-pre",
49        "all",
50    ]
51}
52
53/// Returns true if the benchmark name is recognized.
54pub fn is_valid_benchmark(name: &str) -> bool {
55    available_benchmarks().contains(&name)
56}
57
58// ---------------------------------------------------------------------------
59// Benchmark dispatch
60// ---------------------------------------------------------------------------
61
62/// Run a single named benchmark, dispatching to the correct benchmarks:: function.
63pub fn run_benchmark(name: &str) -> Result<BenchmarkOutput, String> {
64    match name {
65        "gpu-capability" => Ok(convert_result(
66            name,
67            benchmarks::run_gpu_capability_benchmark(),
68        )),
69        "memory-bandwidth" => Ok(convert_result(
70            name,
71            benchmarks::run_memory_bandwidth_benchmark(),
72        )),
73        "tensor-core" => Ok(convert_result(
74            name,
75            benchmarks::run_tensor_core_benchmark(),
76        )),
77        "gemm" => Ok(convert_result(name, benchmarks::run_gemm_benchmark())),
78        "pytorch" => Ok(convert_result(name, benchmarks::run_pytorch_benchmark())),
79        "llama-cpp" => Ok(convert_result(name, benchmarks::run_llama_cpp_benchmark())),
80        "flash-attention" => Ok(convert_result(
81            name,
82            benchmarks::run_flash_attention_benchmark(),
83        )),
84        "flash-attention-ck" => Ok(convert_result(
85            name,
86            benchmarks::run_flash_attention_ck_benchmark(),
87        )),
88        "vllm" => Ok(convert_result(name, benchmarks::run_vllm_benchmark())),
89        "deepspeed" => Ok(convert_result(name, benchmarks::run_deepspeed_benchmark())),
90        "megatron" => Ok(convert_result(name, benchmarks::run_megatron_benchmark())),
91        "onnx" => Ok(convert_result(name, benchmarks::run_onnx_benchmark())),
92        "all-pre" => Ok(run_all_pre()),
93        "all" => Ok(run_all()),
94        _ => Err(format!("Unknown benchmark: {}", name)),
95    }
96}
97
98/// Run all pre-installation benchmarks (gpu-capability, memory-bandwidth, tensor-core).
99fn run_all_pre() -> BenchmarkOutput {
100    let mut output = BenchmarkOutput {
101        name: "all-pre".to_string(),
102        success: true,
103        execution_time_ms: 0,
104        results: serde_json::Value::Object(serde_json::Map::new()),
105        errors: Vec::new(),
106    };
107
108    let mut combined = serde_json::Value::Object(serde_json::Map::new());
109
110    collect_benchmark(
111        &mut combined,
112        "gpu_capability",
113        &mut output,
114        benchmarks::run_gpu_capability_benchmark(),
115        true,
116    );
117    collect_benchmark(
118        &mut combined,
119        "memory_bandwidth",
120        &mut output,
121        benchmarks::run_memory_bandwidth_benchmark(),
122        true,
123    );
124    collect_benchmark(
125        &mut combined,
126        "tensor_core",
127        &mut output,
128        benchmarks::run_tensor_core_benchmark(),
129        true,
130    );
131
132    output.results = combined;
133    output
134}
135
136/// Run the full benchmark suite (all benchmarks).
137fn run_all() -> BenchmarkOutput {
138    let mut output = BenchmarkOutput {
139        name: "all".to_string(),
140        success: true,
141        execution_time_ms: 0,
142        results: serde_json::Value::Object(serde_json::Map::new()),
143        errors: Vec::new(),
144    };
145
146    let mut combined = serde_json::Value::Object(serde_json::Map::new());
147
148    // Pre-installation benchmarks (required)
149    collect_benchmark(
150        &mut combined,
151        "gpu_capability",
152        &mut output,
153        benchmarks::run_gpu_capability_benchmark(),
154        true,
155    );
156    collect_benchmark(
157        &mut combined,
158        "memory_bandwidth",
159        &mut output,
160        benchmarks::run_memory_bandwidth_benchmark(),
161        true,
162    );
163    collect_benchmark(
164        &mut combined,
165        "tensor_core",
166        &mut output,
167        benchmarks::run_tensor_core_benchmark(),
168        true,
169    );
170
171    // GPU performance (required)
172    collect_benchmark(
173        &mut combined,
174        "gemm",
175        &mut output,
176        benchmarks::run_gemm_benchmark(),
177        true,
178    );
179
180    // Component benchmarks (optional — failures don't fail the suite)
181    collect_benchmark(
182        &mut combined,
183        "pytorch",
184        &mut output,
185        benchmarks::run_pytorch_benchmark(),
186        false,
187    );
188    collect_benchmark(
189        &mut combined,
190        "llama_cpp",
191        &mut output,
192        benchmarks::run_llama_cpp_benchmark(),
193        false,
194    );
195    collect_benchmark(
196        &mut combined,
197        "flash_attention",
198        &mut output,
199        benchmarks::run_flash_attention_benchmark(),
200        false,
201    );
202    collect_benchmark(
203        &mut combined,
204        "flash_attention_ck",
205        &mut output,
206        benchmarks::run_flash_attention_ck_benchmark(),
207        false,
208    );
209    collect_benchmark(
210        &mut combined,
211        "vllm",
212        &mut output,
213        benchmarks::run_vllm_benchmark(),
214        false,
215    );
216    collect_benchmark(
217        &mut combined,
218        "deepspeed",
219        &mut output,
220        benchmarks::run_deepspeed_benchmark(),
221        false,
222    );
223    collect_benchmark(
224        &mut combined,
225        "megatron",
226        &mut output,
227        benchmarks::run_megatron_benchmark(),
228        false,
229    );
230    collect_benchmark(
231        &mut combined,
232        "onnx",
233        &mut output,
234        benchmarks::run_onnx_benchmark(),
235        false,
236    );
237
238    output.results = combined;
239    output
240}
241
242// ---------------------------------------------------------------------------
243// Helpers
244// ---------------------------------------------------------------------------
245
246fn convert_result(name: &str, result: benchmarks::BenchmarkResult) -> BenchmarkOutput {
247    BenchmarkOutput {
248        name: name.to_string(),
249        success: result.success,
250        execution_time_ms: result.execution_time_ms,
251        results: serde_json::to_value(&result).unwrap_or_default(),
252        errors: result.errors.clone(),
253    }
254}
255
256fn collect_benchmark(
257    combined: &mut serde_json::Value,
258    key: &str,
259    output: &mut BenchmarkOutput,
260    result: benchmarks::BenchmarkResult,
261    required: bool,
262) {
263    if !result.success {
264        if required {
265            output.success = false;
266        }
267        if result.errors.is_empty() {
268            output.errors.push(format!(
269                "{}: benchmark failed ({})",
270                key,
271                if required { "required" } else { "optional" }
272            ));
273        }
274    }
275    output.execution_time_ms += result.execution_time_ms;
276    for err in &result.errors {
277        output.errors.push(format!(
278            "{} ({}): {}",
279            key,
280            if required { "required" } else { "optional" },
281            err
282        ));
283    }
284
285    if let serde_json::Value::Object(map) = combined {
286        map.insert(
287            key.to_string(),
288            serde_json::to_value(&result).unwrap_or_default(),
289        );
290    }
291}
292
293// ---------------------------------------------------------------------------
294// Output formatting
295// ---------------------------------------------------------------------------
296
297/// Format benchmark output as human-readable text.
298pub fn format_output_human(output: &BenchmarkOutput) -> String {
299    let mut text = String::new();
300    text.push_str(&format!("Benchmark: {}\n", output.name));
301    text.push_str(&format!(
302        "Status: {}\n",
303        if output.success { "SUCCESS" } else { "FAILED" }
304    ));
305    text.push_str(&format!("Time: {} ms\n", output.execution_time_ms));
306
307    if !output.errors.is_empty() {
308        text.push_str("Errors:\n");
309        for e in &output.errors {
310            text.push_str(&format!("  - {}\n", e));
311        }
312    }
313
314    if output.success {
315        if let Some(map) = output.results.as_object() {
316            text.push_str("\nResults:\n");
317            for (key, value) in map {
318                if let Some(obj) = value.as_object() {
319                    let status = obj
320                        .get("success")
321                        .and_then(|v| v.as_bool())
322                        .map(|ok| if ok { "SUCCESS" } else { "FAILED" })
323                        .unwrap_or("UNKNOWN");
324                    let time_ms = obj
325                        .get("execution_time_ms")
326                        .and_then(|v| v.as_u64())
327                        .unwrap_or(0);
328                    text.push_str(&format!("  {} [{} | {} ms]\n", key, status, time_ms));
329                    if let Some(metrics) = obj.get("metrics").and_then(|m| m.as_object()) {
330                        for (metric_key, metric_val) in metrics.iter().take(4) {
331                            text.push_str(&format!("    - {}: {}\n", metric_key, metric_val));
332                        }
333                        if metrics.len() > 4 {
334                            text.push_str(&format!(
335                                "    - ... {} more metrics\n",
336                                metrics.len() - 4
337                            ));
338                        }
339                    }
340                } else {
341                    text.push_str(&format!("  {}: {}\n", key, value));
342                }
343            }
344        }
345    }
346
347    text
348}
349
350/// Format benchmark output as JSON.
351pub fn format_output_json(output: &BenchmarkOutput) -> String {
352    serde_json::to_string_pretty(output)
353        .unwrap_or_else(|e| format!("{{\"error\": \"failed to serialize: {}\"}}", e))
354}
355
356// ---------------------------------------------------------------------------
357// Tests
358// ---------------------------------------------------------------------------
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_available_benchmarks_contains_known_names() {
366        let names = available_benchmarks();
367        assert!(names.contains(&"gpu-capability"));
368        assert!(names.contains(&"memory-bandwidth"));
369        assert!(names.contains(&"tensor-core"));
370        assert!(names.contains(&"gemm"));
371        assert!(names.contains(&"pytorch"));
372        assert!(names.contains(&"flash-attention"));
373        assert!(names.contains(&"flash-attention-ck"));
374        assert!(names.contains(&"vllm"));
375        assert!(names.contains(&"deepspeed"));
376        assert!(names.contains(&"megatron"));
377        assert!(names.contains(&"onnx"));
378        assert!(names.contains(&"all-pre"));
379        assert!(names.contains(&"all"));
380    }
381
382    #[test]
383    fn test_is_valid_benchmark() {
384        assert!(is_valid_benchmark("gpu-capability"));
385        assert!(is_valid_benchmark("all"));
386        assert!(!is_valid_benchmark("nonexistent"));
387        assert!(!is_valid_benchmark(""));
388    }
389
390    #[test]
391    fn test_run_unknown_benchmark_returns_error() {
392        let result = run_benchmark("nonexistent-xyz");
393        assert!(result.is_err());
394        assert!(
395            result.unwrap_err().contains("Unknown benchmark"),
396            "Error should mention 'Unknown benchmark'"
397        );
398    }
399
400    #[test]
401    fn test_run_gpu_capability_dispatches() {
402        // This will likely fail on a system without ROCm, but should not panic
403        let result = run_benchmark("gpu-capability");
404        assert!(result.is_ok());
405        let output = result.unwrap();
406        assert_eq!(output.name, "gpu-capability");
407    }
408
409    #[test]
410    fn test_run_all_pre_dispatches() {
411        let result = run_benchmark("all-pre");
412        assert!(result.is_ok());
413        let output = result.unwrap();
414        assert_eq!(output.name, "all-pre");
415    }
416
417    #[test]
418    fn test_format_human_output() {
419        let output = BenchmarkOutput {
420            name: "test".to_string(),
421            success: true,
422            execution_time_ms: 100,
423            results: serde_json::Value::Object(serde_json::Map::new()),
424            errors: vec![],
425        };
426        let text = format_output_human(&output);
427        assert!(text.contains("test"));
428        assert!(text.contains("SUCCESS"));
429    }
430
431    #[test]
432    fn test_format_json_output() {
433        let output = BenchmarkOutput {
434            name: "test".to_string(),
435            success: true,
436            execution_time_ms: 100,
437            results: serde_json::Value::Object(serde_json::Map::new()),
438            errors: vec![],
439        };
440        let json = format_output_json(&output);
441        let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should be valid JSON");
442        assert_eq!(parsed["name"], "test");
443        assert_eq!(parsed["success"], true);
444    }
445
446    #[test]
447    fn test_benchmark_output_serialization_roundtrip() {
448        let output = BenchmarkOutput {
449            name: "gpu-capability".to_string(),
450            success: false,
451            execution_time_ms: 42,
452            results: serde_json::json!({"key": "value"}),
453            errors: vec!["test error".to_string()],
454        };
455        let json = serde_json::to_string(&output).unwrap();
456        let parsed: BenchmarkOutput = serde_json::from_str(&json).unwrap();
457        assert_eq!(parsed.name, "gpu-capability");
458        assert!(!parsed.success);
459        assert_eq!(parsed.execution_time_ms, 42);
460        assert_eq!(parsed.errors.len(), 1);
461    }
462}