1use crate::fcd_ops::gemm_nt;
25use crate::nemotron_h::{NemotronH, SendPtr, dot, par_rows, tensor_f32};
26use crate::pool::Pool;
27use crate::tokenizer::Tokenizer;
28use cortiq_core::CmfModel;
29use rustfft::{FftPlanner, num_complex::Complex};
30use std::path::Path;
31use std::sync::Arc;
32use std::time::Instant;
33
34pub mod words;
35
36pub const ARCH_NAME: &str = "nemotron3_5_transcribe";
37
38const SR: usize = 16_000;
39const HOP: usize = 160;
40const NFFT: usize = 512;
41const LOG_GUARD: f32 = 5.960_464_5e-8; struct Ln {
44 w: Vec<f32>,
45 b: Vec<f32>,
46}
47
48impl Ln {
49 fn load(m: &CmfModel, p: &str) -> Result<Self, String> {
50 Ok(Self {
51 w: tensor_f32(m, &format!("{p}.weight"))?,
52 b: tensor_f32(m, &format!("{p}.bias"))?,
53 })
54 }
55 fn apply(&self, x: &mut [f32]) {
56 let n = x.len();
57 let mean = x.iter().sum::<f32>() / n as f32;
58 let var = x.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / n as f32;
59 let inv = 1.0 / (var + 1e-5).sqrt();
60 for i in 0..n {
61 x[i] = (x[i] - mean) * inv * self.w[i] + self.b[i];
62 }
63 }
64 fn apply_rows(&self, x: &mut [f32]) {
65 for r in x.chunks_exact_mut(self.w.len()) {
66 self.apply(r);
67 }
68 }
69}
70
71struct Lin {
73 w: Vec<f32>,
74 b: Option<Vec<f32>>,
75 out: usize,
76 inp: usize,
77}
78
79impl Lin {
80 fn load(m: &CmfModel, p: &str, bias: bool) -> Result<Self, String> {
81 let name = format!("{p}.weight");
82 let e = m
83 .tensor(&name)
84 .ok_or_else(|| format!("missing tensor {name}"))?;
85 if e.shape.len() != 2 {
86 return Err(format!("{name}: expected 2-D, got {:?}", e.shape));
87 }
88 Ok(Self {
89 w: tensor_f32(m, &name)?,
90 b: if bias {
91 Some(tensor_f32(m, &format!("{p}.bias"))?)
92 } else {
93 None
94 },
95 out: e.shape[0],
96 inp: e.shape[1],
97 })
98 }
99 fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
100 let mut y = vec![0.0f32; n * self.out];
101 gemm_nt(x, &self.w, &mut y, n, self.inp, self.out, pool);
102 if let Some(b) = &self.b {
103 for r in y.chunks_exact_mut(self.out) {
104 for (v, bb) in r.iter_mut().zip(b) {
105 *v += bb;
106 }
107 }
108 }
109 y
110 }
111}
112
113struct EncLayer {
114 n1: Ln,
115 qkv: Lin,
116 qn: Option<Ln>,
117 kn: Option<Ln>,
118 out: Lin,
119 n2: Ln,
120 fc1: Lin,
121 fc2: Lin,
122}
123
124struct Encoder {
127 proj: Lin,
128 embed_norm: Ln,
129 layers: Vec<EncLayer>,
130 final_norm: Ln,
131 d: usize,
132 heads: usize,
133 hd: usize,
134 rot: usize,
135 stack: usize,
136 #[cfg(feature = "gpu")]
138 dev: Option<crate::gpu_wgpu::nemotron_audio::AudioEncoder>,
139}
140
141impl Encoder {
142 #[cfg(feature = "gpu")]
143 fn build_dev(
144 &self,
145 m: &CmfModel,
146 p: &str,
147 ) -> Option<crate::gpu_wgpu::nemotron_audio::AudioEncoder> {
148 use crate::gpu_wgpu::nemotron_audio::{AeDesc, AeLayerDesc, AudioEncoder};
149 if std::env::var("CMF_NEMO_AUDIO_GPU").as_deref() == Ok("0") || !crate::gpu::wgpu_active() {
150 return None;
151 }
152 let bf16 = |name: String| -> Option<&[u8]> {
153 let e = m.tensor(&name)?;
154 (e.dtype == cortiq_core::TensorDtype::Bf16).then(|| m.entry_bytes(e))
155 };
156 let wb = |ln: &Ln| -> Vec<f32> { ln.w.iter().chain(&ln.b).copied().collect() };
157 let mut layers = Vec::with_capacity(self.layers.len());
158 for (i, l) in self.layers.iter().enumerate() {
159 let q = format!("{p}.layers.{i}");
160 layers.push(AeLayerDesc {
161 ln1: wb(&l.n1),
162 qkv: bf16(format!("{q}.attn.w_qkv.weight"))?,
163 qn: l.qn.as_ref().map(wb),
164 kn: l.kn.as_ref().map(wb),
165 out: bf16(format!("{q}.attn.out_proj.weight"))?,
166 out_b: l.out.b.clone()?,
167 ln2: wb(&l.n2),
168 fc1: bf16(format!("{q}.ffn.net.0.weight"))?,
169 fc1_b: l.fc1.b.clone()?,
170 fc2: bf16(format!("{q}.ffn.net.3.weight"))?,
171 fc2_b: l.fc2.b.clone()?,
172 });
173 }
174 AudioEncoder::build(&AeDesc {
175 d: self.d,
176 heads: self.heads,
177 rot: self.rot,
178 inter: self.layers[0].fc1.out,
179 stack_in: self.proj.inp,
180 proj: bf16(format!("{p}.pre_encode.proj.weight"))?,
181 embed_norm: wb(&self.embed_norm),
182 final_norm: wb(&self.final_norm),
183 layers,
184 })
185 }
186
187 fn load(
188 m: &CmfModel,
189 p: &str,
190 heads: usize,
191 rot_frac: f64,
192 stack: usize,
193 ) -> Result<Self, String> {
194 let proj = Lin::load(m, &format!("{p}.pre_encode.proj"), false)?;
195 let d = proj.out;
196 let hd = d / heads;
197 let mut layers = Vec::new();
198 for i in 0.. {
199 let l = format!("{p}.layers.{i}");
200 if m.tensor(&format!("{l}.norm1.weight")).is_none() {
201 break;
202 }
203 let qk = m.tensor(&format!("{l}.attn.q_norm.weight")).is_some();
204 layers.push(EncLayer {
205 n1: Ln::load(m, &format!("{l}.norm1"))?,
206 qkv: Lin::load(m, &format!("{l}.attn.w_qkv"), false)?,
207 qn: if qk {
208 Some(Ln::load(m, &format!("{l}.attn.q_norm"))?)
209 } else {
210 None
211 },
212 kn: if qk {
213 Some(Ln::load(m, &format!("{l}.attn.k_norm"))?)
214 } else {
215 None
216 },
217 out: Lin::load(m, &format!("{l}.attn.out_proj"), true)?,
218 n2: Ln::load(m, &format!("{l}.norm2"))?,
219 fc1: Lin::load(m, &format!("{l}.ffn.net.0"), true)?,
220 fc2: Lin::load(m, &format!("{l}.ffn.net.3"), true)?,
221 });
222 }
223 if layers.is_empty() {
224 return Err(format!("{p}: no encoder layers"));
225 }
226 #[allow(unused_mut)]
227 let mut enc = Self {
228 embed_norm: Ln::load(m, &format!("{p}.embed_norm"))?,
229 final_norm: Ln::load(m, &format!("{p}.final_norm"))?,
230 proj,
231 layers,
232 d,
233 heads,
234 hd,
235 rot: (hd as f64 * rot_frac) as usize,
236 stack,
237 #[cfg(feature = "gpu")]
238 dev: None,
239 };
240 #[cfg(feature = "gpu")]
241 {
242 enc.dev = enc.build_dev(m, p);
243 if enc.dev.is_some() && std::env::var("CMF_NEMO_KEEP_HOST").as_deref() != Ok("1") {
248 for l in &mut enc.layers {
249 for lin in [&mut l.qkv, &mut l.out, &mut l.fc1, &mut l.fc2] {
250 lin.w = Vec::new();
251 }
252 }
253 }
254 }
255 Ok(enc)
256 }
257
258 fn load_tower(m: &CmfModel, p: &str, heads: usize, rot_frac: f64) -> Result<Self, String> {
261 let embed_norm = Ln::load(m, &format!("{p}.embed_norm"))?;
262 let d = embed_norm.w.len();
263 let hd = d / heads;
264 let mut layers = Vec::new();
265 for i in 0.. {
266 let l = format!("{p}.layers.{i}");
267 if m.tensor(&format!("{l}.norm1.weight")).is_none() {
268 break;
269 }
270 let qk = m.tensor(&format!("{l}.attn.q_norm.weight")).is_some();
271 layers.push(EncLayer {
272 n1: Ln::load(m, &format!("{l}.norm1"))?,
273 qkv: Lin::load(m, &format!("{l}.attn.w_qkv"), false)?,
274 qn: if qk { Some(Ln::load(m, &format!("{l}.attn.q_norm"))?) } else { None },
275 kn: if qk { Some(Ln::load(m, &format!("{l}.attn.k_norm"))?) } else { None },
276 out: Lin::load(m, &format!("{l}.attn.out_proj"), true)?,
277 n2: Ln::load(m, &format!("{l}.norm2"))?,
278 fc1: Lin::load(m, &format!("{l}.ffn.net.0"), true)?,
279 fc2: Lin::load(m, &format!("{l}.ffn.net.3"), true)?,
280 });
281 }
282 if layers.is_empty() {
283 return Err(format!("{p}: no layers"));
284 }
285 Ok(Self {
286 proj: Lin {
287 w: Vec::new(),
288 b: None,
289 out: d,
290 inp: 0,
291 },
292 embed_norm,
293 final_norm: Ln::load(m, &format!("{p}.final_norm"))?,
294 layers,
295 d,
296 heads,
297 hd,
298 rot: (hd as f64 * rot_frac) as usize,
299 stack: 1,
300 #[cfg(feature = "gpu")]
301 dev: None,
302 })
303 }
304
305 fn encode_frames(
307 &self,
308 feats: &[f32],
309 n: usize,
310 valid: &[bool],
311 pool: Option<&Pool>,
312 ) -> Vec<f32> {
313 #[cfg(feature = "gpu")]
314 if let Some(dev) = &self.dev
315 && let Some(h) = dev.run(feats, n, false, valid)
316 {
317 return h;
318 }
319 assert!(
320 !self.layers[0].qkv.w.is_empty(),
321 "audio encoder: the device run failed and the host weights were released (CMF_NEMO_KEEP_HOST=1 keeps them)"
322 );
323 let x = self.embed(feats, n * self.stack, pool);
324 self.run(&x, n, valid, pool)
325 }
326
327 fn encode_rows(&self, x: &[f32], n: usize, valid: &[bool], pool: Option<&Pool>) -> Vec<f32> {
329 #[cfg(feature = "gpu")]
330 if let Some(dev) = &self.dev
331 && let Some(h) = dev.run(x, n, true, valid)
332 {
333 return h;
334 }
335 assert!(
336 !self.layers[0].qkv.w.is_empty(),
337 "audio encoder: the device run failed and the host weights were released (CMF_NEMO_KEEP_HOST=1 keeps them)"
338 );
339 self.run(x, n, valid, pool)
340 }
341
342 fn embed(&self, feats: &[f32], frames: usize, pool: Option<&Pool>) -> Vec<f32> {
344 debug_assert_eq!(frames % self.stack, 0);
345 self.proj.apply(feats, frames / self.stack, pool)
346 }
347
348 fn run(&self, x: &[f32], n: usize, valid: &[bool], pool: Option<&Pool>) -> Vec<f32> {
350 let d = self.d;
351 let mut h = x.to_vec();
352 self.embed_norm.apply_rows(&mut h);
353 let (cos, sin) = rope_table(n, self.rot);
354 for l in &self.layers {
355 let mut a = h.clone();
356 l.n1.apply_rows(&mut a);
357 let mut qkv = l.qkv.apply(&a, n, pool);
358 for t in 0..n {
359 let row = &mut qkv[t * 3 * d..(t + 1) * 3 * d];
360 for hh in 0..self.heads {
361 let q = &mut row[hh * self.hd..(hh + 1) * self.hd];
362 if let Some(qn) = &l.qn {
363 qn.apply(q);
364 }
365 rope(
366 q,
367 &cos[t * self.rot / 2..],
368 &sin[t * self.rot / 2..],
369 self.rot,
370 );
371 }
372 for hh in 0..self.heads {
373 let k = &mut row[d + hh * self.hd..d + (hh + 1) * self.hd];
374 if let Some(kn) = &l.kn {
375 kn.apply(k);
376 }
377 rope(
378 k,
379 &cos[t * self.rot / 2..],
380 &sin[t * self.rot / 2..],
381 self.rot,
382 );
383 }
384 }
385 let o = bidir_attention(&qkv, n, self.heads, self.hd, valid, pool);
386 let o = l.out.apply(&o, n, pool);
387 for (v, w) in h.iter_mut().zip(&o) {
388 *v += w;
389 }
390 let mut a = h.clone();
391 l.n2.apply_rows(&mut a);
392 let mut f = l.fc1.apply(&a, n, pool);
393 for v in f.iter_mut() {
394 *v = gelu_erf(*v);
395 }
396 let f = l.fc2.apply(&f, n, pool);
397 for (v, w) in h.iter_mut().zip(&f) {
398 *v += w;
399 }
400 }
401 self.final_norm.apply_rows(&mut h);
402 h
403 }
404}
405
406fn rope_table(n: usize, rot: usize) -> (Vec<f32>, Vec<f32>) {
408 let half = rot / 2;
409 let inv: Vec<f32> = (0..half)
410 .map(|i| 1.0 / 10000f32.powf((2 * i) as f32 / rot as f32))
411 .collect();
412 let mut cos = vec![0.0; n * half];
413 let mut sin = vec![0.0; n * half];
414 for t in 0..n {
415 for i in 0..half {
416 let a = t as f32 * inv[i];
417 cos[t * half + i] = a.cos();
418 sin[t * half + i] = a.sin();
419 }
420 }
421 (cos, sin)
422}
423
424fn rope(x: &mut [f32], cos: &[f32], sin: &[f32], rot: usize) {
426 let half = rot / 2;
427 for i in 0..half {
428 let (a, b) = (x[i], x[i + half]);
429 x[i] = a * cos[i] - b * sin[i];
430 x[i + half] = b * cos[i] + a * sin[i];
431 }
432}
433
434fn bidir_attention(
437 qkv: &[f32],
438 n: usize,
439 heads: usize,
440 hd: usize,
441 valid: &[bool],
442 pool: Option<&Pool>,
443) -> Vec<f32> {
444 let d = heads * hd;
445 let scale = 1.0 / (hd as f32).sqrt();
446 let mut o = vec![0.0f32; n * d];
447 let o_addr = SendPtr(o.as_mut_ptr());
448 let run = |r0: usize, r1: usize| {
449 let o = o_addr;
450 let mut sc = vec![0.0f32; n];
451 for r in r0..r1 {
452 let (hh, t) = (r / n, r % n);
453 let q = &qkv[t * 3 * d + hh * hd..t * 3 * d + (hh + 1) * hd];
454 let mut mx = f32::NEG_INFINITY;
455 for j in 0..n {
456 if !valid[j] {
457 sc[j] = f32::NEG_INFINITY;
458 continue;
459 }
460 let k = &qkv[j * 3 * d + d + hh * hd..j * 3 * d + d + (hh + 1) * hd];
461 let s = dot(q, k) * scale;
462 sc[j] = s;
463 mx = mx.max(s);
464 }
465 let mut den = 0.0f32;
466 for s in sc.iter_mut() {
467 *s = if s.is_finite() { (*s - mx).exp() } else { 0.0 };
468 den += *s;
469 }
470 let out = unsafe { std::slice::from_raw_parts_mut(o.0.add(t * d + hh * hd), hd) };
471 out.fill(0.0);
472 for j in 0..n {
473 if sc[j] == 0.0 {
474 continue;
475 }
476 let p = sc[j] / den;
477 let v = &qkv[j * 3 * d + 2 * d + hh * hd..j * 3 * d + 2 * d + (hh + 1) * hd];
478 for (a, &b) in out.iter_mut().zip(v) {
479 *a += p * b;
480 }
481 }
482 }
483 };
484 par_rows(pool, heads * n, &run);
485 o
486}
487
488fn gelu_erf(x: f32) -> f32 {
489 0.5 * x * (1.0 + crate::inference::erf_f32(x * std::f32::consts::FRAC_1_SQRT_2))
490}
491
492fn sigmoid(x: f32) -> f32 {
493 1.0 / (1.0 + (-x).exp())
494}
495
496#[derive(Clone, Debug)]
498struct SpkCfg {
499 chunk: usize,
500 left: usize,
501 right: usize,
502 fifo_len: usize,
503 update_period: usize,
504 cache_len: usize,
505 n_spk: usize,
506 sil_frames: usize,
507 pred_thr: f32,
508 latest_boost: f32,
509 min_pos: usize,
510 n_strong: usize,
511 n_weak: usize,
512 activity_thr: f32,
513}
514
515impl SpkCfg {
516 fn from_config(audio: &serde_json::Value) -> Self {
517 let dz = &audio["diarization_config"];
518 let sc = &dz["streaming_config"];
519 let u = |v: &serde_json::Value, k: &str, d: u64| {
520 v.get(k).and_then(|x| x.as_u64()).unwrap_or(d) as usize
521 };
522 let f =
523 |v: &serde_json::Value, k: &str, d: f64| v.get(k).and_then(|x| x.as_f64()).unwrap_or(d);
524 let n_spk = u(sc, "num_speakers", 8);
525 let cache_len = u(sc, "speaker_cache_length", 188);
526 let sil = u(sc, "speaker_cache_silence_frames_per_speaker", 1);
527 let budget = (cache_len / n_spk - sil) as f64;
528 Self {
529 chunk: u(dz, "chunk_length", 500),
530 left: u(dz, "chunk_left_context", 50),
531 right: u(dz, "chunk_right_context", 50),
532 fifo_len: u(dz, "fifo_length", 40),
533 update_period: u(dz, "speaker_cache_update_period", 300),
534 cache_len,
535 n_spk,
536 sil_frames: sil,
537 pred_thr: f(sc, "prediction_score_threshold", 0.25) as f32,
538 latest_boost: f(sc, "latest_frames_score_boost", 0.05) as f32,
539 min_pos: (budget * f(sc, "min_positive_scores_rate", 0.5)).floor() as usize,
540 n_strong: (budget * f(sc, "strong_boost_rate", 0.75)).floor() as usize,
541 n_weak: (budget * f(sc, "weak_boost_rate", 1.5)).floor() as usize,
542 activity_thr: audio
543 .get("speaker_activity_threshold")
544 .and_then(|x| x.as_f64())
545 .unwrap_or(0.5) as f32,
546 }
547 }
548}
549
550struct SpkCache {
553 embeds: Vec<f32>,
554 probs: Vec<f32>,
555 fifo: Vec<f32>,
556 compressed: bool,
557}
558
559impl SpkCache {
560 fn n_cache(&self, d: usize) -> usize {
561 self.embeds.len() / d
562 }
563 fn n_fifo(&self, d: usize) -> usize {
564 self.fifo.len() / d
565 }
566
567 #[allow(clippy::too_many_arguments)]
568 fn update(
569 &mut self,
570 c: &SpkCfg,
571 d: usize,
572 input: &[f32],
573 logits: &[f32],
574 mask: &[bool],
575 silence: &[f32],
576 n_chunk: usize,
577 lookback: usize,
578 ) {
579 let ns = c.n_spk;
580 let n_in = input.len() / d;
581 let sub = logits.len() / (n_in * ns);
583 let mut probs = vec![0.0f32; n_in * ns];
584 for t in 0..n_in {
585 if !mask[t] {
586 continue;
587 }
588 for s in 0..ns {
589 let mut acc = 0.0f32;
590 for j in 0..sub {
591 acc += sigmoid(logits[(t * sub + j) * ns + s]);
592 }
593 probs[t * ns + s] = acc / sub as f32;
594 }
595 }
596 let (nc, nf) = (self.n_cache(d), self.n_fifo(d));
597 let start = nc + nf + lookback;
598 let mut fifo = self.fifo.clone();
599 fifo.extend_from_slice(&input[start * d..(start + n_chunk) * d]);
600 let len = fifo.len() / d;
601 let popped = if len <= c.fifo_len {
602 0
603 } else {
604 c.update_period.max(len - c.fifo_len).min(len)
605 };
606 if popped > 0 {
607 let mut fifo_probs = probs[nc * ns..(nc + nf) * ns].to_vec();
608 fifo_probs.extend_from_slice(&probs[start * ns..(start + n_chunk) * ns]);
609 let mut cache_probs = if self.compressed {
610 self.probs.clone()
611 } else {
612 probs[..nc * ns].to_vec()
613 };
614 let mut cache_embeds = self.embeds.clone();
615 cache_embeds.extend_from_slice(&fifo[..popped * d]);
616 cache_probs.extend_from_slice(&fifo_probs[..popped * ns]);
617 fifo.drain(..popped * d);
618 if cache_embeds.len() / d > c.cache_len {
619 let (e, p) = compress(c, d, &cache_embeds, &cache_probs, silence);
620 cache_embeds = e;
621 cache_probs = p;
622 self.compressed = true;
623 }
624 self.embeds = cache_embeds;
625 self.probs = cache_probs;
626 }
627 self.fifo = fifo;
628 }
629}
630
631fn frame_scores(c: &SpkCfg, probs: &[f32], n: usize) -> Vec<f32> {
635 let ns = c.n_spk;
636 let thr = c.pred_thr;
637 let mut s = vec![0.0f32; n * ns];
638 for t in 0..n {
639 let p = &probs[t * ns..(t + 1) * ns];
640 let lc: Vec<f32> = p.iter().map(|&x| (1.0 - x).max(thr).ln()).collect();
641 let lcs: f32 = lc.iter().sum();
642 for k in 0..ns {
643 let v = p[k].max(thr).ln() - lc[k] + lcs - 0.5f32.ln();
644 s[t * ns + k] = if p[k] > 0.5 { v } else { f32::NEG_INFINITY };
645 }
646 }
647 for k in 0..ns {
648 let positive = (0..n).filter(|&t| s[t * ns + k] > 0.0).count();
649 if positive >= c.min_pos {
650 for t in 0..n {
651 let v = s[t * ns + k];
652 if probs[t * ns + k] > 0.5 && v <= 0.0 {
653 s[t * ns + k] = f32::NEG_INFINITY;
654 }
655 }
656 }
657 }
658 s
659}
660
661fn topk_idx(v: &[f32], k: usize) -> Vec<usize> {
663 let mut idx: Vec<usize> = (0..v.len()).collect();
664 idx.sort_by(|&a, &b| {
665 v[b].partial_cmp(&v[a])
666 .unwrap_or(std::cmp::Ordering::Equal)
667 .then(a.cmp(&b))
668 });
669 idx.truncate(k);
670 idx
671}
672
673fn compress(
676 c: &SpkCfg,
677 d: usize,
678 embeds: &[f32],
679 probs: &[f32],
680 silence: &[f32],
681) -> (Vec<f32>, Vec<f32>) {
682 let ns = c.n_spk;
683 let n = embeds.len() / d;
684 let mut s = frame_scores(c, probs, n);
685 for t in c.cache_len..n {
686 for k in 0..ns {
687 s[t * ns + k] += c.latest_boost;
688 }
689 }
690 for (count, boost) in [(c.n_strong, -2.0 * 0.5f32.ln()), (c.n_weak, -(0.5f32.ln()))] {
691 for k in 0..ns {
692 let col: Vec<f32> = (0..n).map(|t| s[t * ns + k]).collect();
693 for t in topk_idx(&col, count) {
694 s[t * ns + k] += boost;
695 }
696 }
697 }
698 let scored = n + c.sil_frames;
699 let mut flat = vec![f32::INFINITY; ns * scored];
701 for k in 0..ns {
702 for t in 0..n {
703 flat[k * scored + t] = s[t * ns + k];
704 }
705 }
706 let sentinel = scored * ns;
707 let mut picked: Vec<usize> = topk_idx(&flat, c.cache_len)
708 .into_iter()
709 .map(|i| {
710 if flat[i] == f32::NEG_INFINITY {
711 sentinel
712 } else {
713 i
714 }
715 })
716 .collect();
717 picked.sort_unstable();
718 let mut out_e = Vec::with_capacity(c.cache_len * d);
719 let mut out_p = Vec::with_capacity(c.cache_len * ns);
720 for i in picked {
721 let f = if i == sentinel {
722 n
723 } else {
724 (i % scored).min(n)
725 };
726 if f < n {
727 out_e.extend_from_slice(&embeds[f * d..(f + 1) * d]);
728 out_p.extend_from_slice(&probs[f * ns..(f + 1) * ns]);
729 } else {
730 out_e.extend_from_slice(silence);
731 out_p.extend(std::iter::repeat_n(0.0, ns));
732 }
733 }
734 (out_e, out_p)
735}
736
737pub struct Audio {
739 fb: Vec<f32>,
740 window: Vec<f32>,
741 n_mel: usize,
742 asr: Encoder,
743 diar: Encoder,
744 diar_proj: Lin,
745 up_w: Vec<f32>,
746 up_b: Vec<f32>,
747 cls_dense: Lin,
748 cls_out: Lin,
749 silence: Vec<f32>,
750 asr_norm: Ln,
751 spk_norm: Ln,
752 spk_table: Vec<f32>,
753 proj: Lin,
754 spk: SpkCfg,
755}
756
757pub struct AudioOut {
759 pub feats: Vec<f32>,
761 pub frames: usize,
762 pub valid_frames: usize,
763 pub asr_hidden: Vec<f32>,
765 pub speaker_logits: Vec<f32>,
767 pub embeds: Vec<f32>,
769 pub tokens: usize,
770}
771
772impl Audio {
773 pub fn load(m: &Arc<CmfModel>, audio_cfg: &serde_json::Value) -> Result<Self, String> {
774 let fb = tensor_f32(m, "perception.preprocessor.featurizer.fb")?;
775 let window = tensor_f32(m, "perception.preprocessor.featurizer.window")?;
776 let n_mel = fb.len() / (NFFT / 2 + 1);
777 let asr_cfg = &audio_cfg["asr_config"];
778 let dz = &audio_cfg["diarization_config"]["audio_config"];
779 let heads = |v: &serde_json::Value, d| {
780 v.get("num_attention_heads")
781 .and_then(|x| x.as_u64())
782 .unwrap_or(d) as usize
783 };
784 let rot = |v: &serde_json::Value, d| {
785 v.get("rope_parameters")
786 .and_then(|r| r.get("partial_rotary_factor"))
787 .and_then(|x| x.as_f64())
788 .unwrap_or(d)
789 };
790 let stack = asr_cfg
791 .get("subsampling_factor")
792 .and_then(|x| x.as_u64())
793 .unwrap_or(8) as usize;
794 let asr = Encoder::load(
795 m,
796 "perception.encoder.asr_encoder",
797 heads(asr_cfg, 10),
798 rot(asr_cfg, 0.5),
799 stack,
800 )?;
801 let diar = Encoder::load(
802 m,
803 "perception.encoder.diarization_model.encoder",
804 heads(dz, 8),
805 rot(dz, 1.0),
806 stack,
807 )?;
808 let sm = "perception.encoder.diarization_model.sortformer_modules";
809 let spk = SpkCfg::from_config(audio_cfg);
810 let d = asr.d;
811 let mut spk_table = vec![0.0f32; spk.n_spk * d];
813 for s in 0..spk.n_spk {
814 for i in 0..d / 2 {
815 let inv = ((2 * i) as f32 * -(10000f32.ln() / d as f32)).exp();
816 spk_table[s * d + 2 * i] = (s as f32 * inv).sin();
817 spk_table[s * d + 2 * i + 1] = (s as f32 * inv).cos();
818 }
819 }
820 Ok(Self {
821 fb,
822 window,
823 n_mel,
824 asr,
825 diar,
826 diar_proj: Lin::load(m, &format!("{sm}.encoder_proj"), true)?,
827 up_w: {
829 let name = format!("{sm}.subpixel_upsample.weight");
830 let w = tensor_f32(m, &name)?;
831 let shape = &m.tensor(&name).ok_or("upsampler")?.shape;
832 if shape.len() != 3 || shape[2] != 3 {
833 return Err(format!("{name}: shape {shape:?}, expected [cout, cin, 3]"));
834 }
835 let (cout, cin) = (shape[0], shape[1]);
836 let mut g = vec![0.0f32; w.len()];
837 for o in 0..cout {
838 for c in 0..cin {
839 for j in 0..3 {
840 g[o * 3 * cin + j * cin + c] = w[(o * cin + c) * 3 + j];
841 }
842 }
843 }
844 g
845 },
846 up_b: tensor_f32(m, &format!("{sm}.subpixel_upsample.bias"))?,
847 cls_dense: Lin::load(m, &format!("{sm}.first_hidden_to_hidden"), true)?,
848 cls_out: Lin::load(m, &format!("{sm}.single_hidden_to_spks"), true)?,
849 silence: tensor_f32(m, &format!("{sm}.learnable_sil_emb"))?,
850 asr_norm: Ln::load(m, "perception.encoder.asr_norm")?,
851 spk_norm: Ln::load(m, "perception.encoder.diar_norm")?,
852 spk_table,
853 proj: Lin::load(m, "perception.proj", true)?,
854 spk,
855 })
856 }
857
858 pub fn log_mel(&self, x: &[f32]) -> (Vec<f32>, usize, usize) {
861 let len = x.len();
862 let mut y = vec![0.0f32; len];
863 if len > 0 {
864 y[0] = x[0];
865 for i in 1..len {
866 y[i] = x[i] - 0.97 * x[i - 1];
867 }
868 }
869 let half = NFFT / 2;
870 let mut padded = vec![0.0f32; len + NFFT];
871 padded[half..half + len].copy_from_slice(&y);
872 let n_frames = 1 + len / HOP;
873 let valid = len / HOP;
874 let frames = n_frames.div_ceil(self.asr.stack) * self.asr.stack;
875 let wl = self.window.len();
876 let off = (NFFT - wl) / 2;
877 let bins = half + 1;
878 let mut feats = vec![0.0f32; frames * self.n_mel];
879 let fft = FftPlanner::<f64>::new().plan_fft_forward(NFFT);
880 let mut buf = vec![Complex::new(0.0f64, 0.0); NFFT];
881 let mut power = vec![0.0f32; bins];
882 for t in 0..valid {
883 for (i, c) in buf.iter_mut().enumerate() {
884 let w = if i >= off && i < off + wl {
885 self.window[i - off]
886 } else {
887 0.0
888 };
889 *c = Complex::new((padded[t * HOP + i] * w) as f64, 0.0);
890 }
891 fft.process(&mut buf);
892 for k in 0..bins {
893 let (re, im) = (buf[k].re as f32, buf[k].im as f32);
894 let mag = (re * re + im * im).sqrt();
895 power[k] = mag * mag;
896 }
897 let row = &mut feats[t * self.n_mel..(t + 1) * self.n_mel];
898 for (mi, v) in row.iter_mut().enumerate() {
899 let f = &self.fb[mi * bins..(mi + 1) * bins];
900 *v = (dot(f, &power) + LOG_GUARD).ln();
901 }
902 }
903 (feats, frames, valid)
904 }
905
906 fn normalize(&self, feats: &[f32], frames: usize, valid: usize) -> Vec<f32> {
909 let nm = self.n_mel;
910 let mut out = vec![0.0f32; frames * nm];
911 for m in 0..nm {
912 let mean = (0..valid).map(|t| feats[t * nm + m]).sum::<f32>() / valid.max(1) as f32;
913 let var = (0..valid)
914 .map(|t| (feats[t * nm + m] - mean).powi(2))
915 .sum::<f32>()
916 / (valid.max(2) - 1) as f32;
917 let std = var.sqrt() + 1e-5;
918 for t in 0..valid {
919 out[t * nm + m] = (feats[t * nm + m] - mean) / std;
920 }
921 }
922 out
923 }
924
925 fn chunks(&self, n: usize) -> Vec<((usize, usize), (usize, usize))> {
927 let c = &self.spk;
928 let mut out = Vec::new();
929 let mut start = 0;
930 while start < n {
931 let end = (start + c.chunk).min(n);
932 let cs = start.saturating_sub(c.left);
933 let ce = (end + c.right).min(n);
934 out.push(((cs, ce), (start - cs, end - cs)));
935 start += c.chunk;
936 }
937 out
938 }
939
940 fn diarize(&self, feats: &[f32], frames: usize, valid: usize, pool: Option<&Pool>) -> Vec<f32> {
942 let c = &self.spk;
943 let ns = c.n_spk;
944 let sub = self.diar.stack;
945 let d = self.diar.d;
946 let embeds = self.diar.embed(feats, frames, pool);
947 let n_emb = frames / sub;
948 let emask: Vec<bool> = (0..n_emb).map(|t| t * sub < valid).collect();
949 let mut cache = SpkCache {
950 embeds: Vec::new(),
951 probs: Vec::new(),
952 fifo: Vec::new(),
953 compressed: false,
954 };
955 let mut logits_all = Vec::with_capacity(frames * ns);
956 let mut start = 0;
957 while start < n_emb {
958 let end = (start + c.chunk).min(n_emb);
959 let n_chunk = end - start;
960 let cs = start.saturating_sub(c.left);
961 let lookback = start - cs;
962 let ce = (end + c.right).min(n_emb);
963 let mut input = cache.embeds.clone();
964 input.extend_from_slice(&cache.fifo);
965 let cached = input.len() / d;
966 input.extend_from_slice(&embeds[cs * d..ce * d]);
967 let n_in = input.len() / d;
968 let mut mask = vec![true; cached];
969 mask.extend_from_slice(&emask[cs..ce]);
970 let h = self.diar.encode_rows(&input, n_in, &mask, pool);
971 let h = self.diar_proj.apply(&h, n_in, pool);
972 let hu = self.upsample(&h, n_in, pool);
973 let mut a: Vec<f32> = hu.iter().map(|v| v.max(0.0)).collect();
975 a = self.cls_dense.apply(&a, n_in * sub, pool);
976 for v in a.iter_mut() {
977 *v = v.max(0.0);
978 }
979 let logits = self.cls_out.apply(&a, n_in * sub, pool);
980 cache.update(
981 c,
982 d,
983 &input,
984 &logits,
985 &mask,
986 &self.silence,
987 n_chunk,
988 lookback,
989 );
990 let s0 = (cached + lookback) * sub;
991 logits_all.extend_from_slice(&logits[s0 * ns..(s0 + n_chunk * sub) * ns]);
992 start += c.chunk;
993 }
994 logits_all.truncate(frames * ns);
995 logits_all
996 }
997
998 fn upsample(&self, h: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
1001 let cin = self.diar_proj.out;
1004 let cout = self.up_b.len();
1005 let mut x3 = vec![0.0f32; n * 3 * cin];
1006 for t in 0..n {
1007 for j in 0..3 {
1008 let src = t as isize + j as isize - 1;
1009 if src >= 0 && src < n as isize {
1010 let s = src as usize;
1011 x3[t * 3 * cin + j * cin..t * 3 * cin + (j + 1) * cin]
1012 .copy_from_slice(&h[s * cin..(s + 1) * cin]);
1013 }
1014 }
1015 }
1016 let mut y = vec![0.0f32; n * cout];
1017 gemm_nt(&x3, &self.up_w, &mut y, n, 3 * cin, cout, pool);
1018 for r in y.chunks_exact_mut(cout) {
1019 for (v, b) in r.iter_mut().zip(&self.up_b) {
1020 *v += b;
1021 }
1022 }
1023 y
1024 }
1025
1026 pub fn run(&self, samples: &[f32], pool: Option<&Pool>) -> AudioOut {
1027 let prof = std::env::var("CMF_NEMO_PROF").as_deref() == Ok("1");
1028 let t0 = Instant::now();
1029 let (feats, frames, valid) = self.log_mel(samples);
1030 let t_mel = t0.elapsed();
1031 let norm = self.normalize(&feats, frames, valid);
1032 let stack = self.asr.stack;
1033 let t_enc = frames / stack;
1034 let d = self.asr.d;
1035 let mut hidden = vec![0.0f32; t_enc * d];
1036 for ((cs, ce), (a, b)) in self.chunks(t_enc) {
1037 let valid_mask: Vec<bool> = (cs..ce).map(|t| t * stack < valid).collect();
1038 let h = self.asr.encode_frames(
1039 &norm[cs * stack * self.n_mel..ce * stack * self.n_mel],
1040 ce - cs,
1041 &valid_mask,
1042 pool,
1043 );
1044 hidden[(cs + a) * d..(cs + b) * d].copy_from_slice(&h[a * d..b * d]);
1045 }
1046 let t_asr = t0.elapsed();
1047 let speaker_logits = self.diarize(&feats, frames, valid, pool);
1048 let t_diar = t0.elapsed();
1049 let ns = self.spk.n_spk;
1050 let mut joined = hidden.clone();
1051 self.asr_norm.apply_rows(&mut joined);
1052 for t in 0..t_enc {
1053 let mut act = vec![0.0f32; ns];
1054 for (s, a) in act.iter_mut().enumerate() {
1055 let mut p = 0.0f32;
1056 for j in 0..stack {
1057 p += sigmoid(speaker_logits[(t * stack + j) * ns + s]);
1058 }
1059 *a = if p / stack as f32 > self.spk.activity_thr {
1060 1.0
1061 } else {
1062 0.0
1063 };
1064 }
1065 self.spk_norm.apply(&mut act);
1066 let row = &mut joined[t * d..(t + 1) * d];
1067 for (s, &a) in act.iter().enumerate() {
1068 for (v, &e) in row.iter_mut().zip(&self.spk_table[s * d..(s + 1) * d]) {
1069 *v += a * e;
1070 }
1071 }
1072 }
1073 let tokens = valid.div_ceil(stack);
1074 let mut embeds = self.proj.apply(&joined[..tokens * d], tokens, pool);
1075 for v in embeds.iter_mut() {
1076 *v = round_bf16(*v);
1077 }
1078 if prof {
1079 let ms = |d: std::time::Duration| d.as_secs_f64() * 1e3;
1080 eprintln!(
1081 "nemotron audio: {frames} frames, mel {:.1} ms, asr encoder {:.1} ms, diarization {:.1} ms, join+projector {:.1} ms",
1082 ms(t_mel),
1083 ms(t_asr - t_mel),
1084 ms(t_diar - t_asr),
1085 ms(t0.elapsed() - t_diar)
1086 );
1087 }
1088 AudioOut {
1089 feats,
1090 frames,
1091 valid_frames: valid,
1092 asr_hidden: hidden,
1093 speaker_logits,
1094 embeds,
1095 tokens,
1096 }
1097 }
1098}
1099
1100fn round_bf16(x: f32) -> f32 {
1101 let b = x.to_bits();
1102 let r = b.wrapping_add(0x7fff + ((b >> 16) & 1)) & 0xffff_0000;
1103 f32::from_bits(r)
1104}
1105
1106pub struct Transcriber {
1108 pub audio: Audio,
1109 pub llm: NemotronH,
1110 pub words: Option<words::WordTimes>,
1112 pub tok: Tokenizer,
1113 pub audio_token: u32,
1114 pub eos: Vec<u32>,
1115 pub default_prompt: String,
1116}
1117
1118pub struct Transcript {
1120 pub text: String,
1121 pub ids: Vec<u32>,
1122 pub prompt_tokens: usize,
1123 pub audio_tokens: usize,
1124 pub audio_s: f64,
1125 pub encode_s: f64,
1126 pub prefill_s: f64,
1127 pub decode_s: f64,
1128 pub words: Option<Result<Vec<words::WordTime>, String>>,
1130 pub words_s: f64,
1131}
1132
1133impl Transcriber {
1134 pub fn load(model: &Arc<CmfModel>) -> Result<Self, String> {
1135 if model.header.arch.arch_name != ARCH_NAME {
1136 return Err(format!(
1137 "{} is not a Nemotron 3.5 Transcribe CMF",
1138 model.path.display()
1139 ));
1140 }
1141 let prov = model
1142 .header
1143 .provenance
1144 .as_ref()
1145 .and_then(|p| p.get("nemotron_transcribe"))
1146 .ok_or("CMF is missing the nemotron_transcribe provenance")?;
1147 let mut llm = NemotronH::load(model, &prov["llm_config"])?;
1148 if let Some(h) = prov.get("expert_hot").and_then(|v| v.as_array()) {
1151 llm.hot = h
1152 .iter()
1153 .map(|l| {
1154 l.as_array()
1155 .map(|a| a.iter().filter_map(|e| e.as_u64()).map(|e| e as usize).collect())
1156 .unwrap_or_default()
1157 })
1158 .collect();
1159 }
1160 let audio = Audio::load(model, &prov["audio_config"])?;
1161 let words = words::WordTimes::load(model, prov)?;
1162 let vocab = model
1163 .vocab
1164 .as_deref()
1165 .ok_or("CMF has no embedded tokenizer.json")?;
1166 let tok = Tokenizer::from_bytes(vocab).map_err(|e| format!("tokenizer: {e}"))?;
1167 let audio_token = prov
1168 .get("audio_token_id")
1169 .and_then(|v| v.as_u64())
1170 .unwrap_or(131072) as u32;
1171 let mut eos: Vec<u32> = prov["generation_config"]["eos_token_id"]
1172 .as_array()
1173 .map(|a| {
1174 a.iter()
1175 .filter_map(|v| v.as_u64())
1176 .map(|v| v as u32)
1177 .collect()
1178 })
1179 .unwrap_or_default();
1180 if let Some(id) = tok.token_to_id("<|im_end|>")
1181 && !eos.contains(&id)
1182 {
1183 eos.push(id);
1184 }
1185 let _ = llm.frame_seed(&llm.new_state());
1188 let default_prompt = prov["processor"]
1189 .get("default_transcription_prompt")
1190 .and_then(|v| v.as_str())
1191 .unwrap_or("Transcribe the following audio.")
1192 .to_string();
1193 Ok(Self {
1194 audio,
1195 llm,
1196 tok,
1197 audio_token,
1198 eos,
1199 default_prompt,
1200 words,
1201 })
1202 }
1203
1204 pub fn prompt_ids(&self, prompt: &str, n_audio: usize, think: bool) -> Vec<u32> {
1208 let head = format!("<|im_start|>system\n<|im_end|>\n<|im_start|>user\n{prompt} ");
1209 let tail = if think {
1210 "<|im_end|>\n<|im_start|>assistant\n<think>\n"
1211 } else {
1212 "<|im_end|>\n<|im_start|>assistant\n<think></think>"
1213 };
1214 let mut ids = self.tok.encode(&head);
1215 ids.extend(std::iter::repeat_n(self.audio_token, n_audio));
1216 ids.extend(self.tok.encode(tail));
1217 ids
1218 }
1219
1220 pub fn input_rows(&self, ids: &[u32], audio: &[f32]) -> Result<Vec<f32>, String> {
1222 let hd = self.llm.g.hidden;
1223 let mut rows = vec![0.0f32; ids.len() * hd];
1224 let mut ai = 0;
1225 for (i, &id) in ids.iter().enumerate() {
1226 let dst = &mut rows[i * hd..(i + 1) * hd];
1227 if id == self.audio_token {
1228 let src = audio
1229 .get(ai * hd..(ai + 1) * hd)
1230 .ok_or("more audio placeholders than audio tokens")?;
1231 dst.copy_from_slice(src);
1232 ai += 1;
1233 } else {
1234 self.llm.embed_row(id, dst);
1235 }
1236 }
1237 if ai * hd != audio.len() {
1238 return Err(format!(
1239 "{ai} audio placeholders for {} audio tokens",
1240 audio.len() / hd
1241 ));
1242 }
1243 Ok(rows)
1244 }
1245
1246 pub fn transcribe(
1247 &self,
1248 samples: &[f32],
1249 prompt: Option<&str>,
1250 max_new_tokens: usize,
1251 timestamps: bool,
1252 pool: Option<&Pool>,
1253 mut on_token: impl FnMut(&str),
1254 ) -> Result<Transcript, String> {
1255 let t0 = Instant::now();
1256 let a = self.audio.run(samples, pool);
1257 let encode_s = t0.elapsed().as_secs_f64();
1258 let ids = self.prompt_ids(prompt.unwrap_or(&self.default_prompt), a.tokens, false);
1259 let rows = self.input_rows(&ids, &a.embeds)?;
1260 let host_prefill = std::env::var("CMF_NEMO_PREFILL").as_deref() == Ok("host");
1266 let t1 = Instant::now();
1267 let hd = self.llm.g.hidden;
1268 let n = rows.len() / hd;
1269 let mut dev_from: Option<Vec<f32>> = None;
1270 let mut host: Option<(super::nemotron_h::State, u32)> = None;
1271 if !host_prefill && max_new_tokens > 0 && self.llm.frame_prefill(&rows[..(n - 1) * hd], Some(&rows[(n - 1) * hd..]), pool) {
1272 dev_from = Some(rows[(n - 1) * hd..].to_vec());
1273 } else {
1274 let mut st = self.llm.new_state();
1275 let logits = self.llm.forward(&mut st, &rows, pool);
1276 let first = argmax(&logits[..self.llm.g.vocab.min(logits.len())]);
1277 if max_new_tokens > 1 && self.llm.frame_seed(&st) {
1278 let mut r = vec![0.0f32; hd];
1279 self.llm.embed_row(first, &mut r);
1280 dev_from = Some(r);
1281 }
1282 host = Some((st, first));
1283 }
1284 let prefill_s = t1.elapsed().as_secs_f64();
1285 let t2 = Instant::now();
1286 let mut out = Vec::new();
1287 let mut printed = 0usize;
1288 let emit = |tok: u32,
1289 out: &mut Vec<u32>,
1290 printed: &mut usize,
1291 on_token: &mut dyn FnMut(&str)|
1292 -> bool {
1293 if self.eos.contains(&tok) || out.len() >= max_new_tokens {
1294 return false;
1295 }
1296 out.push(tok);
1297 let text = self.tok.decode(out);
1298 if text.len() > *printed && text.is_char_boundary(*printed) {
1299 on_token(&text[*printed..]);
1300 *printed = text.len();
1301 }
1302 true
1303 };
1304 let mut go = true;
1305 if let Some((_, first)) = &host {
1306 go = emit(*first, &mut out, &mut printed, &mut on_token);
1307 }
1308 if go {
1309 if let Some(row) = dev_from {
1310 let budget = max_new_tokens - out.len();
1311 self.llm.frame_decode(&row, budget, pool, |ids| {
1312 ids.iter()
1313 .all(|&t| emit(t, &mut out, &mut printed, &mut on_token))
1314 })?;
1315 } else if let Some((mut st, first)) = host {
1316 let mut row = vec![0.0f32; hd];
1317 let mut tok = first;
1318 loop {
1319 self.llm.embed_row(tok, &mut row);
1320 let logits = self.llm.forward(&mut st, &row, pool);
1321 tok = argmax(&logits[..self.llm.g.vocab.min(logits.len())]);
1322 if !emit(tok, &mut out, &mut printed, &mut on_token) {
1323 break;
1324 }
1325 }
1326 }
1327 }
1328 let decode_s = t2.elapsed().as_secs_f64();
1329 if std::env::var("CMF_NEMO_MTP_PROBE").as_deref() == Ok("1") && out.len() > 4 {
1330 self.mtp_probe(&rows, &ids, &out, pool);
1331 }
1332 let text = self.tok.decode(&out).trim().to_string();
1333 let t3 = Instant::now();
1334 let words = match (&self.words, timestamps) {
1335 (Some(w), true) => {
1336 let stack = self.audio.asr.stack;
1337 let t_enc = a.frames / stack;
1338 let valid: Vec<bool> = (0..t_enc).map(|t| t * stack < a.valid_frames).collect();
1339 let logits = w.logits(&a.asr_hidden, t_enc, &valid, &self.audio.chunks(t_enc), pool);
1340 if let Ok(dir) = std::env::var("CMF_NEMO_WORDS_DUMP") {
1342 let b: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
1343 let _ = std::fs::create_dir_all(&dir);
1344 let _ = std::fs::write(Path::new(&dir).join(format!("logits-{}.f32", a.tokens)), b);
1345 }
1346 let ns = self.audio.spk.n_spk;
1347 Some(w.align(
1348 &text,
1349 &logits,
1350 a.tokens,
1351 a.valid_frames,
1352 Some((&a.speaker_logits[..], ns)),
1353 ))
1354 }
1355 _ => None,
1356 };
1357 let words_s = t3.elapsed().as_secs_f64();
1358 Ok(Transcript {
1359 text,
1360 prompt_tokens: ids.len(),
1361 audio_tokens: a.tokens,
1362 ids: out,
1363 audio_s: samples.len() as f64 / SR as f64,
1364 encode_s,
1365 prefill_s,
1366 decode_s,
1367 words,
1368 words_s,
1369 })
1370 }
1371}
1372
1373impl Transcriber {
1374 fn mtp_probe(&self, rows: &[f32], ids: &[u32], gen_ids: &[u32], pool: Option<&Pool>) {
1379 let llm = &self.llm;
1380 if llm.mtp.is_none() {
1381 eprintln!("mtp probe: no draft module in this file");
1382 return;
1383 }
1384 let hd = llm.g.hidden;
1385 let mut all = rows.to_vec();
1386 let mut r = vec![0.0f32; hd];
1387 for &t in gen_ids {
1388 llm.embed_row(t, &mut r);
1389 all.extend_from_slice(&r);
1390 }
1391 let toks: Vec<u32> = ids.iter().chain(gen_ids).copied().collect();
1392 let n = toks.len();
1393 let np = ids.len();
1394 let mut st = llm.new_state();
1395 let h = llm.forward_h(&mut st, &all, pool);
1396 let mut hn = vec![0.0f32; h.len()];
1397 for (s, d) in h.chunks_exact(hd).zip(hn.chunks_exact_mut(hd)) {
1398 super::nemotron_h::rms_norm(s, &llm.final_norm, llm.g.eps, d);
1399 }
1400 let main = llm.head_argmax(&hn[(np - 1) * hd..(n - 1) * hd], pool);
1402 let ok = main.iter().zip(&toks[np..]).filter(|(a, b)| a == b).count();
1403 eprintln!("mtp probe: main head {ok}/{} of the transcript", main.len());
1404 for (name, src, h_first) in [
1405 ("pre-norm h, [e|h]", &h, false),
1406 ("post-norm h, [e|h]", &hn, false),
1407 ("pre-norm h, [h|e]", &h, true),
1408 ("post-norm h, [h|e]", &hn, true),
1409 ] {
1410 let mut kv = (Vec::new(), Vec::new());
1411 let Some(o1) = llm.mtp_forward(&mut kv, 0, &src[..(n - 1) * hd], &all[hd..], h_first, pool)
1412 else {
1413 return;
1414 };
1415 let a1 = llm.head_argmax(&o1[(np - 1) * hd..(n - 2) * hd], pool);
1417 let ok1 = a1.iter().zip(&toks[np + 1..]).filter(|(a, b)| a == b).count();
1418 let mut kv2 = (Vec::new(), Vec::new());
1419 let o2 = llm
1420 .mtp_forward(&mut kv2, 0, &o1[..(n - 2) * hd], &all[2 * hd..], h_first, pool)
1421 .unwrap_or_default();
1422 let a2 = llm.head_argmax(&o2[(np - 1) * hd..(n - 3) * hd], pool);
1423 let ok2 = a2.iter().zip(&toks[np + 2..]).filter(|(a, b)| a == b).count();
1424 eprintln!(
1425 "mtp probe {name}: depth 1 {ok1}/{} ({:.1}%), depth 2 {ok2}/{} ({:.1}%)",
1426 a1.len(),
1427 100.0 * ok1 as f64 / a1.len().max(1) as f64,
1428 a2.len(),
1429 100.0 * ok2 as f64 / a2.len().max(1) as f64
1430 );
1431 let span = (n - 2) - (np - 1);
1434 let mut kv = (Vec::new(), Vec::new());
1435 let o = llm
1436 .mtp_forward(&mut kv, 0, &src[(np - 1) * hd..(n - 2) * hd], &all[np * hd..(n - 1) * hd], h_first, pool)
1437 .unwrap_or_default();
1438 let a = llm.head_argmax(&o, pool);
1439 let ok_g = a.iter().zip(&toks[np + 1..]).filter(|(a, b)| a == b).count();
1440 let mut ok_s = 0;
1441 for t in 0..span {
1442 let p = np - 1 + t;
1443 let mut kv = (Vec::new(), Vec::new());
1444 let o = llm
1445 .mtp_forward(&mut kv, 0, &src[p * hd..(p + 1) * hd], &all[(p + 1) * hd..(p + 2) * hd], h_first, pool)
1446 .unwrap_or_default();
1447 ok_s += (llm.head_argmax(&o, pool)[0] == toks[p + 2]) as usize;
1448 }
1449 eprintln!(
1450 "mtp probe {name}: depth 1 with generated-span history {ok_g}/{span}, without history {ok_s}/{span}"
1451 );
1452 }
1453 }
1454
1455 pub fn dump_parity(
1460 &self,
1461 samples: &[f32],
1462 prompt: Option<&str>,
1463 forced: &[u32],
1464 dir: &Path,
1465 pool: Option<&Pool>,
1466 ) -> Result<(), String> {
1467 std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
1468 let put = |name: &str, v: &[f32]| -> Result<(), String> {
1469 let b: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
1470 std::fs::write(dir.join(name), b).map_err(|e| format!("{name}: {e}"))
1471 };
1472 let a = self.audio.run(samples, pool);
1473 let ids = self.prompt_ids(prompt.unwrap_or(&self.default_prompt), a.tokens, false);
1474 put("feats.f32", &a.feats)?;
1475 put("asr_hidden.f32", &a.asr_hidden)?;
1476 put("speaker_logits.f32", &a.speaker_logits)?;
1477 put("embeds.f32", &a.embeds)?;
1478 let idb: Vec<u8> = ids.iter().flat_map(|x| x.to_le_bytes()).collect();
1479 std::fs::write(dir.join("input_ids.u32"), idb).map_err(|e| e.to_string())?;
1480 if forced.is_empty() {
1481 let meta = serde_json::json!({
1482 "frames": a.frames, "valid_frames": a.valid_frames, "n_mel": self.audio.n_mel,
1483 "enc_frames": a.asr_hidden.len() / self.audio.asr.d, "enc_d": self.audio.asr.d,
1484 "tokens": a.tokens, "hidden": self.llm.g.hidden, "prompt_len": ids.len(), "forced": 0,
1485 });
1486 return std::fs::write(dir.join("meta.json"), meta.to_string())
1487 .map_err(|e| e.to_string());
1488 }
1489 let rows = self.input_rows(&ids, &a.embeds)?;
1490 let mut st = self.llm.new_state();
1491 let t0 = Instant::now();
1492 let mut logits = self.llm.forward(&mut st, &rows, pool);
1493 let prefill_s = t0.elapsed().as_secs_f64();
1494 let vocab = self.llm.g.vocab;
1495 let mut all = Vec::with_capacity(forced.len() * vocab);
1496 let mut row = vec![0.0f32; self.llm.g.hidden];
1497 let t1 = Instant::now();
1498 for (i, &id) in forced.iter().enumerate() {
1499 all.extend_from_slice(&logits[..vocab]);
1500 if i + 1 < forced.len() {
1501 self.llm.embed_row(id, &mut row);
1502 logits = self.llm.forward(&mut st, &row, pool);
1503 }
1504 }
1505 let decode_s = t1.elapsed().as_secs_f64();
1506 put("logits.f32", &all)?;
1507 let meta = serde_json::json!({
1508 "frames": a.frames, "valid_frames": a.valid_frames, "n_mel": self.audio.n_mel,
1509 "enc_frames": a.asr_hidden.len() / self.audio.asr.d, "enc_d": self.audio.asr.d,
1510 "tokens": a.tokens, "hidden": self.llm.g.hidden, "prompt_len": ids.len(),
1511 "forced": forced.len(), "vocab": vocab, "prefill_s": prefill_s, "decode_s": decode_s,
1512 });
1513 std::fs::write(dir.join("meta.json"), meta.to_string()).map_err(|e| e.to_string())
1514 }
1515}
1516
1517fn argmax(x: &[f32]) -> u32 {
1518 let mut best = 0;
1519 for i in 1..x.len() {
1520 if x[i] > x[best] {
1521 best = i;
1522 }
1523 }
1524 best as u32
1525}
1526
1527pub fn read_audio(path: &Path) -> Result<Vec<f32>, String> {
1529 let (mono, rate) = crate::whisper::read_wav_samples(path, false)?;
1530 Ok(if rate == SR {
1531 mono
1532 } else {
1533 crate::whisper::resample_sinc(&mono, rate, SR)
1534 })
1535}
1536
1537#[cfg(test)]
1538mod tests {
1539 use super::*;
1540
1541 #[test]
1542 fn rope_rotates_the_first_half_pairs() {
1543 let (c, s) = rope_table(3, 4);
1544 let mut x = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1545 rope(&mut x, &c[2 * 2..], &s[2 * 2..], 4);
1546 let (a0, a1) = (2.0f32, 2.0f32 / 100.0);
1548 assert!((x[0] - (1.0 * a0.cos() - 3.0 * a0.sin())).abs() < 1e-6);
1549 assert!((x[2] - (3.0 * a0.cos() + 1.0 * a0.sin())).abs() < 1e-6);
1550 assert!((x[1] - (2.0 * a1.cos() - 4.0 * a1.sin())).abs() < 1e-6);
1551 assert_eq!(x[4], 5.0);
1552 }
1553
1554 #[test]
1555 fn bf16_rounding_is_nearest_even() {
1556 assert_eq!(round_bf16(1.0), 1.0);
1557 let x = f32::from_bits(0x3f80_8000); assert_eq!(round_bf16(x).to_bits(), 0x3f80_0000);
1559 let y = f32::from_bits(0x3f81_8000); assert_eq!(round_bf16(y).to_bits(), 0x3f82_0000);
1561 }
1562
1563 fn cfg() -> SpkCfg {
1564 SpkCfg {
1565 chunk: 500,
1566 left: 50,
1567 right: 50,
1568 fifo_len: 4,
1569 update_period: 3,
1570 cache_len: 16,
1571 n_spk: 2,
1572 sil_frames: 1,
1573 pred_thr: 0.25,
1574 latest_boost: 0.05,
1575 min_pos: 3,
1576 n_strong: 5,
1577 n_weak: 10,
1578 activity_thr: 0.5,
1579 }
1580 }
1581
1582 #[test]
1583 fn compress_keeps_cache_length_with_silence_slots() {
1584 let c = cfg();
1585 let d = 3;
1586 let n = 20;
1587 let embeds: Vec<f32> = (0..n * d).map(|i| i as f32).collect();
1588 let probs: Vec<f32> = (0..n)
1589 .flat_map(|t| if t < 10 { [0.9f32, 0.1] } else { [0.2, 0.8] })
1590 .collect();
1591 let sil = [-1.0f32; 3];
1592 let (e, p) = compress(&c, d, &embeds, &probs, &sil);
1593 assert_eq!(e.len(), c.cache_len * d);
1594 assert_eq!(p.len(), c.cache_len * c.n_spk);
1595 assert_eq!(e.chunks_exact(d).filter(|r| r[0] == -1.0).count(), 2);
1597 }
1598
1599 #[test]
1600 fn fifo_pops_at_least_the_update_period() {
1601 let c = cfg();
1602 let d = 1;
1603 let mut cache = SpkCache {
1604 embeds: vec![],
1605 probs: vec![],
1606 fifo: vec![],
1607 compressed: false,
1608 };
1609 let input: Vec<f32> = (0..6).map(|i| i as f32).collect();
1610 let logits = vec![0.0f32; 6 * 2 * 2];
1611 let mask = vec![true; 6];
1612 cache.update(&c, d, &input, &logits, &mask, &[0.0], 6, 0);
1613 assert_eq!(cache.embeds, vec![0.0, 1.0, 2.0]);
1615 assert_eq!(cache.fifo, vec![3.0, 4.0, 5.0]);
1616 }
1617}