1use crate::error::{CliError, Result};
63use crate::output;
64use colored::Colorize;
65use serde::{Deserialize, Serialize};
66use std::path::Path;
67use std::time::{Duration, Instant};
68
69#[cfg(not(feature = "visualization"))]
70use brick_tracer_shim::BrickTracer as TracerImpl;
71#[cfg(feature = "visualization")]
72use renacer::brick_tracer::BrickTracer as TracerImpl;
73
74#[cfg(not(feature = "visualization"))]
77mod brick_tracer_shim {
78 pub struct SyscallBreakdown {
80 pub compute_us: u64,
81 pub mmap_us: u64,
82 pub futex_us: u64,
83 pub ioctl_us: u64,
84 }
85 impl SyscallBreakdown {
86 pub fn syscall_overhead_percent(&self) -> f64 {
87 0.0
88 }
89 pub fn dominant_syscall(&self) -> &'static str {
90 "none"
91 }
92 }
93
94 pub struct TraceMetadata {
96 pub budget_us: u64,
97 pub actual_us: u64,
98 pub efficiency: f64,
99 }
100
101 pub struct TracedResult<T> {
103 pub result: T,
104 pub duration_us: u64,
105 pub syscall_breakdown: SyscallBreakdown,
106 pub metadata: Option<TraceMetadata>,
107 }
108
109 pub struct BrickTracer;
111 impl BrickTracer {
112 pub fn new_local() -> Self {
113 Self
114 }
115 pub fn trace<T>(
116 &self,
117 _name: &str,
118 _budget_us: u64,
119 f: impl FnOnce() -> T,
120 ) -> TracedResult<T> {
121 let start = std::time::Instant::now();
122 let result = f();
123 let duration_us = start.elapsed().as_micros() as u64;
124 TracedResult {
125 result,
126 duration_us,
127 syscall_breakdown: SyscallBreakdown {
128 compute_us: duration_us,
129 mmap_us: 0,
130 futex_us: 0,
131 ioctl_us: 0,
132 },
133 metadata: None,
134 }
135 }
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct QaConfig {
142 pub min_tps: Option<f64>,
150 pub min_speedup: f64,
152 pub min_gpu_speedup: f64,
154 pub skip_golden: bool,
156 pub skip_throughput: bool,
158 pub skip_ollama: bool,
160 pub skip_gpu_speedup: bool,
162 pub skip_contract: bool,
164 pub skip_format_parity: bool,
166 pub skip_ptx_parity: bool,
168 pub safetensors_path: Option<std::path::PathBuf>,
170 pub iterations: usize,
172 pub warmup: usize,
174 pub max_tokens: usize,
176 pub json: bool,
178 pub verbose: bool,
180 pub min_executed: Option<usize>,
182 pub previous_report: Option<std::path::PathBuf>,
184 pub regression_threshold: f64,
186 pub skip_gpu_state: bool,
188 pub skip_metadata: bool,
190 pub skip_capability: bool,
192 pub assert_classifier_head: bool,
194}
195
196impl Default for QaConfig {
197 fn default() -> Self {
198 Self {
199 min_tps: None, min_speedup: 0.2, min_gpu_speedup: 2.0, skip_golden: false,
203 skip_throughput: false,
204 skip_ollama: false,
205 skip_gpu_speedup: false,
206 skip_contract: false,
207 skip_format_parity: false,
208 skip_ptx_parity: false,
209 safetensors_path: None,
210 iterations: 10,
211 warmup: 3,
212 max_tokens: 32,
213 json: false,
214 verbose: false,
215 min_executed: None,
216 previous_report: None,
217 regression_threshold: 0.10,
218 skip_gpu_state: false,
219 skip_metadata: false,
220 skip_capability: false,
221 assert_classifier_head: false,
222 }
223 }
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct GateResult {
229 pub name: String,
231 pub passed: bool,
233 pub message: String,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub value: Option<f64>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub threshold: Option<f64>,
241 pub duration_ms: u64,
243 pub skipped: bool,
245}
246
247impl GateResult {
248 pub(crate) fn passed(
249 name: &str,
250 message: &str,
251 value: Option<f64>,
252 threshold: Option<f64>,
253 duration: Duration,
254 ) -> Self {
255 Self {
256 name: name.to_string(),
257 passed: true,
258 message: message.to_string(),
259 value,
260 threshold,
261 duration_ms: duration.as_millis() as u64,
262 skipped: false,
263 }
264 }
265
266 pub(crate) fn failed(
267 name: &str,
268 message: &str,
269 value: Option<f64>,
270 threshold: Option<f64>,
271 duration: Duration,
272 ) -> Self {
273 Self {
274 name: name.to_string(),
275 passed: false,
276 message: message.to_string(),
277 value,
278 threshold,
279 duration_ms: duration.as_millis() as u64,
280 skipped: false,
281 }
282 }
283
284 pub(crate) fn skipped(name: &str, reason: &str) -> Self {
285 Self {
286 name: name.to_string(),
287 passed: true, message: format!("Skipped: {reason}"),
289 value: None,
290 threshold: None,
291 duration_ms: 0,
292 skipped: true,
293 }
294 }
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct SystemInfo {
300 pub cpu_model: String,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub gpu_model: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub gpu_driver: Option<String>,
308}
309
310impl SystemInfo {
311 fn capture() -> Self {
312 let cpu_model = std::fs::read_to_string("/proc/cpuinfo")
313 .ok()
314 .and_then(|s| {
315 s.lines()
316 .find(|l| l.starts_with("model name"))
317 .and_then(|l| l.split(':').nth(1))
318 .map(|s| s.trim().to_string())
319 })
320 .unwrap_or_else(|| "unknown".to_string());
321
322 let (gpu_model, gpu_driver) = Self::detect_gpu();
323
324 Self {
325 cpu_model,
326 gpu_model,
327 gpu_driver,
328 }
329 }
330
331 fn detect_gpu() -> (Option<String>, Option<String>) {
332 let output = std::process::Command::new("nvidia-smi")
333 .args(["--query-gpu=name,driver_version", "--format=csv,noheader"])
334 .output()
335 .ok();
336 if let Some(out) = output {
337 if out.status.success() {
338 let text = String::from_utf8_lossy(&out.stdout);
339 let parts: Vec<&str> = text.trim().splitn(2, ',').collect();
340 return (
341 parts.first().map(|s| s.trim().to_string()),
342 parts.get(1).map(|s| s.trim().to_string()),
343 );
344 }
345 }
346 (None, None)
347 }
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct QaReport {
353 pub model: String,
355 pub passed: bool,
357 pub gates: Vec<GateResult>,
359 #[serde(default)]
361 pub gates_executed: usize,
362 #[serde(default)]
364 pub gates_skipped: usize,
365 pub total_duration_ms: u64,
367 pub timestamp: String,
369 pub summary: String,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub system_info: Option<SystemInfo>,
374}
375
376#[allow(clippy::too_many_arguments)]
378#[provable_contracts_macros::contract(
379 "apr-cli-operations-v1",
380 equation = "side_effect_classification"
381)]
382pub fn run(
383 path: &Path,
384 min_tps: Option<f64>,
385 min_speedup: Option<f64>,
386 min_gpu_speedup: Option<f64>,
387 skip_golden: bool,
388 skip_throughput: bool,
389 skip_ollama: bool,
390 skip_gpu_speedup: bool,
391 skip_contract: bool,
392 skip_format_parity: bool,
393 skip_ptx_parity: bool,
394 safetensors_path: Option<std::path::PathBuf>,
395 iterations: usize,
396 warmup: usize,
397 max_tokens: usize,
398 json: bool,
399 verbose: bool,
400 min_executed: Option<usize>,
401 previous_report: Option<std::path::PathBuf>,
402 regression_threshold: Option<f64>,
403 skip_gpu_state: bool,
404 skip_metadata: bool,
405 skip_capability: bool,
406 assert_classifier_head: bool,
407) -> Result<()> {
408 contract_pre_qa_gate_composition!();
409 use crate::commands::threshold_arg;
413 threshold_arg::guard_opt("--assert-tps", min_tps, threshold_arg::TOLERANCE)?;
414 threshold_arg::guard_opt("--assert-speedup", min_speedup, threshold_arg::TOLERANCE)?;
415 threshold_arg::guard_opt(
416 "--assert-gpu-speedup",
417 min_gpu_speedup,
418 threshold_arg::TOLERANCE,
419 )?;
420 threshold_arg::guard_opt(
421 "--regression-threshold",
422 regression_threshold,
423 threshold_arg::FRACTION,
424 )?;
425
426 let config = QaConfig {
427 min_tps,
428 min_speedup: min_speedup.unwrap_or(0.2), min_gpu_speedup: min_gpu_speedup.unwrap_or(2.0), skip_golden,
431 skip_throughput,
432 skip_ollama,
433 skip_gpu_speedup,
434 skip_contract,
435 skip_format_parity,
436 skip_ptx_parity,
437 safetensors_path,
438 iterations,
439 warmup,
440 max_tokens,
441 json,
442 verbose,
443 min_executed,
444 previous_report,
445 regression_threshold: regression_threshold.unwrap_or(0.10),
446 skip_gpu_state,
447 skip_metadata,
448 skip_capability,
449 assert_classifier_head,
450 };
451
452 let report = run_qa(path, &config)?;
453
454 if json {
455 println!(
456 "{}",
457 serde_json::to_string_pretty(&report).unwrap_or_default()
458 );
459 }
460
461 if !report.passed {
462 return Err(CliError::ValidationFailed(report.summary));
463 }
464
465 contract_post_qa_gate_composition!(&());
466 Ok(())
467}
468
469fn dispatch_gate(
471 gates: &mut Vec<GateResult>,
472 json: bool,
473 skip: bool,
474 name: &str,
475 skip_reason: &str,
476 runner: impl FnOnce() -> Result<GateResult>,
477) -> Result<()> {
478 let result = if skip {
479 GateResult::skipped(name, skip_reason)
480 } else {
481 runner()?
482 };
483 if !json {
484 print_gate_result(&result);
485 }
486 gates.push(result);
487 Ok(())
488}
489
490fn gate_display_name(name: &str) -> &str {
493 match name {
494 "capability_match" => "Capability Match",
495 "tensor_contract" => "Tensor Contract",
496 "golden_output" => "Golden Output",
497 "throughput" => "Throughput",
498 "ollama_parity" => "Ollama Parity",
499 "gpu_speedup" => "GPU Speedup",
500 "format_parity" => "Format Parity",
501 "ptx_parity" => "PTX Parity",
502 "gpu_state_isolation" => "GPU State Isolation",
503 "performance_regression" => "Perf Regression",
504 "metadata_plausibility" => "Metadata Plausibility",
505 "classifier_head" => "Classifier Head",
506 other => other,
507 }
508}
509
510fn print_qa_summary(gates: &[GateResult], passed: bool, total_duration: Duration) {
512 output::header("QA Summary");
513
514 let gate_rows: Vec<Vec<String>> = gates
515 .iter()
516 .map(|g| {
517 let badge = if g.skipped {
518 output::badge_skip("SKIP")
519 } else if g.passed {
520 output::badge_pass("PASS")
521 } else {
522 output::badge_fail("FAIL")
523 };
524 let measured = g.value.map_or("—".to_string(), |v| format!("{v:.2}"));
525 let threshold = g.threshold.map_or("—".to_string(), |v| format!("{v:.2}"));
526 vec![
527 gate_display_name(&g.name).to_string(),
528 badge,
529 measured,
530 threshold,
531 output::duration_fmt(g.duration_ms),
532 ]
533 })
534 .collect();
535 println!(
536 "{}",
537 output::table(
538 &["Gate", "Status", "Measured", "Threshold", "Duration"],
539 &gate_rows,
540 )
541 );
542
543 println!();
544 if passed {
545 println!(" {}", output::badge_pass("ALL GATES PASSED"));
546 } else {
547 println!(" {}", output::badge_fail("GATES FAILED"));
548 for gate in gates.iter().filter(|g| !g.passed && !g.skipped) {
549 println!(" {} {}", "✗".red(), gate.name);
550 }
551 }
552 output::metric(
553 "Total Duration",
554 output::duration_fmt(total_duration.as_millis() as u64),
555 "",
556 );
557}
558
559include!("qa_gguf.rs");
560include!("output_verification.rs");
561include!("golden_output.rs");
562include!("speedup.rs");
563include!("forward_error.rs");
564include!("gpu_isolation_result.rs");
565include!("qa_08.rs");