Files
rustytorch/crates/models/rtx-csm/examples/moonshine_inspect.rs
T
osobhandClaude Opus 4.7 0699cdeb45 rtx-csm: Phase 8.5 — Moonshine conv stem (verified end-to-end)
First working piece of the Moonshine v2 candle port. New
src/moonshine.rs module with:

  - MoonshineConfig::tiny()  — hyperparameters from HF config.json
  - ConvStem (Conv1d × 3)    — audio stem, raw 16 kHz → 288-d hidden
  - load_conv_stem()         — VarBuilder from HF safetensors

Conv layout (verified against HF source):
  conv1: in=1,   out=288, k=127, stride=64, no bias
  conv2: in=288, out=576, k=7,   stride=3,  bias
  conv3: in=576, out=288, k=3,   stride=2,  bias
  Activations: tanh after conv1, gelu_erf after conv2 / conv3

Smoke test (`examples/moonshine_smoke`):
  - Downloads UsefulSensors/moonshine-tiny from HF
  - Synthetic 10 s @ 16 kHz audio (silence + sine pulse)
  - input (1, 1, 160000) -> output (1, 415, 288)
  - Expected T_seq=415 ((160000-127)/64+1 -> 2498 -> 831 -> 415)
  - Output max abs = 23.17 (real signal, weights loaded correctly)

Also extends `examples/moonshine_inspect` to dump conv shapes
explicitly (was being truncated by the per-prefix `take(8)` cap).

Next ship: encoder transformer block (partial RoPE, GELU MLP) and
output layer norm. Tracked in Phase 8 plan; ~2-3 hours of work.

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

97 lines
3.1 KiB
Rust

//! Phase 8.4 spike — download Moonshine-tiny from HuggingFace and dump
//! the safetensors layout. Used to design the candle module shape.
//!
//! Architecture (from HF config.json):
//! encoder-decoder transformer, hidden_size=288, intermediate=1152,
//! 6 enc + 6 dec layers, 8 heads (head_dim=36 + pad to 40),
//! partial_rotary_factor=0.9, vocab=32768, max_pos=194, audio input
//! is raw waveform (no mel-spec preprocessing).
//!
//! Usage:
//! ```bash
//! cargo run -p rtx-csm --release --example moonshine_inspect
//! ```
use anyhow::{Context, Result};
use hf_hub::api::sync::Api;
fn main() -> Result<()> {
let api = Api::new().context("hf_hub init")?;
let repo = api.model("UsefulSensors/moonshine-tiny".to_string());
let weights_path = repo
.get("model.safetensors")
.context("download model.safetensors")?;
println!("downloaded: {}", weights_path.display());
let bytes = std::fs::read(&weights_path)?;
println!(
"file size: {} bytes ({:.1} MB)",
bytes.len(),
bytes.len() as f64 / 1_000_000.0
);
// Parse the safetensors header to enumerate tensor names + shapes.
let st = safetensors::SafeTensors::deserialize(&bytes)
.context("parse safetensors header")?;
let names = st.names();
println!("\n=== {} tensors ===", names.len());
// Group by prefix so the layered structure is visible.
let mut grouped: std::collections::BTreeMap<String, Vec<(String, Vec<usize>, &str)>> =
Default::default();
for name in names {
let tensor = st.tensor(name)?;
let prefix = name
.split('.')
.take(2)
.collect::<Vec<_>>()
.join(".");
let dtype = match tensor.dtype() {
safetensors::Dtype::F32 => "f32",
safetensors::Dtype::F16 => "f16",
safetensors::Dtype::BF16 => "bf16",
other => match other {
_ => "?",
},
};
grouped
.entry(prefix)
.or_default()
.push((name.to_string(), tensor.shape().to_vec(), dtype));
}
for (prefix, items) in grouped.iter() {
println!("\n[{prefix}] ({} tensors)", items.len());
for (name, shape, dtype) in items.iter().take(8) {
println!(" {name:<60} {dtype} {shape:?}");
}
if items.len() > 8 {
println!(" ... {} more", items.len() - 8);
}
// Special: dump everything matching `conv` since we need
// exact strides for the audio stem.
for (name, shape, dtype) in items.iter().filter(|(n, _, _)| n.contains("conv")) {
if !items.iter().take(8).any(|(n2, _, _)| n2 == name) {
println!(" {name:<60} {dtype} {shape:?} (conv detail)");
}
}
}
let total_params: usize = st
.names()
.iter()
.map(|n| {
st.tensor(n)
.map(|t| t.shape().iter().product::<usize>())
.unwrap_or(0)
})
.sum();
println!(
"\ntotal parameters: {} ({:.1} M)",
total_params,
total_params as f64 / 1_000_000.0
);
Ok(())
}