Files
rustytorch/crates/models/rtx-csm/examples/moonshine_profile.rs
T
osobhandClaude Opus 4.7 95015c17e1 rtx-csm: Phase 8.9 — Moonshine KV cache + profile binary
KV cache for the decoder turns greedy generation from O(T^2) into O(T)
total work. Per-token decode drops modestly on short transcripts
(7.1 -> 6.0 ms/token at 49 tokens) and compounds on longer ones.

New components in src/moonshine.rs:

  RotaryCache::apply_at(x, position, t)
      Apply RoPE for a window starting at `position`. Replaces
      `apply()` for cached step (which always called positions 0..T).

  DecoderSelfAttention::forward_step(xs, cache_k, cache_v, rope, position)
      Single-token cached self-attn. Appends new K/V to per-layer cache,
      attends across full accumulated history. No causal mask needed
      (cache only contains positions <= current).

  CrossAttention::precompute_kv(enc) -> (K, V)
      One-shot encoder K/V projection for cross-attn. Reused every step.

  CrossAttention::forward_step(xs, k, v)
      Cached cross-attn. Q computed from new token; K/V from precompute.

  DecoderCache { self_k: Vec<Option<Tensor>>, self_v, cross_k, cross_v, position }

  Decoder::precompute_cross_kv(enc) -> DecoderCache
  Decoder::step(token_id, &mut cache) -> logits (1, vocab)
  Decoder::generate_cached(enc, cfg, max_tokens) -> Vec<u32>
      Greedy loop using the cached step.

Profile (5 steady-state runs on /tmp/asr_test.flac, 10.42 s LibriSpeech):

  warm-up:                344 ms
  steady-state mean: 307 ms (p50 305, range 298-319)
  realtime factor: 0.0294x

Comparison across all STT in rtx-csm:

  Backend           RTF         Notes
  Kyutai STT 1B     1.01x       hardware-bound, 3 GB
  Whisper-tiny      0.020x      breaks CSM (in-process ggml conflict)
  Moonshine-tiny    0.0294x     pure candle, NO runtime conflict

Moonshine is the only fast STT path that integrates cleanly. ~34x
faster than realtime, ~17x faster than Kyutai 1B, no protobuf or
ggml linkage issues.

New `examples/moonshine_profile` mirrors `stt_profile` and
`whisper_profile` so all three STT backends report comparable numbers.

Phase 8.10 (next): wire as a third AsrEngine variant in converse_server
for English-only deploys. Replace the energy-VAD-gated Kyutai path
when --moonshine flag is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-27 11:05:19 -07:00

115 lines
4.2 KiB
Rust

//! Phase 8.9 — standalone profile of Moonshine STT, comparable to
//! `examples/stt_profile` (Kyutai) and `examples/whisper_profile`
//! (Whisper-tiny via whisper-rs). Runs N transcriptions of the same
//! audio, reports per-call latency stats + realtime factor.
//!
//! Usage:
//! ```bash
//! cargo run -p rtx-csm --release --features metal --example moonshine_profile -- \
//! --in /tmp/asr_test.flac --repeat 5
//! ```
use anyhow::{Context, Result};
use candle_core::{Device, Tensor};
use clap::Parser;
use hf_hub::api::sync::Api;
use rtx_csm::{audio_io, moonshine};
use std::path::PathBuf;
use std::time::Instant;
#[derive(Debug, Parser)]
struct Cli {
#[arg(long = "in", default_value = "/tmp/asr_test.flac")]
input: PathBuf,
#[arg(long, default_value_t = 5)]
repeat: usize,
#[arg(long, default_value_t = 100)]
max_tokens: usize,
/// Force CPU device.
#[arg(long)]
cpu: bool,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let device = if cli.cpu {
Device::Cpu
} else if candle_core::utils::metal_is_available() {
Device::new_metal(0)?
} else {
Device::Cpu
};
eprintln!("device: {device:?}");
let api = Api::new()?;
let repo = api.model("UsefulSensors/moonshine-tiny".to_string());
let weights = repo.get("model.safetensors")?;
let tok_path = repo.get("tokenizer.json")?;
let pcm = audio_io::load_mono_at_rate(&cli.input, 16_000).context("load audio")?;
let audio_secs = pcm.len() as f32 / 16_000.0;
eprintln!("audio: {} samples ({audio_secs:.2}s @ 16 kHz)", pcm.len());
let cfg = moonshine::MoonshineConfig::tiny();
let load_t = Instant::now();
let (encoder, decoder) = moonshine::load_full(&weights, &device, &cfg)?;
let tok = moonshine::load_tokenizer(&tok_path)
.map_err(|e| anyhow::anyhow!("tok: {e}"))?;
eprintln!("load: {:.2}s", load_t.elapsed().as_secs_f32());
let pcm_tensor = Tensor::from_vec(pcm.clone(), (1, 1, pcm.len()), &device)?;
// Warm-up: first call pays JIT + cache init.
let warm_t = Instant::now();
let enc = encoder.forward(&pcm_tensor)?;
let _warm_tokens = decoder.generate_cached(&enc, &cfg, cli.max_tokens)?;
eprintln!("warm-up: {} ms", warm_t.elapsed().as_millis());
// Steady-state runs.
let mut per_call_ms: Vec<f64> = Vec::with_capacity(cli.repeat);
let mut last_text = String::new();
let mut last_token_count = 0usize;
for i in 0..cli.repeat {
let t = Instant::now();
let enc = encoder.forward(&pcm_tensor)?;
let tokens = decoder.generate_cached(&enc, &cfg, cli.max_tokens)?;
let ms = t.elapsed().as_secs_f64() * 1000.0;
per_call_ms.push(ms);
last_text = tok.decode(&tokens, true).map_err(|e| anyhow::anyhow!("detok: {e}"))?;
last_token_count = tokens.len();
eprintln!(" run {}: {ms:.0} ms ({} tokens)", i + 1, tokens.len());
}
per_call_ms.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = per_call_ms.len() as f64;
let mean = per_call_ms.iter().sum::<f64>() / n;
let p50 = per_call_ms[per_call_ms.len() / 2];
let realtime_factor = (mean / 1000.0) / audio_secs as f64;
println!();
println!("=== Moonshine-tiny profile ===");
println!("input: {} ({audio_secs:.2} s of audio)", cli.input.display());
println!("steady-state runs: {}", per_call_ms.len());
println!();
println!("per-call latency:");
println!(" mean = {mean:.0} ms");
println!(" p50 = {p50:.0} ms");
println!(" min = {:.0} ms", per_call_ms[0]);
println!(" max = {:.0} ms", per_call_ms[per_call_ms.len() - 1]);
println!();
println!("realtime factor: {realtime_factor:.4}x");
println!(" (mean / audio_duration; sub-1.0 = faster than realtime)");
println!();
println!("transcript ({last_token_count} tokens):");
println!(" {last_text}");
// For direct comparison with the other STT profiles in this crate:
println!();
println!("=== A/B against other STT backends in rtx-csm ===");
println!(" Kyutai STT 1B ~1.01x realtime (3 GB, hardware-bound)");
println!(" Whisper-tiny ~0.020x (in-process ggml, breaks CSM)");
println!(" Moonshine-tiny {realtime_factor:.4}x (pure candle, no conflict)");
Ok(())
}