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