Skip to main content

qts/
synthesis_profile.rs

1//! Wall-clock stage timings for one synthesis pass (tokenizer / transformer / vocoder, etc.).
2
3use std::fmt::Write as _;
4use std::time::Duration;
5
6use crate::pipeline::tts_transformer::CodecRolloutSubTimings;
7
8/// Per-stage wall times for a single [`crate::Qwen3TtsEngine::synthesize_with_profile`] call.
9///
10/// When pipelining is disabled, stages are sequential and their sum approximates
11/// end-to-end latency. With pipelining (`vocoder_chunk_size > 0`), `codec_rollout`
12/// and `vocoder_decode` overlap; use [`total()`] which accounts for this via
13/// `pipeline_overlap`.
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct SynthesisStageTimings {
16    /// Reference WAV → speaker embedding (zero if not used).
17    pub speaker_encode: Duration,
18    /// Text → token ids (includes voice-clone ref+target path when applicable).
19    pub tokenize: Duration,
20    /// `build_prefill_inputs` (text projection + codec input layout).
21    pub prefill_build: Duration,
22    /// `rollout_codec_frames_kv` (talker prefill + autoregressive codec steps).
23    pub codec_rollout: Duration,
24    /// Vocoder decode to PCM.
25    pub vocoder_decode: Duration,
26    /// Flatten codec rows + optional ICL prefix trim on PCM.
27    pub post: Duration,
28    /// Time saved by overlapping transformer and vocoder (pipelining).
29    /// Zero when pipelining is disabled.
30    pub pipeline_overlap: Duration,
31    /// Time from synthesis start to first generated codec frame (includes
32    /// speaker encode + tokenize + prefill + first frame prediction).
33    /// This is the theoretical minimum latency for streaming playback.
34    pub first_frame_latency: Duration,
35    /// Sub-component breakdown within the `codec_rollout` stage.
36    pub codec_rollout_detail: CodecRolloutSubTimings,
37    /// Number of PCM samples in the final output (after prefix trim).
38    pub generated_samples: usize,
39    /// Output sample rate (Hz).
40    pub sample_rate_hz: u32,
41}
42
43impl SynthesisStageTimings {
44    #[must_use]
45    pub fn total(&self) -> Duration {
46        let sum = self.speaker_encode
47            + self.tokenize
48            + self.prefill_build
49            + self.codec_rollout
50            + self.vocoder_decode
51            + self.post;
52        sum.saturating_sub(self.pipeline_overlap)
53    }
54
55    /// Arithmetic mean of each stage (wall clock). Empty slice returns `None`.
56    #[must_use]
57    pub fn average(samples: &[Self]) -> Option<Self> {
58        if samples.is_empty() {
59            return None;
60        }
61        let n = samples.len() as u128;
62        let avg = |field: fn(&Self) -> Duration| {
63            let sum: u128 = samples.iter().map(|s| field(s).as_nanos()).sum();
64            let nanos = (sum / n).min(u128::from(u64::MAX)) as u64;
65            Duration::from_nanos(nanos)
66        };
67        let avg_usize = |field: fn(&Self) -> usize| {
68            let sum: u128 = samples.iter().map(|s| field(s) as u128).sum();
69            (sum / n) as usize
70        };
71        Some(Self {
72            speaker_encode: avg(|s| s.speaker_encode),
73            tokenize: avg(|s| s.tokenize),
74            prefill_build: avg(|s| s.prefill_build),
75            codec_rollout: avg(|s| s.codec_rollout),
76            vocoder_decode: avg(|s| s.vocoder_decode),
77            post: avg(|s| s.post),
78            pipeline_overlap: avg(|s| s.pipeline_overlap),
79            first_frame_latency: avg(|s| s.first_frame_latency),
80            codec_rollout_detail: CodecRolloutSubTimings {
81                talker_prefill: avg(|s| s.codec_rollout_detail.talker_prefill),
82                talker_steps: avg(|s| s.codec_rollout_detail.talker_steps),
83                code_pred_total: avg(|s| s.codec_rollout_detail.code_pred_total),
84                kv_writeback: avg(|s| s.codec_rollout_detail.kv_writeback),
85                kv_download: avg(|s| s.codec_rollout_detail.kv_download),
86                kv_quantize: avg(|s| s.codec_rollout_detail.kv_quantize),
87                kv_upload: avg(|s| s.codec_rollout_detail.kv_upload),
88                talker_kv_bytes: avg_usize(|s| s.codec_rollout_detail.talker_kv_bytes),
89            },
90            generated_samples: avg_usize(|s| s.generated_samples),
91            sample_rate_hz: samples[0].sample_rate_hz,
92        })
93    }
94
95    /// Multi-line table: stage name, milliseconds, % of total.
96    #[must_use]
97    pub fn format_table(&self) -> String {
98        let total = self.total();
99        let total_ms = duration_ms(total);
100        let mut out = String::new();
101        let _ = writeln!(
102            out,
103            "synthesis stage timings (wall clock, single run){}",
104            if total_ms > 0.0 {
105                String::new()
106            } else {
107                " — total 0ms".to_string()
108            }
109        );
110        let rows: [(&str, Duration); 6] = [
111            ("speaker_encode", self.speaker_encode),
112            ("tokenize", self.tokenize),
113            ("prefill_build", self.prefill_build),
114            ("codec_rollout (transformer)", self.codec_rollout),
115            ("vocoder_decode", self.vocoder_decode),
116            ("post (flatten/trim)", self.post),
117        ];
118        for (name, d) in rows {
119            let ms = duration_ms(d);
120            let pct = if total.is_zero() {
121                0.0
122            } else {
123                100.0 * ms / total_ms.max(f64::EPSILON)
124            };
125            let _ = writeln!(out, "  {name:<28} {ms:>10.3} ms  ({pct:>5.1}%)");
126        }
127        let d = &self.codec_rollout_detail;
128        let has_detail = !d.talker_prefill.is_zero()
129            || !d.talker_steps.is_zero()
130            || !d.code_pred_total.is_zero()
131            || !d.kv_writeback.is_zero()
132            || !d.kv_download.is_zero()
133            || !d.kv_quantize.is_zero()
134            || !d.kv_upload.is_zero()
135            || d.talker_kv_bytes > 0;
136        if has_detail {
137            let sub_rows: [(&str, Duration); 7] = [
138                ("  talker_prefill", d.talker_prefill),
139                ("  talker_steps", d.talker_steps),
140                ("  code_pred_total", d.code_pred_total),
141                ("  kv_writeback", d.kv_writeback),
142                ("  kv_download", d.kv_download),
143                ("  kv_quantize", d.kv_quantize),
144                ("  kv_upload", d.kv_upload),
145            ];
146            for (name, sd) in sub_rows {
147                let _ = writeln!(out, "    {name:<26} {:>10.3} ms", duration_ms(sd));
148            }
149            if d.talker_kv_bytes > 0 {
150                let mib = d.talker_kv_bytes as f64 / (1024.0 * 1024.0);
151                let _ = writeln!(out, "    {:<26} {:>10.3} MiB", "talker_kv_bytes", mib);
152            }
153        }
154        if !self.pipeline_overlap.is_zero() {
155            let overlap_ms = duration_ms(self.pipeline_overlap);
156            let _ = writeln!(
157                out,
158                "  {:<28} {:>+10.3} ms  (pipeline overlap)",
159                "pipeline_overlap", -overlap_ms
160            );
161        }
162        if !self.first_frame_latency.is_zero() {
163            let _ = writeln!(
164                out,
165                "  {:<28} {:>10.3} ms",
166                "first_frame_latency",
167                duration_ms(self.first_frame_latency)
168            );
169        }
170        let _ = writeln!(out, "  {:<28} {:>10.3} ms  (100.0%)", "total", total_ms);
171        if self.sample_rate_hz > 0 && self.generated_samples > 0 {
172            let audio_secs = self.generated_samples as f64 / self.sample_rate_hz as f64;
173            let audio_ms = audio_secs * 1_000.0;
174            let rtf = if audio_secs > 0.0 {
175                total.as_secs_f64() / audio_secs
176            } else {
177                0.0
178            };
179            let _ = writeln!(
180                out,
181                "  {:<28} {:>10.3} ms  (RTF = {:.3}x)",
182                "audio duration", audio_ms, rtf
183            );
184        }
185        out
186    }
187}
188
189fn duration_ms(d: Duration) -> f64 {
190    d.as_secs_f64() * 1_000.0
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn format_table_zero_total() {
199        let t = SynthesisStageTimings::default();
200        let s = t.format_table();
201        assert!(s.contains("speaker_encode"));
202        assert!(s.contains("total"));
203    }
204
205    #[test]
206    fn average_two_samples() {
207        let a = SynthesisStageTimings {
208            tokenize: Duration::from_millis(2),
209            codec_rollout: Duration::from_millis(8),
210            ..Default::default()
211        };
212        let b = SynthesisStageTimings {
213            tokenize: Duration::from_millis(4),
214            codec_rollout: Duration::from_millis(12),
215            ..Default::default()
216        };
217        let m = SynthesisStageTimings::average(&[a, b]).unwrap();
218        assert_eq!(m.tokenize, Duration::from_millis(3));
219        assert_eq!(m.codec_rollout, Duration::from_millis(10));
220    }
221}