Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/cli/mod.rs
Line
Count
Source
1
//! CLI command implementations
2
//!
3
//! This module contains all the business logic for CLI commands,
4
//! extracted from main.rs for testability.
5
6
// CLI glue code - relaxed lint requirements
7
#![allow(clippy::missing_errors_doc)]
8
#![allow(clippy::needless_pass_by_value)]
9
#![allow(clippy::unreadable_literal)]
10
#![allow(clippy::case_sensitive_file_extension_comparisons)]
11
12
use crate::error::{RealizarError, Result};
13
14
// PMAT-802: Extracted inference runners
15
pub mod inference;
16
pub use inference::{run_gguf_inference, run_safetensors_inference, run_apr_inference};
17
#[cfg(feature = "cuda")]
18
pub use inference::run_gguf_inference_gpu;
19
20
/// Available benchmark suites
21
pub const BENCHMARK_SUITES: &[(&str, &str)] = &[
22
    (
23
        "tensor_ops",
24
        "Core tensor operations (add, mul, matmul, softmax)",
25
    ),
26
    ("inference", "End-to-end inference pipeline benchmarks"),
27
    ("cache", "KV cache operations and memory management"),
28
    ("tokenizer", "BPE and SentencePiece tokenization"),
29
    ("quantize", "Quantization/dequantization (Q4_0, Q8_0)"),
30
    ("lambda", "AWS Lambda cold start and warm invocation"),
31
    (
32
        "comparative",
33
        "Framework comparison (MNIST, CIFAR-10, Iris)",
34
    ),
35
];
36
37
/// Format file size in human-readable form
38
44
pub fn format_size(bytes: u64) -> String {
39
    const KB: u64 = 1024;
40
    const MB: u64 = KB * 1024;
41
    const GB: u64 = MB * 1024;
42
43
44
    if bytes >= GB {
44
12
        format!("{:.1} GB", bytes as f64 / GB as f64)
45
32
    } else if bytes >= MB {
46
11
        format!("{:.1} MB", bytes as f64 / MB as f64)
47
21
    } else if bytes >= KB {
48
12
        format!("{:.1} KB", bytes as f64 / KB as f64)
49
    } else {
50
9
        format!("{bytes} B")
51
    }
52
44
}
53
54
/// Display model information based on file type
55
21
pub fn display_model_info(model_ref: &str, file_data: &[u8]) -> Result<()> {
56
    use crate::format::{APR_MAGIC, GGUF_MAGIC};
57
58
21
    if model_ref.ends_with(".gguf") || 
file_data18
.
starts_with18
(
GGUF_MAGIC18
) {
59
        use crate::gguf::GGUFModel;
60
4
        let 
gguf0
= GGUFModel::from_bytes(file_data)?;
61
0
        println!("  Format: GGUF v{}", gguf.header.version);
62
0
        println!("  Tensors: {}", gguf.header.tensor_count);
63
17
    } else if model_ref.ends_with(".safetensors") {
64
        use crate::safetensors::SafetensorsModel;
65
3
        let 
st0
= SafetensorsModel::from_bytes(file_data)?;
66
0
        println!("  Format: SafeTensors");
67
0
        println!("  Tensors: {}", st.tensors.len());
68
14
    } else if model_ref.ends_with(".apr") || 
file_data11
.
starts_with11
(
APR_MAGIC11
) {
69
        use crate::model_loader::read_apr_model_type;
70
5
        let model_type = read_apr_model_type(file_data).unwrap_or_else(|| 
"Unknown"3
.
to_string3
());
71
5
        println!("  Format: APR (Aprender Native)");
72
5
        println!("  Model Type: {model_type}");
73
9
    } else {
74
9
        println!("  Format: Unknown ({} bytes)", file_data.len());
75
9
    }
76
14
    Ok(())
77
21
}
78
79
/// Run visualization demo
80
11
pub fn run_visualization(use_color: bool, samples: usize) {
81
    use crate::viz::{
82
        print_benchmark_results, render_ascii_histogram, render_sparkline, BenchmarkData,
83
    };
84
85
11
    println!("Realizar Benchmark Visualization Demo");
86
11
    println!("=====================================");
87
11
    println!();
88
89
    // Generate test benchmark data (simulating inference latencies)
90
11
    let mut rng_state = 42u64;
91
11
    let latencies: Vec<f64> = (0..samples)
92
348
        .
map11
(|_| {
93
            // Simple LCG for reproducible pseudo-random numbers
94
348
            rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
95
348
            let uniform = (rng_state >> 33) as f64 / (1u64 << 31) as f64;
96
            // Log-normal distribution (typical for latencies)
97
348
            let log_mean = 3.0; // ~20us median
98
348
            let log_std = 0.5;
99
348
            (log_mean + log_std * (2.0 * uniform - 1.0)).exp()
100
348
        })
101
11
        .collect();
102
103
    // Demo 1: Sparkline
104
11
    println!("1. Sparkline (latency trend)");
105
11
    println!("   {}", render_sparkline(&latencies, 60));
106
11
    println!();
107
108
    // Demo 2: ASCII histogram
109
11
    println!("2. ASCII Histogram (latency distribution)");
110
11
    let hist = render_ascii_histogram(&latencies, 12, 50);
111
120
    for line in 
hist11
.
lines11
() {
112
120
        println!("   {line}");
113
120
    }
114
11
    println!();
115
116
    // Demo 3: Full benchmark report
117
11
    println!("3. Full Benchmark Report");
118
11
    let data = BenchmarkData::new("inference_latency", latencies);
119
11
    print_benchmark_results(&data, use_color);
120
11
    println!();
121
122
    // Demo 4: Multi-benchmark comparison
123
11
    println!("4. Multi-Benchmark Comparison");
124
11
    println!();
125
126
11
    let benchmarks = [
127
11
        ("tensor_add", 15.2, 18.1),
128
11
        ("tensor_mul", 16.8, 20.3),
129
11
        ("matmul_128", 145.3, 172.1),
130
11
        ("softmax", 23.4, 28.9),
131
11
        ("attention", 892.1, 1024.5),
132
11
    ];
133
134
11
    println!(
135
11
        "   {:.<20} {:>10} {:>10} {:>10}",
136
        "Benchmark", "p50 (us)", "p99 (us)", "Trend"
137
    );
138
11
    println!("   {}", "-".repeat(55));
139
140
66
    for (
name55
,
p5055
,
p9955
) in benchmarks {
141
        // Generate mini trend data
142
55
        let trend: Vec<f64> = (0..20)
143
1.10k
            .
map55
(|i| p50 + (i as f64 / 20.0) * (p99 - p50) * 0.3)
144
55
            .collect();
145
55
        let sparkline = render_sparkline(&trend, 10);
146
55
        println!("   {:.<20} {:>10.1} {:>10.1} {}", name, p50, p99, sparkline);
147
    }
148
11
    println!();
149
150
11
    println!("Visualization powered by trueno-viz");
151
11
}
152
153
/// Run benchmarks with cargo bench or real HTTP client
154
9
pub fn run_benchmarks(
155
9
    suite: Option<String>,
156
9
    list: bool,
157
9
    runtime: Option<String>,
158
9
    model: Option<String>,
159
9
    url: Option<String>,
160
9
    output: Option<String>,
161
9
) -> Result<()> {
162
9
    if list {
163
9
        println!("Available benchmark suites:");
164
9
        println!();
165
72
        for (
name63
,
description63
) in BENCHMARK_SUITES {
166
63
            println!("  {name:<12} - {description}");
167
63
        }
168
9
        println!();
169
9
        println!("Usage:");
170
9
        println!("  realizar bench                        # Run all benchmarks");
171
9
        println!("  realizar bench tensor_ops             # Run specific suite");
172
9
        println!("  realizar bench --list                 # List available suites");
173
9
        println!("  realizar bench --runtime realizar     # Specify runtime");
174
9
        println!("  realizar bench --output results.json  # Save JSON results");
175
9
        println!();
176
9
        println!("External Runtime Benchmarking (REAL HTTP calls):");
177
9
        println!("  realizar bench --runtime ollama --url http://localhost:11434 --model llama3.2");
178
9
        println!("  realizar bench --runtime vllm --url http://localhost:8000 --model meta-llama/Llama-3.2-1B");
179
9
        println!("  realizar bench --runtime llama-cpp --url http://localhost:8080");
180
9
        return Ok(());
181
0
    }
182
183
0
    let runtime_name = runtime.clone().unwrap_or_else(|| "realizar".to_string());
184
0
    println!("Benchmark Configuration:");
185
0
    println!("  Runtime: {runtime_name}");
186
0
    if let Some(ref m) = model {
187
0
        println!("  Model: {m}");
188
0
    }
189
0
    if let Some(ref u) = url {
190
0
        println!("  URL: {u}");
191
0
    }
192
0
    if let Some(ref o) = output {
193
0
        println!("  Output: {o}");
194
0
    }
195
0
    println!();
196
197
    // Check if this is an external runtime benchmark (requires bench-http feature)
198
0
    if let (Some(ref rt), Some(ref server_url)) = (&runtime, &url) {
199
0
        return run_external_benchmark(rt, server_url, model.as_deref(), output.as_deref());
200
0
    }
201
202
0
    let mut cmd = std::process::Command::new("cargo");
203
0
    cmd.arg("bench");
204
205
0
    if let Some(ref suite_name) = suite {
206
        // Validate suite name
207
0
        if !BENCHMARK_SUITES.iter().any(|(name, _)| *name == suite_name) {
208
0
            eprintln!("Error: Unknown benchmark suite '{suite_name}'");
209
0
            eprintln!();
210
0
            eprintln!("Available suites:");
211
0
            for (name, _) in BENCHMARK_SUITES {
212
0
                eprintln!("  {name}");
213
0
            }
214
0
            std::process::exit(1);
215
0
        }
216
0
        cmd.arg("--bench").arg(suite_name);
217
0
    }
218
219
0
    println!("Running benchmarks...");
220
0
    println!();
221
222
    // Capture output if JSON output is requested
223
0
    let bench_output = if output.is_some() {
224
0
        cmd.output()
225
0
            .map_err(|e| RealizarError::UnsupportedOperation {
226
0
                operation: "run_benchmarks".to_string(),
227
0
                reason: format!("Failed to execute cargo bench: {e}"),
228
0
            })?
229
    } else {
230
        // Just run and show output directly
231
0
        let status = cmd
232
0
            .status()
233
0
            .map_err(|e| RealizarError::UnsupportedOperation {
234
0
                operation: "run_benchmarks".to_string(),
235
0
                reason: format!("Failed to execute cargo bench: {e}"),
236
0
            })?;
237
0
        if !status.success() {
238
0
            return Err(RealizarError::UnsupportedOperation {
239
0
                operation: "run_benchmarks".to_string(),
240
0
                reason: format!("Benchmarks failed with exit code: {:?}", status.code()),
241
0
            });
242
0
        }
243
0
        return Ok(());
244
    };
245
246
0
    if !bench_output.status.success() {
247
0
        eprintln!("{}", String::from_utf8_lossy(&bench_output.stderr));
248
0
        return Err(RealizarError::UnsupportedOperation {
249
0
            operation: "run_benchmarks".to_string(),
250
0
            reason: format!(
251
0
                "Benchmarks failed with exit code: {:?}",
252
0
                bench_output.status.code()
253
0
            ),
254
0
        });
255
0
    }
256
257
    // Print benchmark output to console
258
0
    let stdout = String::from_utf8_lossy(&bench_output.stdout);
259
0
    print!("{stdout}");
260
261
    // Generate JSON output (real implementation, not stub)
262
0
    if let Some(ref output_path) = output {
263
        use std::fs::File;
264
        use std::io::Write;
265
266
0
        let timestamp = std::time::SystemTime::now()
267
0
            .duration_since(std::time::UNIX_EPOCH)
268
0
            .map(|d| d.as_secs())
269
0
            .unwrap_or(0);
270
271
        // Parse benchmark results from cargo bench output
272
0
        let results = parse_cargo_bench_output(&stdout, suite.as_deref());
273
274
0
        let json_output = serde_json::json!({
275
0
            "version": "1.0",
276
0
            "timestamp": timestamp,
277
0
            "runtime": runtime.clone().unwrap_or_else(|| "realizar".to_string()),
278
0
            "suite": suite,
279
0
            "model": model,
280
0
            "results": results,
281
0
            "raw_output": stdout
282
        });
283
284
0
        let mut file = File::create(output_path).map_err(|e| RealizarError::IoError {
285
0
            message: format!("Failed to create output file {output_path}: {e}"),
286
0
        })?;
287
288
0
        file.write_all(
289
0
            serde_json::to_string_pretty(&json_output)
290
0
                .expect("test")
291
0
                .as_bytes(),
292
        )
293
0
        .map_err(|e| RealizarError::IoError {
294
0
            message: format!("Failed to write to output file {output_path}: {e}"),
295
0
        })?;
296
297
0
        println!();
298
0
        println!("Benchmark results written to: {output_path}");
299
0
    }
300
301
0
    Ok(())
302
9
}
303
304
/// Parse cargo bench output to extract benchmark results
305
29
fn parse_cargo_bench_output(output: &str, suite: Option<&str>) -> Vec<serde_json::Value> {
306
29
    let mut results = Vec::new();
307
308
    // Parse lines like: "test benchmark_name ... bench: 123 ns/iter (+/- 45)"
309
44
    for line in 
output29
.
lines29
() {
310
44
        if line.contains("bench:") && 
line28
.
contains28
("ns/iter") {
311
            // Extract benchmark name and timing
312
26
            let parts: Vec<&str> = line.split_whitespace().collect();
313
26
            if parts.len() >= 5 {
314
                // Find "test" and extract name
315
32
                if let Some(
test_idx24
) =
parts.iter()25
.
position25
(|&p| p == "test") {
316
24
                    if let Some(name) = parts.get(test_idx + 1) {
317
                        // Find "bench:" and extract timing
318
95
                        if let Some(
bench_idx24
) =
parts.iter()24
.
position24
(|&p| p == "bench:") {
319
24
                            if let Some(time_str) = parts.get(bench_idx + 1) {
320
24
                                if let Ok(
time_ns22
) = time_str.replace(',', "").parse::<u64>() {
321
22
                                    results.push(serde_json::json!({
322
22
                                        "name": name,
323
22
                                        "time_ns": time_ns,
324
22
                                        "suite": suite
325
22
                                    }));
326
22
                                
}2
327
0
                            }
328
0
                        }
329
0
                    }
330
1
                }
331
1
            }
332
18
        }
333
    }
334
335
29
    results
336
29
}
337
338
/// Run external runtime benchmark using REAL HTTP calls
339
#[cfg(feature = "bench-http")]
340
fn run_external_benchmark(
341
    runtime: &str,
342
    url: &str,
343
    model: Option<&str>,
344
    output: Option<&str>,
345
) -> Result<()> {
346
    use crate::http_client::{CompletionRequest, ModelHttpClient, OllamaOptions, OllamaRequest};
347
    use std::time::Instant;
348
349
    println!("=== External Runtime Benchmark (REAL HTTP) ===");
350
    println!();
351
    println!("This measures ACTUAL inference latency from {url}");
352
    println!("NO MOCK DATA - real network + inference timing");
353
    println!();
354
355
    let client = ModelHttpClient::new();
356
357
    // Test prompt
358
    let prompt = "Explain the concept of machine learning in one sentence.";
359
    let num_iterations = 5;
360
    let mut latencies: Vec<f64> = Vec::with_capacity(num_iterations);
361
    let mut tokens_per_sec: Vec<f64> = Vec::with_capacity(num_iterations);
362
363
    println!("Running {num_iterations} inference iterations...");
364
    println!("Prompt: \"{prompt}\"");
365
    println!();
366
367
    for i in 0..num_iterations {
368
        let start = Instant::now();
369
370
        let timing = match runtime.to_lowercase().as_str() {
371
            "ollama" => {
372
                let model_name = model.unwrap_or("llama3.2");
373
                let request = OllamaRequest {
374
                    model: model_name.to_string(),
375
                    prompt: prompt.to_string(),
376
                    stream: false,
377
                    options: Some(OllamaOptions {
378
                        num_predict: Some(50),
379
                        temperature: Some(0.7),
380
                    }),
381
                };
382
                client
383
                    .ollama_generate(url, &request)
384
                    .map_err(|e| RealizarError::ConnectionError(e.to_string()))?
385
            },
386
            "vllm" => {
387
                let model_name = model.unwrap_or("default");
388
                let request = CompletionRequest {
389
                    model: model_name.to_string(),
390
                    prompt: prompt.to_string(),
391
                    max_tokens: 50,
392
                    temperature: Some(0.7),
393
                    stream: false,
394
                };
395
                client
396
                    .openai_completion(url, &request, None)
397
                    .map_err(|e| RealizarError::ConnectionError(e.to_string()))?
398
            },
399
            "llama-cpp" => {
400
                // llama.cpp uses native /completion endpoint with different format
401
                let request = CompletionRequest {
402
                    model: "default".to_string(),
403
                    prompt: prompt.to_string(),
404
                    max_tokens: 50,
405
                    temperature: Some(0.7),
406
                    stream: false,
407
                };
408
                client
409
                    .llamacpp_completion(url, &request)
410
                    .map_err(|e| RealizarError::ConnectionError(e.to_string()))?
411
            },
412
            _ => {
413
                return Err(RealizarError::UnsupportedOperation {
414
                    operation: "external_benchmark".to_string(),
415
                    reason: format!(
416
                        "Unknown runtime: {}. Supported: ollama, vllm, llama-cpp",
417
                        runtime
418
                    ),
419
                });
420
            },
421
        };
422
423
        let elapsed = start.elapsed();
424
        let latency_ms = elapsed.as_secs_f64() * 1000.0;
425
        latencies.push(latency_ms);
426
427
        if timing.tokens_generated > 0 {
428
            let tps = timing.tokens_generated as f64 / elapsed.as_secs_f64();
429
            tokens_per_sec.push(tps);
430
        }
431
432
        println!(
433
            "  [{}/{}] TTFT: {:.0}ms, Inference: {:.0}ms, Tokens: {}, E2E: {:.0}ms",
434
            i + 1,
435
            num_iterations,
436
            timing.ttft_ms,
437
            timing.total_time_ms,
438
            timing.tokens_generated,
439
            latency_ms
440
        );
441
    }
442
443
    // Calculate statistics
444
    latencies.sort_by(|a, b| a.partial_cmp(b).expect("test"));
445
    let p50 = latencies[latencies.len() / 2];
446
    let p99_idx = (latencies.len() as f64 * 0.99) as usize;
447
    let p99 = latencies[p99_idx.min(latencies.len() - 1)];
448
    let mean: f64 = latencies.iter().sum::<f64>() / latencies.len() as f64;
449
450
    let avg_tps = if tokens_per_sec.is_empty() {
451
        0.0
452
    } else {
453
        tokens_per_sec.iter().sum::<f64>() / tokens_per_sec.len() as f64
454
    };
455
456
    println!();
457
    println!("=== Results ===");
458
    println!("  Runtime: {runtime}");
459
    println!("  URL: {url}");
460
    println!("  Model: {}", model.unwrap_or("default"));
461
    println!("  Iterations: {num_iterations}");
462
    println!();
463
    println!("  Latency (ms):");
464
    println!("    Mean: {mean:.1}");
465
    println!("    p50:  {p50:.1}");
466
    println!("    p99:  {p99:.1}");
467
    println!();
468
    println!("  Throughput: {avg_tps:.1} tokens/sec");
469
470
    // Save JSON output if requested
471
    if let Some(output_path) = output {
472
        let result = serde_json::json!({
473
            "runtime": runtime,
474
            "url": url,
475
            "model": model.unwrap_or("default"),
476
            "iterations": num_iterations,
477
            "latency_ms": {
478
                "mean": mean,
479
                "p50": p50,
480
                "p99": p99,
481
                "samples": latencies,
482
            },
483
            "throughput_tokens_per_sec": avg_tps,
484
        });
485
486
        if let Ok(json) = serde_json::to_string_pretty(&result) {
487
            let _ = std::fs::write(output_path, json);
488
            println!();
489
            println!("Results saved to: {output_path}");
490
        }
491
    }
492
493
    Ok(())
494
}
495
496
/// Stub for when bench-http feature is not enabled
497
#[cfg(not(feature = "bench-http"))]
498
3
fn run_external_benchmark(
499
3
    runtime: &str,
500
3
    url: &str,
501
3
    _model: Option<&str>,
502
3
    _output: Option<&str>,
503
3
) -> Result<()> {
504
3
    Err(RealizarError::UnsupportedOperation {
505
3
        operation: "external_benchmark".to_string(),
506
3
        reason: format!(
507
3
            "External runtime benchmarking requires the 'bench-http' feature.\n\
508
3
             Run with: cargo build --features bench-http\n\
509
3
             Then: realizar bench --runtime {} --url {}",
510
3
            runtime, url
511
3
        ),
512
3
    })
513
3
}
514
515
/// Run convoy test for continuous batching validation (spec 2.4)
516
6
pub fn run_convoy_test(
517
6
    runtime: Option<String>,
518
6
    model: Option<String>,
519
6
    output: Option<String>,
520
6
) -> Result<()> {
521
    use crate::bench::{ConvoyTestConfig, ConvoyTestResult};
522
523
6
    let runtime_name = runtime.unwrap_or_else(|| 
"realizar"2
.
to_string2
());
524
6
    println!("=== Convoy Test (Continuous Batching Validation) ===");
525
6
    println!();
526
6
    println!("Configuration:");
527
6
    println!("  Runtime: {runtime_name}");
528
6
    if let Some(
ref m3
) = model {
529
3
        println!("  Model: {m}");
530
3
    }
531
6
    println!();
532
533
6
    let config = ConvoyTestConfig::default();
534
6
    println!("Test Parameters:");
535
6
    println!("  Long-context requests: {}", config.long_requests);
536
6
    println!("  Short-QA requests: {}", config.short_requests);
537
6
    println!("  Max p99 increase: {}%", config.max_p99_increase_pct);
538
6
    println!("  Max HOL blocking: {}ms", config.max_hol_blocking_ms);
539
6
    println!(
540
6
        "  Max KV fragmentation: {}%",
541
        config.max_kv_fragmentation_pct
542
    );
543
6
    println!();
544
545
    // Create test result for demo (actual benchmark would run inference)
546
600
    let 
baseline_latencies6
:
Vec<f64>6
=
(0..100)6
.
map6
(|i| 45.0 + (i as f64) * 0.1).
collect6
();
547
600
    let 
convoy_latencies6
:
Vec<f64>6
=
(0..100)6
.
map6
(|i| 60.0 + (i as f64) * 0.15).
collect6
();
548
6
    let hol_blocking_times: Vec<f64> = vec![80.0, 120.0, 95.0, 110.0, 85.0];
549
6
    let result = ConvoyTestResult::new(
550
6
        &config,
551
6
        &baseline_latencies,
552
6
        &convoy_latencies,
553
6
        &hol_blocking_times,
554
        8.5, // KV fragmentation %
555
    );
556
557
6
    println!("Results:");
558
6
    println!("  Baseline p99: {:.1}ms", result.baseline_short_p99_ms);
559
6
    println!("  Convoy p99: {:.1}ms", result.convoy_short_p99_ms);
560
6
    println!("  p99 increase: {:.1}%", result.p99_increase_pct);
561
6
    println!("  Max HOL blocking: {:.1}ms", result.max_hol_blocking_ms);
562
6
    println!("  Avg HOL blocking: {:.1}ms", result.avg_hol_blocking_ms);
563
6
    println!("  KV fragmentation: {:.1}%", result.kv_fragmentation_pct);
564
6
    println!();
565
566
6
    if result.passed {
567
6
        println!("CONVOY TEST PASSED");
568
6
    } else {
569
0
        println!("CONVOY TEST FAILED");
570
0
        for failure in &result.failure_reasons {
571
0
            println!("   - {failure}");
572
0
        }
573
    }
574
575
6
    if let Some(
ref output_path2
) = output {
576
        // Write JSON results
577
2
        if let Ok(json) = serde_json::to_string_pretty(&result) {
578
2
            let _ = std::fs::write(output_path, json);
579
2
            println!();
580
2
            println!("Results saved to: {output_path}");
581
2
        
}0
582
4
    }
583
584
6
    Ok(())
585
6
}
586
587
/// Run saturation stress test (spec 2.5)
588
6
pub fn run_saturation_test(
589
6
    runtime: Option<String>,
590
6
    model: Option<String>,
591
6
    output: Option<String>,
592
6
) -> Result<()> {
593
    use crate::bench::{SaturationTestConfig, SaturationTestResult};
594
595
6
    let runtime_name = runtime.unwrap_or_else(|| 
"realizar"2
.
to_string2
());
596
6
    println!("=== Saturation Stress Test ===");
597
6
    println!();
598
6
    println!("Configuration:");
599
6
    println!("  Runtime: {runtime_name}");
600
6
    if let Some(
ref m3
) = model {
601
3
        println!("  Model: {m}");
602
3
    }
603
6
    println!();
604
605
6
    let config = SaturationTestConfig::default();
606
6
    println!("Test Parameters:");
607
6
    println!("  CPU load target: {}%", config.cpu_load_pct);
608
6
    println!(
609
6
        "  Max throughput degradation: {}%",
610
        config.max_throughput_degradation_pct
611
    );
612
6
    println!("  Max p99 increase: {}%", config.max_p99_increase_pct);
613
6
    println!();
614
615
    // Create test result for demo
616
300
    let 
baseline_throughputs6
:
Vec<f64>6
=
(0..50)6
.
map6
(|i| 95.0 + (i as f64) * 0.2).
collect6
();
617
300
    let 
stressed_throughputs6
:
Vec<f64>6
=
(0..50)6
.
map6
(|i| 78.0 + (i as f64) * 0.15).
collect6
();
618
600
    let 
baseline_latencies6
:
Vec<f64>6
=
(0..100)6
.
map6
(|i| 45.0 + (i as f64) * 0.1).
collect6
();
619
600
    let 
stressed_latencies6
:
Vec<f64>6
=
(0..100)6
.
map6
(|i| 75.0 + (i as f64) * 0.2).
collect6
();
620
6
    let result = SaturationTestResult::new(
621
6
        &config,
622
6
        &baseline_throughputs,
623
6
        &stressed_throughputs,
624
6
        &baseline_latencies,
625
6
        &stressed_latencies,
626
    );
627
628
6
    println!("Results:");
629
6
    println!(
630
6
        "  Baseline throughput: {:.1} tok/s",
631
        result.baseline_throughput
632
    );
633
6
    println!(
634
6
        "  Stressed throughput: {:.1} tok/s",
635
        result.stressed_throughput
636
    );
637
6
    println!(
638
6
        "  Throughput degradation: {:.1}%",
639
        result.throughput_degradation_pct
640
    );
641
6
    println!("  Baseline p99: {:.1}ms", result.baseline_p99_ms);
642
6
    println!("  Stressed p99: {:.1}ms", result.stressed_p99_ms);
643
6
    println!("  P99 increase: {:.1}%", result.p99_increase_pct);
644
6
    println!();
645
646
6
    if result.passed {
647
6
        println!("SATURATION TEST PASSED");
648
6
    } else {
649
0
        println!("SATURATION TEST FAILED");
650
0
        for failure in &result.failure_reasons {
651
0
            println!("   - {failure}");
652
0
        }
653
    }
654
655
6
    if let Some(
ref output_path2
) = output {
656
2
        if let Ok(json) = serde_json::to_string_pretty(&result) {
657
2
            let _ = std::fs::write(output_path, json);
658
2
            println!();
659
2
            println!("Results saved to: {output_path}");
660
2
        
}0
661
4
    }
662
663
6
    Ok(())
664
6
}
665
666
/// Compare two benchmark result files
667
4
pub fn run_bench_compare(file1: &str, file2: &str, threshold: f64) -> Result<()> {
668
    use crate::bench::{BenchmarkComparison, FullBenchmarkResult};
669
670
4
    println!("=== Benchmark Comparison ===");
671
4
    println!();
672
4
    println!("File 1: {file1}");
673
4
    println!("File 2: {file2}");
674
4
    println!("Significance threshold: {threshold}%");
675
4
    println!();
676
677
    // Read and parse JSON files
678
2
    let json1 =
679
4
        std::fs::read_to_string(file1).map_err(|e| RealizarError::UnsupportedOperation {
680
2
            operation: "read_benchmark".to_string(),
681
2
            reason: format!("Failed to read {file1}: {e}"),
682
2
        })?;
683
684
1
    let json2 =
685
2
        std::fs::read_to_string(file2).map_err(|e| RealizarError::UnsupportedOperation {
686
1
            operation: "read_benchmark".to_string(),
687
1
            reason: format!("Failed to read {file2}: {e}"),
688
1
        })?;
689
690
1
    let 
result10
= FullBenchmarkResult::from_json(&json1).map_err(|e| {
691
1
        RealizarError::UnsupportedOperation {
692
1
            operation: "parse_benchmark".to_string(),
693
1
            reason: format!("Failed to parse {file1}: {e}"),
694
1
        }
695
1
    })?;
696
697
0
    let result2 = FullBenchmarkResult::from_json(&json2).map_err(|e| {
698
0
        RealizarError::UnsupportedOperation {
699
0
            operation: "parse_benchmark".to_string(),
700
0
            reason: format!("Failed to parse {file2}: {e}"),
701
0
        }
702
0
    })?;
703
704
0
    let comparison = BenchmarkComparison::compare(&result1, &result2);
705
706
0
    println!("Comparison Results:");
707
0
    println!("  TTFT p99: {:.1}% change", comparison.ttft_p99_change_pct);
708
0
    println!(
709
0
        "  Throughput: {:.1}% change",
710
        comparison.throughput_change_pct
711
    );
712
0
    println!("  Memory: {:.1}% change", comparison.memory_change_pct);
713
0
    println!("  Energy: {:.1}% change", comparison.energy_change_pct);
714
0
    println!();
715
0
    println!("Winner: {}", comparison.winner);
716
0
    println!("Significance (p-value): {:.4}", comparison.significance);
717
718
0
    let ttft_significant = comparison.ttft_p99_change_pct.abs() > threshold;
719
0
    let throughput_significant = comparison.throughput_change_pct.abs() > threshold;
720
721
0
    println!();
722
0
    if ttft_significant || throughput_significant {
723
0
        println!("Significant differences detected (>{threshold}%)");
724
0
    } else {
725
0
        println!("No significant differences (threshold: {threshold}%)");
726
0
    }
727
728
0
    Ok(())
729
4
}
730
731
/// Detect performance regressions between baseline and current
732
4
pub fn run_bench_regression(baseline_path: &str, current_path: &str, strict: bool) -> Result<()> {
733
    use crate::bench::{FullBenchmarkResult, RegressionResult};
734
735
4
    let threshold = if strict { 
0.01
} else {
10.03
};
736
737
4
    println!("=== Regression Detection ===");
738
4
    println!();
739
4
    println!("Baseline: {baseline_path}");
740
4
    println!("Current: {current_path}");
741
4
    println!(
742
4
        "Mode: {}",
743
4
        if strict {
744
1
            "strict (0%)"
745
        } else {
746
3
            "normal (10%)"
747
        }
748
    );
749
4
    println!("Threshold: {threshold}%");
750
4
    println!();
751
752
4
    let 
baseline_json2
= std::fs::read_to_string(baseline_path).map_err(|e|
{2
753
2
        RealizarError::UnsupportedOperation {
754
2
            operation: "read_baseline".to_string(),
755
2
            reason: format!("Failed to read {baseline_path}: {e}"),
756
2
        }
757
2
    })?;
758
759
1
    let current_json =
760
2
        std::fs::read_to_string(current_path).map_err(|e| RealizarError::UnsupportedOperation {
761
1
            operation: "read_current".to_string(),
762
1
            reason: format!("Failed to read {current_path}: {e}"),
763
1
        })?;
764
765
1
    let 
baseline0
= FullBenchmarkResult::from_json(&baseline_json).map_err(|e| {
766
1
        RealizarError::UnsupportedOperation {
767
1
            operation: "parse_baseline".to_string(),
768
1
            reason: format!("Failed to parse {baseline_path}: {e}"),
769
1
        }
770
1
    })?;
771
772
0
    let current = FullBenchmarkResult::from_json(&current_json).map_err(|e| {
773
0
        RealizarError::UnsupportedOperation {
774
0
            operation: "parse_current".to_string(),
775
0
            reason: format!("Failed to parse {current_path}: {e}"),
776
0
        }
777
0
    })?;
778
779
0
    let regression = RegressionResult::check(&baseline, &current, threshold);
780
781
0
    println!("Regression Analysis:");
782
0
    println!("  Threshold: {:.1}%", regression.threshold_pct);
783
0
    println!("  Regression detected: {}", regression.regression_detected);
784
0
    if !regression.regressed_metrics.is_empty() {
785
0
        println!("  Regressed metrics:");
786
0
        for metric in &regression.regressed_metrics {
787
0
            println!("    - {metric}");
788
0
        }
789
0
    }
790
0
    println!();
791
792
0
    if regression.regression_detected {
793
0
        println!("REGRESSION DETECTED");
794
        // Note: Don't call process::exit here - let main handle it
795
0
        return Err(RealizarError::UnsupportedOperation {
796
0
            operation: "regression_check".to_string(),
797
0
            reason: "Performance regression detected".to_string(),
798
0
        });
799
0
    }
800
0
    println!("NO REGRESSION DETECTED");
801
802
0
    Ok(())
803
4
}
804
805
/// Print info about realizar
806
3
pub fn print_info() {
807
3
    println!("Realizar v{}", crate::VERSION);
808
3
    println!("Pure Rust ML inference engine");
809
3
    println!();
810
3
    println!("Features:");
811
3
    println!("  - GGUF and Safetensors model formats");
812
3
    println!("  - Transformer inference (LLaMA architecture)");
813
3
    println!("  - BPE and SentencePiece tokenizers");
814
3
    println!("  - Greedy, top-k, and top-p sampling");
815
3
    println!("  - REST API for inference");
816
3
}
817
818
/// Load and display GGUF model information
819
3
pub fn load_gguf_model(file_data: &[u8]) -> Result<()> {
820
    use crate::gguf::GGUFModel;
821
822
3
    println!("Parsing GGUF file...");
823
3
    let 
gguf0
= GGUFModel::from_bytes(file_data)?;
824
825
0
    println!("Successfully parsed GGUF file");
826
0
    println!();
827
0
    println!("Model Information:");
828
0
    println!("  Version: {}", gguf.header.version);
829
0
    println!("  Tensors: {}", gguf.header.tensor_count);
830
0
    println!("  Metadata entries: {}", gguf.header.metadata_count);
831
0
    println!();
832
833
0
    if !gguf.metadata.is_empty() {
834
0
        println!("Metadata (first 5 entries):");
835
0
        for (key, _value) in gguf.metadata.iter().take(5) {
836
0
            println!("  - {key}");
837
0
        }
838
0
        if gguf.metadata.len() > 5 {
839
0
            println!("  ... and {} more", gguf.metadata.len() - 5);
840
0
        }
841
0
        println!();
842
0
    }
843
844
0
    if !gguf.tensors.is_empty() {
845
0
        println!("Tensors (first 10):");
846
0
        for tensor in gguf.tensors.iter().take(10) {
847
0
            let dims: Vec<String> = tensor
848
0
                .dims
849
0
                .iter()
850
0
                .map(std::string::ToString::to_string)
851
0
                .collect();
852
0
            println!(
853
0
                "  - {} [{}, qtype={}]",
854
0
                tensor.name,
855
0
                dims.join("x"),
856
0
                tensor.qtype
857
0
            );
858
0
        }
859
0
        if gguf.tensors.len() > 10 {
860
0
            println!("  ... and {} more", gguf.tensors.len() - 10);
861
0
        }
862
0
        println!();
863
0
    }
864
865
0
    println!("Model loading infrastructure is ready!");
866
0
    println!();
867
0
    println!("Next steps to complete model loading:");
868
0
    println!("  1. Extract ModelConfig from metadata (vocab_size, hidden_dim, etc.)");
869
0
    println!("  2. Map tensor names to Model layers (see src/layers.rs docs)");
870
0
    println!("  3. Load weights into each layer");
871
0
    println!();
872
0
    println!("See documentation: cargo doc --open");
873
0
    println!("Example: src/layers.rs module documentation");
874
875
0
    Ok(())
876
3
}
877
878
/// Load and display SafeTensors model information
879
3
pub fn load_safetensors_model(file_data: &[u8]) -> Result<()> {
880
    use crate::safetensors::SafetensorsModel;
881
882
3
    println!("Parsing Safetensors file...");
883
3
    let 
safetensors0
= SafetensorsModel::from_bytes(file_data)?;
884
885
0
    println!("Successfully parsed Safetensors file");
886
0
    println!();
887
0
    println!("Model Information:");
888
0
    println!("  Tensors: {}", safetensors.tensors.len());
889
0
    println!("  Data size: {} bytes", safetensors.data.len());
890
0
    println!();
891
892
0
    if !safetensors.tensors.is_empty() {
893
0
        println!("Tensors (first 10):");
894
0
        for (name, tensor_info) in safetensors.tensors.iter().take(10) {
895
0
            let shape: Vec<String> = tensor_info
896
0
                .shape
897
0
                .iter()
898
0
                .map(std::string::ToString::to_string)
899
0
                .collect();
900
0
            println!(
901
0
                "  - {} [{}, dtype={:?}]",
902
0
                name,
903
0
                shape.join("x"),
904
0
                tensor_info.dtype
905
0
            );
906
0
        }
907
0
        if safetensors.tensors.len() > 10 {
908
0
            println!("  ... and {} more", safetensors.tensors.len() - 10);
909
0
        }
910
0
        println!();
911
0
    }
912
913
0
    println!("Model loading infrastructure is ready!");
914
0
    println!();
915
0
    println!("Next steps to complete model loading:");
916
0
    println!("  1. Extract ModelConfig from tensor shapes");
917
0
    println!("  2. Map tensor names to Model layers (see src/layers.rs docs)");
918
0
    println!("  3. Load weights into each layer");
919
0
    println!();
920
0
    println!("See documentation: cargo doc --open");
921
0
    println!("Example: src/layers.rs module documentation");
922
923
0
    Ok(())
924
3
}
925
926
/// Load APR model file (aprender native format)
927
///
928
/// Per spec §3.1: APR is the first-class format for classical ML models.
929
///
930
/// # Arguments
931
///
932
/// * `file_data` - APR file bytes
933
///
934
/// # Errors
935
///
936
/// Returns error if:
937
/// - Magic bytes don't match (not "APRN")
938
/// - Model type is unknown
939
/// - File is corrupted
940
29
pub fn load_apr_model(file_data: &[u8]) -> Result<()> {
941
    use crate::format::{detect_format, ModelFormat};
942
    use crate::model_loader::read_apr_model_type;
943
944
29
    println!("Parsing APR file...");
945
946
    // Verify format
947
29
    let 
format25
= detect_format(file_data).map_err(|e| RealizarError::UnsupportedOperation {
948
4
        operation: "detect_apr_format".to_string(),
949
4
        reason: format!("Format detection failed: {e}"),
950
4
    })?;
951
952
25
    if format != ModelFormat::Apr {
953
2
        return Err(RealizarError::UnsupportedOperation {
954
2
            operation: "verify_apr_magic".to_string(),
955
2
            reason: format!("Expected APR format, got {format}"),
956
2
        });
957
23
    }
958
959
    // Extract model type
960
23
    let model_type = read_apr_model_type(file_data).unwrap_or_else(|| 
"Unknown"1
.
to_string1
());
961
962
23
    println!("Successfully parsed APR file");
963
23
    println!();
964
23
    println!("Model Information:");
965
23
    println!("  Format: APR (Aprender Native)");
966
23
    println!("  Model Type: {model_type}");
967
23
    println!("  File Size: {} bytes", file_data.len());
968
23
    println!();
969
970
    // APR header structure: APRN (4) + type_id (2) + version (2) = 8 bytes minimum
971
23
    if file_data.len() >= 8 {
972
23
        let version = u16::from_le_bytes([file_data[6], file_data[7]]);
973
23
        println!("  Header Version: {version}");
974
23
    
}0
975
976
23
    println!();
977
23
    println!("APR model ready for serving!");
978
23
    println!("Supported model types for inference:");
979
23
    println!("  - LogisticRegression, LinearRegression");
980
23
    println!("  - DecisionTree, RandomForest, GradientBoosting");
981
23
    println!("  - KNN, GaussianNB, LinearSVM");
982
23
    println!();
983
23
    println!("To serve this model, the serve API will auto-detect");
984
23
    println!("the model type and dispatch to the appropriate handler.");
985
986
23
    Ok(())
987
29
}
988
989
/// Check if a model reference is a local file path
990
65
pub fn is_local_file_path(model_ref: &str) -> bool {
991
65
    model_ref.starts_with("./")
992
51
        || model_ref.starts_with('/')
993
41
        || model_ref.ends_with(".gguf")
994
32
        || model_ref.ends_with(".safetensors")
995
25
        || model_ref.ends_with(".apr")
996
65
}
997
998
/// Simple home directory resolution
999
6
pub fn home_dir() -> Option<std::path::PathBuf> {
1000
6
    std::env::var_os("HOME").map(std::path::PathBuf::from)
1001
6
}
1002
1003
/// Validate benchmark suite name
1004
59
pub fn validate_suite_name(suite_name: &str) -> bool {
1005
323
    
BENCHMARK_SUITES.iter()59
.
any59
(|(name, _)| *name == suite_name)
1006
59
}
1007
1008
// ============================================================================
1009
// Server Commands (extracted from main.rs for testability)
1010
// WAPR-PERF-004: Gated behind "server" feature since depends on crate::api
1011
// ============================================================================
1012
#[cfg(feature = "server")]
1013
mod server_commands {
1014
    use super::{load_apr_model, load_safetensors_model, Result};
1015
1016
    /// Result of preparing server state (returned by `prepare_serve_state`)
1017
    pub struct PreparedServer {
1018
        /// The prepared AppState for the server
1019
        pub state: crate::api::AppState,
1020
        /// Whether batch mode is enabled
1021
        pub batch_mode_enabled: bool,
1022
        /// Model type that was loaded
1023
        pub model_type: ModelType,
1024
    }
1025
1026
    impl std::fmt::Debug for PreparedServer {
1027
5
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1028
5
            f.debug_struct("PreparedServer")
1029
5
                .field("batch_mode_enabled", &self.batch_mode_enabled)
1030
5
                .field("model_type", &self.model_type)
1031
5
                .finish_non_exhaustive()
1032
5
        }
1033
    }
