Skip to main content

qualia_core_db/inference/lab/
timeline.rs

1//! Decode timeline: host phase counters + optional GPU profile + tok/s.
2
3use std::path::Path;
4use std::time::Instant;
5
6use crate::hardware_passport::measure_decode_proxy_tok_s;
7use crate::llm_bench::{
8    phase_snapshot, reset_phase_metrics, reset_resident_path_counts, resident_path_counts,
9};
10use crate::llm_gpu_profiler::{self, Phase};
11
12#[derive(Debug, Clone)]
13pub struct DecodeTimeline {
14    pub model: String,
15    pub tokens: u32,
16    pub tok_s: Option<f64>,
17    pub wall_ms: f64,
18    pub resident_hits: u64,
19    pub resident_fallbacks: u64,
20    pub host_phase_ns: String,
21    pub gpu_phase_ns: String,
22    pub notes: String,
23}
24
25/// Run a short decode with phase counters; enable GPU timestamps if supported.
26pub fn run_decode_timeline(model: &Path, tokens: u32) -> DecodeTimeline {
27    let tokens = tokens.max(1).min(64);
28    reset_phase_metrics();
29    reset_resident_path_counts();
30    llm_gpu_profiler::set_enabled(true);
31    llm_gpu_profiler::reset();
32
33    let t0 = Instant::now();
34    let tok_s = measure_decode_proxy_tok_s(model, tokens);
35    let wall_ms = t0.elapsed().as_secs_f64() * 1e3;
36    let (hits, falls) = resident_path_counts();
37    let snap = phase_snapshot();
38    let host_phase_ns = format!(
39        "{{\"load_ns\":{},\"prefill_ns\":{},\"prefill_tokens\":{},\"decode_ns\":{},\"decode_tokens\":{},\"decode_forward_ns\":{},\"decode_output_ns\":{}}}",
40        snap.load_ns,
41        snap.prefill_ns,
42        snap.prefill_tokens,
43        snap.decode_ns,
44        snap.decode_tokens,
45        snap.decode_forward_ns,
46        snap.decode_output_ns
47    );
48
49    let mut gpu_parts = Vec::new();
50    for pt in llm_gpu_profiler::snapshot() {
51        if pt.calls > 0 || pt.total_ns > 0 {
52            gpu_parts.push(format!(
53                "\"{}\":{{\"ns\":{},\"calls\":{}}}",
54                pt.phase.label(),
55                pt.total_ns,
56                pt.calls
57            ));
58        }
59    }
60    let gpu_phase_ns = format!("{{{}}}", gpu_parts.join(","));
61
62    let mut notes = Vec::new();
63    if !llm_gpu_profiler::enabled() {
64        notes.push("GPU timestamps not active (device or flag)".into());
65    }
66    if falls > 0 {
67        notes.push(format!("resident fallbacks={falls}"));
68    }
69    if hits == 0 && tok_s.is_some() {
70        notes.push("no resident hits counted - check path".into());
71    }
72    let _ = Phase::COUNT;
73
74    llm_gpu_profiler::set_enabled(false);
75
76    DecodeTimeline {
77        model: model.display().to_string(),
78        tokens,
79        tok_s,
80        wall_ms,
81        resident_hits: hits,
82        resident_fallbacks: falls,
83        host_phase_ns,
84        gpu_phase_ns,
85        notes: notes.join("; "),
86    }
87}
88
89impl DecodeTimeline {
90    pub fn format_report(&self) -> String {
91        format!(
92            "Decode timeline\n  model:     {}\n  tokens:    {}\n  tok_s:     {}\n  wall_ms:   {:.2}\n  resident:  hits={} fallbacks={}\n  host_ns:   {}\n  gpu_ns:    {}\n  notes:     {}\n",
93            self.model,
94            self.tokens,
95            self.tok_s
96                .map(|t| format!("{t:.4}"))
97                .unwrap_or_else(|| "fail".into()),
98            self.wall_ms,
99            self.resident_hits,
100            self.resident_fallbacks,
101            self.host_phase_ns,
102            self.gpu_phase_ns,
103            self.notes
104        )
105    }
106}