1use crate::audio::{MelSpectrogram, load_wav_mono_f32, pcm_to_mel};
19use crate::config::VoxtralConfig;
20use crate::embed::{
21 argmax_token, fuse_inputs_embeds, transcription_prompt_ids, validate_prompt_audio_match,
22};
23use crate::encoder::build_voxtral_encoder_built;
24use crate::lm_flow::{build_voxtral_decode_built, build_voxtral_prefill_built};
25use crate::load::{VoxtralWeightStore, resolve_model_dir};
26use crate::projector::build_voxtral_projector_built;
27use crate::weights::VoxtralWeightPrefix;
28use anyhow::{Context, Result, ensure};
29use rlx_core::compact_bucketed_kv_buffer;
30use rlx_core::flow_util::compile_built;
31use rlx_core::validate_standard_device;
32use rlx_runtime::Device;
33use rlx_runtime::attn_mask::bucket_decode_mask;
34use std::path::{Path, PathBuf};
35
36#[derive(Debug, Clone, Default)]
37pub struct VoxtralRunnerBuilder {
38 weights: Option<PathBuf>,
39 config_path: Option<PathBuf>,
40 config: Option<VoxtralConfig>,
41 device: Option<Device>,
42 max_new_tokens: usize,
43}
44
45impl VoxtralRunnerBuilder {
46 pub fn weights(mut self, path: impl Into<PathBuf>) -> Self {
47 self.weights = Some(path.into());
48 self
49 }
50
51 pub fn config_path(mut self, path: impl Into<PathBuf>) -> Self {
52 self.config_path = Some(path.into());
53 self
54 }
55
56 pub fn config(mut self, cfg: VoxtralConfig) -> Self {
57 self.config = Some(cfg);
58 self
59 }
60
61 pub fn device(mut self, d: Device) -> Self {
62 self.device = Some(d);
63 self
64 }
65
66 pub fn max_new_tokens(mut self, n: usize) -> Self {
67 self.max_new_tokens = n;
68 self
69 }
70
71 pub fn build(self) -> Result<VoxtralRunner> {
72 let weights_path = self
73 .weights
74 .ok_or_else(|| anyhow::anyhow!("weights path required"))?;
75 let model_dir = resolve_model_dir(&weights_path)?;
76 let cfg_path = self
77 .config_path
78 .clone()
79 .unwrap_or_else(|| model_dir.join("config.json"));
80 let cfg = match self.config {
81 Some(c) => c,
82 None => VoxtralConfig::from_file(&cfg_path)
83 .with_context(|| format!("reading {cfg_path:?}"))?,
84 };
85 cfg.validate()?;
86 let device = self.device.unwrap_or(Device::Cpu);
87 validate_standard_device("voxtral", device)?;
88 let max_new_tokens = if self.max_new_tokens == 0 {
89 256
90 } else {
91 self.max_new_tokens
92 };
93 let weight_store = VoxtralWeightStore::open(&weights_path)?;
94
95 Ok(VoxtralRunner {
96 cfg,
97 device,
98 max_new_tokens,
99 weight_store,
100 })
101 }
102}
103
104pub struct VoxtralRunner {
105 cfg: VoxtralConfig,
106 device: Device,
107 max_new_tokens: usize,
108 weight_store: VoxtralWeightStore,
109}
110
111impl VoxtralRunner {
112 pub fn builder() -> VoxtralRunnerBuilder {
113 VoxtralRunnerBuilder::default()
114 }
115
116 pub fn config(&self) -> &VoxtralConfig {
117 &self.cfg
118 }
119
120 pub fn model_dir(&self) -> &Path {
121 self.weight_store.model_dir()
122 }
123
124 pub fn encode_audio(&self, mel: &MelSpectrogram) -> Result<Vec<f32>> {
125 let batch = 1;
126 let mel_frames = mel.n_frames;
127 let enc_seq = self.cfg.audio_config.encoder_seq_len(mel_frames);
128 ensure!(
129 enc_seq.is_multiple_of(4),
130 "encoder seq {enc_seq} not divisible by 4 — pad mel to a compatible length"
131 );
132
133 let mut wm = self.weight_store.load_audio_weights()?;
134 let enc_built =
135 build_voxtral_encoder_built(&self.cfg.audio_config, &mut wm, batch, mel_frames)?;
136 let enc_params = enc_built.params().clone();
137 let mut enc = compile_built(enc_built, self.device)?;
138 for (n, d) in &enc_params {
139 enc.set_param(n, d);
140 }
141 let enc_out = enc
142 .run(&[("mel", mel.data.as_slice())])
143 .into_iter()
144 .next()
145 .context("encoder output")?;
146 drop(wm);
147
148 let mut wm2 = self.weight_store.load_projector_weights()?;
149 let proj_built = build_voxtral_projector_built(&self.cfg, &mut wm2, batch, enc_seq)?;
150 let proj_params = proj_built.params().clone();
151 let mut proj = compile_built(proj_built, self.device)?;
152 for (n, d) in &proj_params {
153 proj.set_param(n, d);
154 }
155 let audio_embeds = proj
156 .run(&[("encoder_hidden", &enc_out)])
157 .into_iter()
158 .next()
159 .context("projector output")?;
160 Ok(audio_embeds)
161 }
162
163 pub fn generate(&self, prompt_ids: &[u32], mel: &MelSpectrogram) -> Result<Vec<u32>> {
164 let batch = 1;
165 let dbg = std::env::var("RLX_VOXTRAL_DEBUG").is_ok();
166 let ck = |label: &str, v: &[f32]| {
167 if dbg {
168 let sum: f64 = v.iter().map(|&x| x as f64).sum();
169 let sumsq: f64 = v.iter().map(|&x| (x as f64) * (x as f64)).sum();
170 let finite = v.iter().filter(|x| x.is_finite()).count();
171 eprintln!(
172 "[dbg] {label}: n={} sum={sum:.5} sumsq={sumsq:.5} finite={finite} first={:?}",
173 v.len(),
174 &v[..v.len().min(4)]
175 );
176 }
177 };
178 let audio_embeds = self.encode_audio(mel)?;
179 let h = self.cfg.text_config.hidden_size;
180 let n_audio = audio_embeds.len() / h;
181 ck("audio_embeds", &audio_embeds);
182 validate_prompt_audio_match(&self.cfg, prompt_ids, n_audio)?;
183
184 let embed_wm = self
185 .weight_store
186 .load_keys(&[VoxtralWeightPrefix::lm_embed_tokens()])?;
187 let inputs_embeds = fuse_inputs_embeds(&self.cfg, &embed_wm, prompt_ids, &audio_embeds)?;
188 drop(embed_wm);
189 let seq = prompt_ids.len();
190
191 let no_gather = std::env::var("RLX_VOXTRAL_NOGATHER").is_ok();
195 let last_logits_only = !no_gather;
196 let with_kv = std::env::var("RLX_VOXTRAL_NOKV").is_err();
200 let mut wm = self.weight_store.load_language_model_weights()?;
201 let prefill_built =
202 build_voxtral_prefill_built(&self.cfg, &mut wm, batch, seq, with_kv, last_logits_only)?;
203 drop(wm);
209 let mut prefill = compile_built(prefill_built, self.device)?;
210
211 ck("inputs_embeds", &inputs_embeds);
212 let pre_in = [("inputs_embeds", inputs_embeds.as_slice())];
213 let outs = prefill.run(&pre_in);
214 let logits = &outs[0];
215 if no_gather {
216 ck("prefill_all_logits", logits);
217 anyhow::bail!("RLX_VOXTRAL_NOGATHER: stopped after full-sequence prefill checksum");
218 }
219 ck("prefill_logits", logits);
220 let vocab = self.cfg.text_config.vocab_size;
221 let mut tokens: Vec<u32> = prompt_ids.to_vec();
222 ensure!(
223 logits.len() == batch * vocab,
224 "expected last-token logits [{batch}, {vocab}], got {}",
225 logits.len()
226 );
227 let mut next = argmax_token(logits);
228
229 let kv_start = 1usize;
230 let mut kv_caches: Vec<Vec<f32>> = outs[kv_start..].to_vec();
231 drop(prefill);
232
233 let layers = self.cfg.text_config.num_hidden_layers;
234 let key_past: Vec<String> = (0..layers)
235 .flat_map(|i| [format!("past_k_{i}"), format!("past_v_{i}")])
236 .collect();
237 let kv_dim = self.cfg.llama_config().kv_proj_dim();
238
239 let upper = seq.saturating_add(self.max_new_tokens);
245 let mut wm_dec = self.weight_store.load_language_model_weights()?;
246 let dec_built =
247 build_voxtral_decode_built(&self.cfg, &mut wm_dec, batch, upper, false, true)?;
248 drop(wm_dec);
251 let mut dec = compile_built(dec_built, self.device)?;
252
253 let mut padded_k = vec![vec![0f32; upper * kv_dim]; layers];
254 let mut padded_v = vec![vec![0f32; upper * kv_dim]; layers];
255
256 let eos = self.cfg.eos_token_id;
257 for past_len in seq..upper {
258 if next == 0 || next == eos {
261 break;
262 }
263 tokens.push(next);
264
265 let token_f = [next as f32];
266 let position = [past_len as f32];
267 let mask = bucket_decode_mask(past_len, upper);
268
269 for i in 0..layers {
271 let kf = &kv_caches[2 * i];
272 let vf = &kv_caches[2 * i + 1];
273 padded_k[i][..kf.len()].copy_from_slice(kf);
274 padded_v[i][..vf.len()].copy_from_slice(vf);
275 }
276
277 let mut dec_in: Vec<(&str, &[f32])> = vec![
278 ("input_ids", &token_f),
279 ("position", &position),
280 ("mask", mask.as_slice()),
281 ];
282 for i in 0..layers {
283 dec_in.push((key_past[2 * i].as_str(), padded_k[i].as_slice()));
284 dec_in.push((key_past[2 * i + 1].as_str(), padded_v[i].as_slice()));
285 }
286 let step_out = dec.run(&dec_in);
287 next = argmax_token(&step_out[0]);
288
289 let new_len = past_len + 1;
292 kv_caches = step_out[1..]
293 .iter()
294 .map(|buf| compact_bucketed_kv_buffer(buf, new_len, kv_dim, batch))
295 .collect();
296 }
297
298 Ok(tokens)
299 }
300
301 pub fn transcribe_wav(&self, wav: &Path, language: Option<&str>) -> Result<Vec<u32>> {
304 let pcm = load_wav_mono_f32(wav)?;
305 let mel = pcm_to_mel(&self.cfg.audio_config, &pcm)?;
306 let n_audio = self.cfg.audio_config.audio_token_count(mel.n_frames);
307 let prompt =
308 transcription_prompt_ids(&self.cfg, n_audio, language, Some(self.model_dir()))?;
309 self.generate(&prompt, &mel)
310 }
311}