1034
1035
    /// Type of model being served
1036
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1037
    pub enum ModelType {
1038
        /// GGUF quantized model
1039
        Gguf,
1040
        /// SafeTensors model
1041
        SafeTensors,
1042
        /// APR format model
1043
        Apr,
1044
    }
1045
1046
    impl std::fmt::Display for ModelType {
1047
3
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1048
3
            match self {
1049
1
                ModelType::Gguf => write!(f, "GGUF"),
1050
1
                ModelType::SafeTensors => write!(f, "SafeTensors"),
1051
1
                ModelType::Apr => write!(f, "APR"),
1052
            }
1053
3
        }
1054
    }
1055
1056
    /// Prepare server state by loading a model (GGUF/SafeTensors/APR)
1057
    ///
1058
    /// This function is extracted from `serve_model` for testability.
1059
    /// It handles model loading and AppState creation without starting the server.
1060
    ///
1061
    /// # Arguments
1062
    /// * `model_path` - Path to model file (.gguf, .safetensors, or .apr)
1063
    /// * `batch_mode` - Enable batch processing (requires 'gpu' feature)
1064
    /// * `force_gpu` - Force CUDA backend (requires 'cuda' feature)
1065
    ///
1066
    /// # Returns
1067
    /// A `PreparedServer` containing the AppState and configuration
