Skip to main content

runtime/benchmark/
runner.rs

1//! Benchmark runner implementation
2
3use std::path::Path;
4use std::time::Instant;
5
6use anyhow::Result;
7use prettytable::{row, Table};
8use sysinfo::{Pid, System};
9
10use super::{BenchmarkComparison, BenchmarkConfig, BenchmarkResults, GenerationResult, InferenceMetrics};
11
12/// Trait for inference backends that can be benchmarked
13pub trait InferenceBackend: Send + Sync {
14    /// Backend identifier name
15    fn name(&self) -> &str;
16
17    /// Load model from path, return load time in milliseconds
18    fn load_model(&mut self, path: &Path) -> Result<f64>;
19
20    /// Generate text from prompt, return generation result
21    fn generate(&mut self, prompt: &str, max_tokens: usize) -> Result<GenerationResult>;
22
23    /// Get current memory usage in bytes
24    fn memory_usage(&self) -> u64;
25
26    /// Unload model and free resources
27    fn unload(&mut self);
28}
29
30/// Get current process memory usage in bytes
31pub fn get_process_memory() -> u64 {
32    let mut sys = System::new_all();
33    let pid = Pid::from_u32(std::process::id());
34    sys.refresh_all();
35
36    sys.process(pid).map(|p| p.memory()).unwrap_or(0)
37}
38
39/// Benchmark runner that executes benchmarks on backends
40pub struct BenchmarkRunner {
41    config: BenchmarkConfig,
42}
43
44impl BenchmarkRunner {
45    /// Create new benchmark runner with configuration
46    pub fn new(config: BenchmarkConfig) -> Self {
47        Self { config }
48    }
49
50    /// Run benchmark on a backend
51    pub fn run<B: InferenceBackend>(&self, backend: &mut B) -> Result<BenchmarkResults> {
52        let mut runs = Vec::new();
53
54        // Memory before loading
55        let memory_before = get_process_memory();
56
57        // Load model and measure time
58        println!("  Loading model...");
59        let load_time = backend.load_model(&self.config.model_path)?;
60        println!("  Model loaded in {:.2}ms", load_time);
61
62        // Memory after loading
63        let memory_after = get_process_memory();
64        let model_memory = memory_after.saturating_sub(memory_before);
65        println!(
66            "  Model memory: {:.2} MB",
67            model_memory as f64 / (1024.0 * 1024.0)
68        );
69
70        // Warmup phase
71        println!(
72            "  Running {} warmup iterations...",
73            self.config.warmup_iterations
74        );
75        for i in 0..self.config.warmup_iterations {
76            for prompt in &self.config.prompts {
77                let _ = backend.generate(prompt, self.config.max_new_tokens)?;
78            }
79            print!("    Warmup {}/{}\r", i + 1, self.config.warmup_iterations);
80        }
81        println!();
82
83        // Measurement phase
84        println!(
85            "  Running {} measurement iterations...",
86            self.config.measurement_iterations
87        );
88        let mut peak_memory = memory_after;
89
90        for i in 0..self.config.measurement_iterations {
91            for prompt in &self.config.prompts {
92                let result = backend.generate(prompt, self.config.max_new_tokens)?;
93
94                // Track peak memory
95                let current_memory = get_process_memory();
96                peak_memory = peak_memory.max(current_memory);
97
98                let tokens_per_sec = if result.total_time_ms > 0.0 {
99                    result.tokens_generated as f64 / (result.total_time_ms / 1000.0)
100                } else {
101                    0.0
102                };
103
104                runs.push(InferenceMetrics {
105                    load_time_ms: load_time,
106                    time_to_first_token_ms: result.time_to_first_token_ms,
107                    total_inference_time_ms: result.total_time_ms,
108                    tokens_generated: result.tokens_generated,
109                    tokens_per_second: tokens_per_sec,
110                    prompt_tokens: result.prompt_tokens,
111                    memory_before_load_bytes: memory_before,
112                    memory_after_load_bytes: memory_after,
113                    peak_memory_bytes: peak_memory,
114                });
115            }
116            print!(
117                "    Iteration {}/{}\r",
118                i + 1,
119                self.config.measurement_iterations
120            );
121        }
122        println!();
123
124        Ok(BenchmarkResults::aggregate(
125            backend.name(),
126            runs,
127            load_time,
128            peak_memory,
129        ))
130    }
131}
132
133/// Print comparison results as a formatted table
134pub fn print_comparison_table(comparison: &BenchmarkComparison) {
135    let mut table = Table::new();
136
137    // Header
138    table.add_row(row![
139        "Metric",
140        "llama.cpp",
141        "UniLLM",
142        "Diff"
143    ]);
144
145    // Load time
146    table.add_row(row![
147        "Load Time (ms)",
148        format!("{:.1}", comparison.llama_cpp.load_time_ms),
149        format!("{:.1}", comparison.unillm.load_time_ms),
150        BenchmarkComparison::format_diff(
151            comparison.llama_cpp.load_time_ms,
152            comparison.unillm.load_time_ms
153        )
154    ]);
155
156    // TTFT
157    table.add_row(row![
158        "TTFT (ms)",
159        format!("{:.1}", comparison.llama_cpp.avg_ttft_ms),
160        format!("{:.1}", comparison.unillm.avg_ttft_ms),
161        BenchmarkComparison::format_diff(
162            comparison.llama_cpp.avg_ttft_ms,
163            comparison.unillm.avg_ttft_ms
164        )
165    ]);
166
167    // Tokens/sec (avg)
168    table.add_row(row![
169        "Tokens/sec (avg)",
170        format!("{:.1}", comparison.llama_cpp.avg_tokens_per_sec),
171        format!("{:.1}", comparison.unillm.avg_tokens_per_sec),
172        BenchmarkComparison::format_diff(
173            comparison.llama_cpp.avg_tokens_per_sec,
174            comparison.unillm.avg_tokens_per_sec
175        )
176    ]);
177
178    // Tokens/sec (p50)
179    table.add_row(row![
180        "Tokens/sec (p50)",
181        format!("{:.1}", comparison.llama_cpp.p50_tokens_per_sec),
182        format!("{:.1}", comparison.unillm.p50_tokens_per_sec),
183        BenchmarkComparison::format_diff(
184            comparison.llama_cpp.p50_tokens_per_sec,
185            comparison.unillm.p50_tokens_per_sec
186        )
187    ]);
188
189    // Memory (avg)
190    table.add_row(row![
191        "Memory (MB)",
192        format!("{:.1}", comparison.llama_cpp.avg_memory_mb),
193        format!("{:.1}", comparison.unillm.avg_memory_mb),
194        BenchmarkComparison::format_diff(
195            comparison.llama_cpp.avg_memory_mb,
196            comparison.unillm.avg_memory_mb
197        )
198    ]);
199
200    // Peak memory
201    table.add_row(row![
202        "Peak Memory (MB)",
203        format!("{:.1}", comparison.llama_cpp.peak_memory_mb),
204        format!("{:.1}", comparison.unillm.peak_memory_mb),
205        BenchmarkComparison::format_diff(
206            comparison.llama_cpp.peak_memory_mb,
207            comparison.unillm.peak_memory_mb
208        )
209    ]);
210
211    println!("\n╔══════════════════════════════════════════════════════════════╗");
212    println!("║         UniLLM vs llama.cpp Benchmark Results                ║");
213    println!("╚══════════════════════════════════════════════════════════════╝\n");
214
215    table.printstd();
216
217    // Notes
218    println!("\n--- Notes ---");
219    println!("* UniLLM uses KV caching for efficient autoregressive generation");
220    println!("* UniLLM keeps weights in quantized format (Q4/Q8) for 4-5x memory savings");
221    println!("* UniLLM runs on CPU only (no GPU acceleration yet)");
222    println!("* llama.cpp uses quantized inference with optimized SIMD kernels");
223}
224
225/// Print results for a single backend
226pub fn print_single_results(results: &BenchmarkResults) {
227    println!("\n=== {} Results ===", results.backend_name);
228    println!("Load Time:      {:.1} ms", results.load_time_ms);
229    println!("Avg TTFT:       {:.1} ms", results.avg_ttft_ms);
230    println!("Avg Tokens/sec: {:.1}", results.avg_tokens_per_sec);
231    println!("P50 Tokens/sec: {:.1}", results.p50_tokens_per_sec);
232    println!("P99 Tokens/sec: {:.1}", results.p99_tokens_per_sec);
233    println!("Avg Memory:     {:.1} MB", results.avg_memory_mb);
234    println!("Peak Memory:    {:.1} MB", results.peak_memory_mb);
235}