rtx-csm: Phase 8.2 — extended boot warm-up (~50% TTFA reduction)

Increased the boot warm-up from `max_audio_ms=200` (~3 frames) to
`max_audio_ms=2000` (~25 frames) and switched the prompt to a fuller
sentence so more Metal kernel paths fire during the throwaway gen.

The original 200 ms warm-up only compiled the early fast paths; the
first user turn still paid 4-5 s of additional kernel compilation as
new branches lit up under longer-context generation. The 2 s warm-up
gives the JIT a chance to compile everything.

3-turn bench (mock LLM, Q8 + stream, M-series Metal, 10.43 s
LibriSpeech in):

  Metric                   Original warm-up   Extended warm-up   Δ
  Boot warm-up cost        819 ms             2748 ms            +1.9 s
  client first_audio_ms p50 4915 ms           529 ms             -89%
  llm_to_first_audio       4892 ms            627 ms             -87%
  total_turn               18408 ms           18112 ms           wash

Sub-second TTFA on every turn. The extra 2 s at boot is paid back on
the first user turn — every turn after is pure win.

This is the new production-recommended config:
  --quantized-gguf <Q8> --stream-tts --vad-gate
  (warm-up always on; no flag toggle).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-04-27 09:07:29 -07:00
co-authored by Claude Opus 4.7
parent f4a6ffeaf5
commit 64386720a3
@@ -548,19 +548,26 @@ async fn main() -> Result<()> {
}; };
tracing::info!("models loaded"); tracing::info!("models loaded");
// Warm up the Generator: do one throwaway generate() so Metal kernel // Warm up the Generator: a longer throwaway generate() so the full
// compilation + first-frame KV cache init happen now, not on the // Metal kernel set (backbone + depth decoder + Mimi decode + every
// first user turn. Empirically this eliminates ~5-6s of dead time // sampler path) compiles up-front. Empirically eliminates ~4-6 s
// from the first-sentence TTS gen latency. Cost is paid once at // from the first-sentence TTS gen latency. Cost is paid once at
// boot. The throwaway audio is discarded. // boot. Phase 8.2 raised this from 200 ms to 2000 ms (~25 frames)
// because at 200 ms only the early code paths fired — variable
// first-sentence cost still showed up on first user turn.
let warmup_t = std::time::Instant::now(); let warmup_t = std::time::Instant::now();
{ {
let opts = GenerateOptions { let opts = GenerateOptions {
max_audio_ms: 200, max_audio_ms: 2000,
seed: 0, seed: 0,
..GenerateOptions::default() ..GenerateOptions::default()
}; };
match generator.generate("Hi.", cli.speaker, &[], opts) { match generator.generate(
"Warming up the speech model with a slightly longer prompt.",
cli.speaker,
&[],
opts,
) {
Ok(_) => { Ok(_) => {
tracing::info!( tracing::info!(
"generator warm-up complete ({}ms)", "generator warm-up complete ({}ms)",