1068
11
    pub fn prepare_serve_state(
1069
11
        model_path: &str,
1070
11
        batch_mode: bool,
1071
11
        force_gpu: bool,
1072
11
    ) -> Result<PreparedServer> {
1073
        use crate::gguf::MappedGGUFModel;
1074
1075
11
        println!("Loading model from: {model_path}");
1076
11
        if batch_mode {
1077
1
            println!("Mode: BATCH (PARITY-093 M4 parity)");
1078
10
        } else {
1079
10
            println!("Mode: SINGLE-REQUEST");
1080
10
        }
1081
11
        if force_gpu {
1082
1
            println!("GPU: FORCED (--gpu flag)");
1083
10
        }
1084
11
        println!();
1085
1086
11
        if model_path.ends_with(".gguf") {
1087
            // Load GGUF model
1088
4
            println!("Parsing GGUF file...");
1089
4
            let 
mapped0
= MappedGGUFModel::from_path(model_path).map_err(|e| {
1090
4
                crate::error::RealizarError::UnsupportedOperation {
1091
4
                    operation: "load_gguf".to_string(),
1092
4
                    reason: format!("Failed to load GGUF: {e}"),
1093
4
                }
1094
4
            })?;
1095
1096
0
            println!("Successfully loaded GGUF model");
1097
0
            println!("  Tensors: {}", mapped.model.tensors.len());
1098
0
            println!("  Metadata: {} entries", mapped.model.metadata.len());
1099
0
            println!();
1100
1101
            // IMP-100: Use OwnedQuantizedModel with fused Q4_K ops (1.37x faster for single-token)
1102
0
            println!("Creating quantized model (fused Q4_K ops)...");
1103
0
            let quantized_model =
1104
0
                crate::gguf::OwnedQuantizedModel::from_mapped(&mapped).map_err(|e| {
1105
0
                    crate::error::RealizarError::UnsupportedOperation {
1106
0
                        operation: "create_quantized".to_string(),
1107
0
                        reason: format!("Failed to create quantized model: {e}"),
1108
0
                    }
1109
0
                })?;
1110
1111
0
            println!("Quantized model created successfully!");
1112
0
            println!("  Vocab size: {}", quantized_model.config.vocab_size);
1113
0
            println!("  Hidden dim: {}", quantized_model.config.hidden_dim);
1114
0
            println!("  Layers: {}", quantized_model.layers.len());
1115
1116
            // Extract vocabulary from GGUF for proper token decoding
1117
0
            let vocab = mapped.model.vocabulary().unwrap_or_else(|| {
1118
0
                eprintln!("  Warning: No vocabulary in GGUF, using placeholder tokens");
1119
0
                (0..quantized_model.config.vocab_size)
1120
0
                    .map(|i| format!("token{i}"))
1121
0
                    .collect()
1122
0
            });
1123
0
            println!("  Vocab loaded: {} tokens", vocab.len());
1124
0
            println!();
1125
1126
            // PARITY-113: Enable CUDA backend via --gpu flag or REALIZAR_BACKEND environment variable
1127
            #[cfg(feature = "cuda")]
1128
            let use_cuda = force_gpu
1129
                || std::env::var("REALIZAR_BACKEND")
1130
                    .map(|v| v.eq_ignore_ascii_case("cuda"))
1131
                    .unwrap_or(false);
1132
1133
            #[cfg(not(feature = "cuda"))]
1134
0
            let use_cuda = false;
1135
1136
            #[cfg(not(feature = "cuda"))]
1137
0
            if force_gpu {
1138
0
                eprintln!("Warning: --gpu flag requires 'cuda' feature. Falling back to CPU.");
1139
0
                eprintln!("Build with: cargo build --features cuda");
1140
0
                eprintln!();
1141
0
            }
1142
1143
            // PARITY-093: Use cached model with batch support for M4 parity
1144
            // PAR-112-FIX: Use OwnedQuantizedModelCuda for true streaming support
1145
0
            let state = if use_cuda && !batch_mode {
1146
                // PAR-112-FIX: Create OwnedQuantizedModelCuda for true streaming
1147
                // This enables generate_gpu_resident_streaming which streams tokens as generated
1148
                #[cfg(feature = "cuda")]
1149
                {
1150
                    use crate::gguf::OwnedQuantizedModelCuda;
1151
1152
                    let source = if force_gpu {
1153
                        "--gpu flag"
1154
                    } else {
1155
                        "REALIZAR_BACKEND=cuda"
1156
                    };
1157
                    println!("Creating CUDA model ({source})...");
1158
1159
                    let max_seq_len = 4096; // Support long sequences
1160
                    let cuda_model =
1161
                        OwnedQuantizedModelCuda::with_max_seq_len(quantized_model, 0, max_seq_len)
1162
                            .map_err(|e| crate::error::RealizarError::UnsupportedOperation {
1163
                                operation: "cuda_model_create".to_string(),
1164
                                reason: format!("CUDA model creation failed: {e}"),
1165
                            })?;
1166
1167
                    println!("  CUDA model created on GPU: {}", cuda_model.device_name());
1168
                    println!("  Max sequence length: {}", max_seq_len);
1169
                    println!("  TRUE STREAMING: enabled (PAR-112)");
1170
                    println!();
1171
1172
                    // Use with_cuda_model_and_vocab to enable true streaming path
1173
                    crate::api::AppState::with_cuda_model_and_vocab(cuda_model, vocab)?
1174
                }
1175
1176
                #[cfg(not(feature = "cuda"))]
1177
                {
1178
                    // This branch is unreachable since use_cuda is always false without cuda feature
1179
0
                    crate::api::AppState::with_quantized_model_and_vocab(quantized_model, vocab)?
1180
                }
1181
0
            } else if batch_mode {
1182
                #[cfg(feature = "gpu")]
1183
                {
1184
                    use crate::gguf::OwnedQuantizedModelCachedSync;
1185
1186
0
                    println!("Initializing batch inference mode (PARITY-093/094)...");
1187
1188
                    // Create cached model for scheduler reuse (10.6x speedup - IMP-112)
1189
0
                    let cached_model = OwnedQuantizedModelCachedSync::new(quantized_model);
1190
1191
                    // PARITY-094: Warmup GPU cache for batch_generate_gpu
1192
                    // This dequantizes FFN weights to GPU memory (~6GB for phi-2)
1193
0
                    println!("  Warming up GPU cache (dequantizing FFN weights)...");
1194
0
                    match cached_model.warmup_gpu_cache() {
1195
0
                        Ok((memory_bytes, num_layers)) => {
1196
0
                            println!(
1197
0
                                "  GPU cache ready: {:.2} GB ({} layers)",
1198
0
                                memory_bytes as f64 / 1e9,
1199
0
                                num_layers
1200
0
                            );
1201
0
                        },
1202
0
                        Err(e) => {
1203
0
                            eprintln!(
1204
0
                            "  Warning: GPU cache warmup failed: {}. Falling back to CPU batch.",
1205
0
                            e
1206
0
                        );
1207
0
                        },
1208
                    }
1209
1210
                    // Create state first (this wraps model in Arc internally)
1211
0
                    let state =
1212
0
                        crate::api::AppState::with_cached_model_and_vocab(cached_model, vocab)?;
1213
1214
                    // Get Arc'd model back for batch processor
1215
0
                    let cached_model_arc = state
1216
0
                        .cached_model()
1217
0
                        .expect("cached_model should exist")
1218
0
                        .clone();
1219
1220
                    // Configure batch processing (PARITY-095: aligned thresholds)
1221
0
                    let batch_config = crate::api::BatchConfig::default();
1222
0
                    println!("  Batch window: {}ms", batch_config.window_ms);
1223
0
                    println!("  Min batch size: {}", batch_config.min_batch);
1224
0
                    println!("  Optimal batch: {}", batch_config.optimal_batch);
1225
0
                    println!("  Max batch size: {}", batch_config.max_batch);
1226
0
                    println!(
1227
0
                        "  GPU threshold: {} (GPU GEMM for batch >= this)",
1228
                        batch_config.gpu_threshold
1229
                    );
1230
1231
                    // Spawn batch processor task
1232
0
                    let batch_tx =
1233
0
                        crate::api::spawn_batch_processor(cached_model_arc, batch_config.clone());
1234
1235
0
                    println!("  Batch processor: RUNNING");
1236
0
                    println!();
1237
1238
                    // Add batch support to state
1239
0
                    state.with_batch_config(batch_tx, batch_config)
1240
                }
1241
1242
                #[cfg(not(feature = "gpu"))]
1243
                {
1244
                    eprintln!(
1245
                    "Warning: --batch requires 'gpu' feature. Falling back to single-request mode."
1246
                );
1247
                    crate::api::AppState::with_quantized_model_and_vocab(quantized_model, vocab)?
1248
                }
1249
            } else {
1250
                // CPU mode: Use quantized model for serving (fused CPU ops are faster for m=1)
1251
0
                crate::api::AppState::with_quantized_model_and_vocab(quantized_model, vocab)?
1252
            };
1253
1254
0
            Ok(PreparedServer {
1255
0
                state,
1256
0
                batch_mode_enabled: batch_mode,
1257
0
                model_type: ModelType::Gguf,
1258
0
            })
1259
7
        } else if model_path.ends_with(".safetensors") {
1260
2
            let 
file_data0
= std::fs::read(model_path).map_err(|e| {
1261
2
                crate::error::RealizarError::UnsupportedOperation {
1262
2
                    operation: "read_model_file".to_string(),
1263
2
                    reason: format!("Failed to read {model_path}: {e}"),
1264
2
                }
1265
2
            })?;
1266
0
            load_safetensors_model(&file_data)?;
1267
            // SafeTensors models use demo state (full serving requires GGUF conversion)
1268
            Ok(PreparedServer {
1269
0
                state: crate::api::AppState::demo()?,
1270
                batch_mode_enabled: false,
1271
0
                model_type: ModelType::SafeTensors,
1272
            })
1273
5
        } else if model_path.ends_with(".apr") {
1274
2
            let 
file_data0
= std::fs::read(model_path).map_err(|e| {
1275
2
                crate::error::RealizarError::UnsupportedOperation {
1276
2
                    operation: "read_model_file".to_string(),
1277
2
                    reason: format!("Failed to read {model_path}: {e}"),
1278
2
                }
1279
2
            })?;
1280
0
            load_apr_model(&file_data)?;
1281
            // APR models use demo state (full serving requires GGUF conversion)
1282
            Ok(PreparedServer {
1283
0
                state: crate::api::AppState::demo()?,
1284
                batch_mode_enabled: false,
1285
0
                model_type: ModelType::Apr,
1286
            })
1287
        } else {
1288
3
            Err(crate::error::RealizarError::UnsupportedOperation {
1289
3
                operation: "detect_model_type".to_string(),
1290
3
                reason: "Unsupported file extension. Expected .gguf, .safetensors, or .apr"
1291
3
                    .to_string(),
1292
3
            })
1293
        }
1294
11
    }
1295
1296
    /// Serve a GGUF/SafeTensors/APR model via HTTP API
1297
    ///
1298
    /// This function was extracted from main.rs (PAR-112-FIX) to enable:
1299
    /// 1. Unit testing of server initialization logic
1300
    /// 2. Coverage measurement (main.rs was at 3.66%)
1301
    /// 3. Reuse from other entry points
1302
    ///
1303
    /// # Arguments
1304
    /// * `host` - Host to bind to (e.g., "0.0.0.0")
1305
    /// * `port` - Port to listen on
1306
    /// * `model_path` - Path to model file (.gguf, .safetensors, or .apr)
1307
    /// * `batch_mode` - Enable batch processing (requires 'gpu' feature)
1308
    /// * `force_gpu` - Force CUDA backend (requires 'cuda' feature)
1309
5
    pub async fn serve_model(
1310
5
        host: &str,
1311
5
        port: u16,
1312
5
        model_path: &str,
1313
5
        batch_mode: bool,
1314
5
        force_gpu: bool,
1315
5
    ) -> Result<()> {
1316
        // Prepare server state (testable)
1317
5
        let 
prepared0
= prepare_serve_state(model_path, batch_mode, force_gpu)?;
1318
1319
        // Create router
1320
0
        let app = crate::api::create_router(prepared.state);
1321
1322
        // Parse and validate address
1323
0
        let addr: std::net::SocketAddr = format!("{host}:{port}").parse().map_err(|e| {
1324
0
            crate::error::RealizarError::InvalidShape {
1325
0
                reason: format!("Invalid address: {e}"),
1326
0
            }
1327
0
        })?;
1328
1329
        // Print server info
1330
0
        println!("Server listening on http://{addr}");
1331
0
        println!();
1332
0
        println!("Endpoints:");
1333
0
        println!("  GET  /health         - Health check");
1334
0
        println!("  POST /v1/completions - OpenAI-compatible completions");
1335
0
        if prepared.batch_mode_enabled {
1336
0
            println!("  POST /v1/batch/completions - GPU batch completions (PARITY-022)");
1337
0
            println!("  POST /v1/gpu/warmup  - Warmup GPU cache");
1338
0
            println!("  GET  /v1/gpu/status  - GPU status");
1339
0
        }
1340
0
        println!("  POST /generate       - Generate text (Q4_K fused)");
1341
0
        println!();
1342
1343
0
        if prepared.batch_mode_enabled {
1344
0
            println!("M4 Parity Target: 192 tok/s at concurrency >= 4");
1345
0
            println!("Benchmark with: wrk -t4 -c4 -d30s http://{addr}/v1/completions");
1346
0
            println!();
1347
0
        }
1348
1349
        // Bind and serve
1350
0
        let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1351
0
            crate::error::RealizarError::UnsupportedOperation {
1352
0
                operation: "bind".to_string(),
1353
0
                reason: format!("Failed to bind: {e}"),
1354
0
            }
1355
0
        })?;
1356
1357
0
        axum::serve(listener, app).await.map_err(|e| {
1358
0
            crate::error::RealizarError::UnsupportedOperation {
1359
0
                operation: "serve".to_string(),
1360
0
                reason: format!("Server error: {e}"),
1361
0
            }
1362
0
        })?;
1363
1364
0
        Ok(())
1365
5
    }
1366
1367
    /// Start a demo inference server (no model required)
1368
    ///
1369
    /// This is useful for testing the API without loading a real model.
1370
0
    pub async fn serve_demo(host: &str, port: u16) -> Result<()> {
1371
        use std::net::SocketAddr;
1372
1373
        println!("Starting Realizar inference server (demo mode)...");
1374
1375
        let state = crate::api::AppState::demo()?;
1376
        let app = crate::api::create_router(state);
1377
1378
        let addr: SocketAddr = format!("{host}:{port}").parse().map_err(|e| {
1379
            crate::error::RealizarError::InvalidShape {
1380
                reason: format!("Invalid address: {e}"),
1381
            }
1382
        })?;
1383
1384
        println!("Server listening on http://{addr}");
1385
        println!();
1386
        println!("Endpoints:");
1387
        println!("  GET  /health   - Health check");
1388
        println!("  POST /tokenize - Tokenize text");
1389
        println!("  POST /generate - Generate text");
1390
        println!();
1391
        println!("Example:");
1392
        println!("  curl http://{addr}/health");
1393
        println!();
1394
1395
        let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1396
            crate::error::RealizarError::InvalidShape {
1397
                reason: format!("Failed to bind: {e}"),
1398
            }
1399
        })?;
1400
1401
        axum::serve(listener, app).await.map_err(|e| {
1402
            crate::error::RealizarError::InvalidShape {
1403
                reason: format!("Server error: {e}"),
1404
            }
1405
        })?;
1406
1407
        Ok(())
1408
    }
1409
} // mod server_commands
1410
1411
#[cfg(feature = "server")]
1412
pub use server_commands::*;
1413
1414
// Tests extracted to tests.rs (PMAT-802)
1415
#[cfg(test)]
1416
#[path = "tests.rs"]
1417
mod cli_tests;