fix(cuda+csm): P0 rtx-backend-cuda compile fix + P2 rtx-csm clippy cleanup (#9)

Co-authored-by: Omar Sobh <[email protected]>
Co-committed-by: Omar Sobh <[email protected]>
This commit is contained in:
Omar Sobh
2026-04-30 05:24:58 +00:00
committed by redclawsystems
parent 8045ba79d4
commit 6a03aeba61
24 changed files with 68 additions and 48 deletions
+4
View File
@@ -49,3 +49,7 @@ path = "src/lib.rs"
[lints] [lints]
workspace = true workspace = true
[[test]]
name = "backend_parity_tests"
required-features = ["cuda"]
@@ -6,6 +6,18 @@
use cudarc::cublas::CudaBlas; use cudarc::cublas::CudaBlas;
#[cfg(feature = "cuda")] #[cfg(feature = "cuda")]
use cudarc::driver::{CudaContext, CudaStream}; use cudarc::driver::{CudaContext, CudaStream};
#[cfg(feature = "cuda")]
use crate::CudaBackend;
#[cfg(feature = "cuda")]
use rtx_backend::{DeviceId, DeviceOps};
#[cfg(feature = "cuda")]
use once_cell::sync::{Lazy, OnceCell};
#[cfg(feature = "cuda")]
use parking_lot::RwLock;
#[cfg(feature = "cuda")]
use std::collections::HashMap;
#[cfg(feature = "cuda")]
use std::sync::Arc;
use crate::{CudaError, CudaResult}; use crate::{CudaError, CudaResult};
+2
View File
@@ -104,6 +104,8 @@ pub use device::CudaDevice;
pub use error::{CudaError, CudaResult}; pub use error::{CudaError, CudaResult};
pub use tensor::CudaTensorPrimitive; pub use tensor::CudaTensorPrimitive;
#[cfg(feature = "cuda")]
use rtx_backend::{Backend, BoolU8, DeviceId, DeviceOps};
use std::fmt::Debug; use std::fmt::Debug;
/// CUDA backend for RustyTorch++. /// CUDA backend for RustyTorch++.
@@ -5,6 +5,8 @@
use crate::CudaDevice; use crate::CudaDevice;
#[cfg(feature = "cuda")] #[cfg(feature = "cuda")]
use cudarc::driver::CudaSlice; use cudarc::driver::CudaSlice;
#[cfg(feature = "cuda")]
use std::sync::Arc;
#[cfg(feature = "cuda")] #[cfg(feature = "cuda")]
/// CUDA tensor primitive - GPU memory storage for tensors. /// CUDA tensor primitive - GPU memory storage for tensors.
+1 -1
View File
@@ -448,7 +448,7 @@ impl MsgProcessor {
debug_assert_eq!(c, self.hidden); debug_assert_eq!(c, self.hidden);
let mut indices = Vec::with_capacity(self.nbits); let mut indices = Vec::with_capacity(self.nbits);
for k in 0..self.nbits { for k in 0..self.nbits {
let bit = ((message >> k) & 1) as u32; let bit = (message >> k) & 1;
indices.push(2 * k as u32 + bit); indices.push(2 * k as u32 + bit);
} }
let idx = Tensor::from_vec(indices, (self.nbits,), device)?; let idx = Tensor::from_vec(indices, (self.nbits,), device)?;
+3
View File
@@ -923,6 +923,7 @@ impl Model {
self.decoder.clear_kv_cache(); self.decoder.clear_kv_cache();
let mut decoder_pos = 0; let mut decoder_pos = 0;
#[allow(clippy::needless_range_loop)]
for i in 1..self.config.audio_num_codebooks { for i in 1..self.config.audio_num_codebooks {
let proj_h = curr_h.apply(&self.projection)?; let proj_h = curr_h.apply(&self.projection)?;
let decoder_h = self.decoder.forward(&proj_h, decoder_pos)?; let decoder_h = self.decoder.forward(&proj_h, decoder_pos)?;
@@ -1019,6 +1020,7 @@ impl Model {
let mut curr_h = Tensor::cat(&[h, c0_embed], 1)?; let mut curr_h = Tensor::cat(&[h, c0_embed], 1)?;
self.decoder.clear_kv_cache(); self.decoder.clear_kv_cache();
let mut decoder_pos = 0usize; let mut decoder_pos = 0usize;
#[allow(clippy::needless_range_loop)]
for i in 1..self.config.audio_num_codebooks { for i in 1..self.config.audio_num_codebooks {
let proj_h = curr_h.apply(&self.projection)?; let proj_h = curr_h.apply(&self.projection)?;
let decoder_h = self.decoder.forward(&proj_h, decoder_pos)?; let decoder_h = self.decoder.forward(&proj_h, decoder_pos)?;
@@ -1054,6 +1056,7 @@ impl Model {
/// respectively). /// respectively).
/// ///
/// Reference: Koel-TTS (NVIDIA, arXiv 2502.05236), `cfg_scale ∈ [1.5, 3.0]`. /// Reference: Koel-TTS (NVIDIA, arXiv 2502.05236), `cfg_scale ∈ [1.5, 3.0]`.
#[allow(clippy::too_many_arguments)]
pub fn generate_frame_cfg( pub fn generate_frame_cfg(
&mut self, &mut self,
cond_tokens: &Tensor, cond_tokens: &Tensor,
+4 -5
View File
@@ -406,9 +406,9 @@ impl Layer {
let xs = xs.apply(&self.mlp_norm)?.apply(&self.mlp)?; let xs = xs.apply(&self.mlp_norm)?.apply(&self.mlp)?;
let out = (residual + xs)?; let out = (residual + xs)?;
// Optional layer-by-layer dump for qmatmul bisection. // Optional layer-by-layer dump for qmatmul bisection.
if std::env::var("CSM_DUMP_LAYERS").is_ok() { if std::env::var("CSM_DUMP_LAYERS").is_ok()
if let Some(idx) = dbg_idx { && let Some(idx) = dbg_idx
if seqlen_offset == 0 { && seqlen_offset == 0 {
let f = out.flatten_all()?.to_vec1::<f32>().unwrap_or_default(); let f = out.flatten_all()?.to_vec1::<f32>().unwrap_or_default();
let n: f32 = f.iter().map(|x| x * x).sum::<f32>().sqrt(); let n: f32 = f.iter().map(|x| x * x).sum::<f32>().sqrt();
eprintln!( eprintln!(
@@ -419,8 +419,6 @@ impl Layer {
&f[..8.min(f.len())] &f[..8.min(f.len())]
); );
} }
}
}
Ok(out) Ok(out)
} }
@@ -761,6 +759,7 @@ impl Model {
self.run_decoder(h, c0_sample, lp) self.run_decoder(h, c0_sample, lp)
} }
#[allow(clippy::too_many_arguments)]
pub fn generate_frame_cfg( pub fn generate_frame_cfg(
&mut self, &mut self,
cond_tokens: &Tensor, cond_tokens: &Tensor,
+5 -9
View File
@@ -231,12 +231,9 @@ fn vad_intervals(probs: &[f32], cfg: &DiarizationConfig) -> Vec<(usize, usize)>
let mut last_speech: Option<usize> = None; let mut last_speech: Option<usize> = None;
for i in 0..active.len() { for i in 0..active.len() {
if active[i] { if active[i] {
if let Some(prev) = last_speech { if let Some(prev) = last_speech
if i - prev <= max_silence_chunks + 1 { && i - prev <= max_silence_chunks + 1 {
for j in (prev + 1)..i { active[(prev + 1)..i].fill(true);
active[j] = true;
}
}
} }
last_speech = Some(i); last_speech = Some(i);
} }
@@ -308,11 +305,10 @@ fn agglomerative_cluster(
if clusters.len() == 1 { if clusters.len() == 1 {
break; break;
} }
if let Some(k) = n_speakers { if let Some(k) = n_speakers
if clusters.len() <= k { && clusters.len() <= k {
break; break;
} }
}
// Find the closest pair using average linkage. // Find the closest pair using average linkage.
let mut best: Option<(usize, usize, f32)> = None; let mut best: Option<(usize, usize, f32)> = None;
for i in 0..clusters.len() { for i in 0..clusters.len() {
+1 -1
View File
@@ -504,7 +504,7 @@ impl RelativePositionalEncoder {
groups: usize, groups: usize,
vb: VarBuilder, vb: VarBuilder,
) -> CsmResult<Self> { ) -> CsmResult<Self> {
if kernel % 2 == 0 { if kernel.is_multiple_of(2) {
return Err(crate::CsmError::Config(format!( return Err(crate::CsmError::Config(format!(
"relative_positional_encoder kernel must be odd for same-padding, got {kernel}" "relative_positional_encoder kernel must be odd for same-padding, got {kernel}"
))); )));
+8 -12
View File
@@ -392,14 +392,13 @@ impl Generator {
break; break;
} }
if let Some(g) = rep_guard.as_mut() { if let Some(g) = rep_guard.as_mut()
if g.observe(&sampled) { && g.observe(&sampled) {
tracing::warn!( tracing::warn!(
"loop-escape: repetition guard tripped at frame {frame_idx}; ending generation" "loop-escape: repetition guard tripped at frame {frame_idx}; ending generation"
); );
break; break;
} }
}
all_frames.push(sampled.clone()); all_frames.push(sampled.clone());
@@ -502,14 +501,13 @@ impl Generator {
tracing::info!("EOT detected at frame {frame_idx}"); tracing::info!("EOT detected at frame {frame_idx}");
break; break;
} }
if let Some(g) = rep_guard.as_mut() { if let Some(g) = rep_guard.as_mut()
if g.observe(&sampled) { && g.observe(&sampled) {
tracing::warn!( tracing::warn!(
"loop-escape: repetition guard tripped at frame {frame_idx}; ending" "loop-escape: repetition guard tripped at frame {frame_idx}; ending"
); );
break; break;
} }
}
pending.push(sampled.clone()); pending.push(sampled.clone());
@@ -541,12 +539,11 @@ impl Generator {
)?; )?;
} }
// Drain any internal Mimi buffering by feeding a `None` step. // Drain any internal Mimi buffering by feeding a `None` step.
if let Some(tail) = self.mimi.decode_step(None)? { if let Some(tail) = self.mimi.decode_step(None)?
if !tail.is_empty() { && !tail.is_empty() {
on_chunk(&tail)?; on_chunk(&tail)?;
full_pcm.extend_from_slice(&tail); full_pcm.extend_from_slice(&tail);
} }
}
tracing::info!( tracing::info!(
"streaming generation finished: {} samples (~{:.2}s)", "streaming generation finished: {} samples (~{:.2}s)",
@@ -630,12 +627,11 @@ where
} }
let codes = Tensor::from_vec(flat, (1, num_codebooks, k), device)?.to_dtype(DType::U32)?; let codes = Tensor::from_vec(flat, (1, num_codebooks, k), device)?.to_dtype(DType::U32)?;
pending.clear(); pending.clear();
if let Some(samples) = mimi.decode_step(Some(&codes))? { if let Some(samples) = mimi.decode_step(Some(&codes))?
if !samples.is_empty() { && !samples.is_empty() {
on_chunk(&samples)?; on_chunk(&samples)?;
full_pcm.extend_from_slice(&samples); full_pcm.extend_from_slice(&samples);
} }
}
Ok(()) Ok(())
} }
+2 -3
View File
@@ -239,13 +239,12 @@ impl LlmClient for OpenAiCompatibleClient {
// default case so this is a no-op. // default case so this is a no-op.
let mut body = serde_json::to_value(&req) let mut body = serde_json::to_value(&req)
.map_err(|e| CsmError::Config(format!("LLM serialize: {e}")))?; .map_err(|e| CsmError::Config(format!("LLM serialize: {e}")))?;
if !config.extra_body.is_empty() { if !config.extra_body.is_empty()
if let Some(obj) = body.as_object_mut() { && let Some(obj) = body.as_object_mut() {
for (k, v) in config.extra_body.iter() { for (k, v) in config.extra_body.iter() {
obj.insert(k.clone(), v.clone()); obj.insert(k.clone(), v.clone());
} }
} }
}
let response = self let response = self
.http .http
.post(&url) .post(&url)
+4 -3
View File
@@ -106,9 +106,9 @@ fn split_on_sentence_boundaries(text: &str) -> Vec<String> {
impl Generator { impl Generator {
/// Generate audio for arbitrarily long text by chunking on sentence /// Generate audio for arbitrarily long text by chunking on sentence
/// boundaries with a rolling context. The previous generated chunk's audio /// boundaries with a rolling context. The previous generated chunk's
/// + transcript becomes context for the next call. Returns the full /// audio + transcript becomes context for the next call. Returns the
/// concatenated PCM. /// full concatenated PCM.
pub fn generate_long( pub fn generate_long(
&mut self, &mut self,
text: &str, text: &str,
@@ -185,6 +185,7 @@ impl Generator {
/// ///
/// Order matches `generate_to_wav` exactly so installing a watermarker /// Order matches `generate_to_wav` exactly so installing a watermarker
/// applies uniformly to short-form and long-form output. /// applies uniformly to short-form and long-form output.
#[allow(clippy::too_many_arguments)]
pub fn generate_long_to_wav( pub fn generate_long_to_wav(
&mut self, &mut self,
text: &str, text: &str,
+2
View File
@@ -78,6 +78,7 @@ pub struct LoraDelta {
} }
impl LoraDelta { impl LoraDelta {
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
rank: usize, rank: usize,
alpha: f64, alpha: f64,
@@ -169,6 +170,7 @@ impl LoraLinear {
/// Wrap an existing frozen `Linear` with a trainable rank-r LoRA adapter. /// Wrap an existing frozen `Linear` with a trainable rank-r LoRA adapter.
/// The A/B params get registered into `vm` under `<prefix>.lora_a` / /// The A/B params get registered into `vm` under `<prefix>.lora_a` /
/// `<prefix>.lora_b` so AdamW (or any optimizer) can find and update them. /// `<prefix>.lora_b` so AdamW (or any optimizer) can find and update them.
#[allow(clippy::too_many_arguments)]
pub fn wrap( pub fn wrap(
base: Linear, base: Linear,
rank: usize, rank: usize,
+1
View File
@@ -68,6 +68,7 @@ impl ModelBackend {
Self::Quantized(m) => m.generate_frame(tokens, mask, input_pos, lp), Self::Quantized(m) => m.generate_frame(tokens, mask, input_pos, lp),
} }
} }
#[allow(clippy::too_many_arguments)]
pub fn generate_frame_cfg( pub fn generate_frame_cfg(
&mut self, &mut self,
cond_tokens: &Tensor, cond_tokens: &Tensor,
+2 -2
View File
@@ -77,7 +77,7 @@ impl MoonshineConfig {
pub fn padded_head_dim(&self, num_heads: usize) -> usize { pub fn padded_head_dim(&self, num_heads: usize) -> usize {
let raw = self.hidden_size / num_heads; let raw = self.hidden_size / num_heads;
let pad = self.pad_head_dim_to_multiple_of; let pad = self.pad_head_dim_to_multiple_of;
((raw + pad - 1) / pad) * pad raw.div_ceil(pad) * pad
} }
} }
@@ -191,7 +191,7 @@ struct RotaryCache {
impl RotaryCache { impl RotaryCache {
fn new(rotary_dim: usize, max_seq: usize, theta: f64, dtype: DType, dev: &Device) -> Result<Self> { fn new(rotary_dim: usize, max_seq: usize, theta: f64, dtype: DType, dev: &Device) -> Result<Self> {
assert!(rotary_dim % 2 == 0, "rotary_dim must be even"); assert!(rotary_dim.is_multiple_of(2), "rotary_dim must be even");
let inv_freq: Vec<f32> = (0..rotary_dim) let inv_freq: Vec<f32> = (0..rotary_dim)
.step_by(2) .step_by(2)
.map(|i| (1.0 / theta.powf(i as f64 / rotary_dim as f64)) as f32) .map(|i| (1.0 / theta.powf(i as f64 / rotary_dim as f64)) as f32)
+1 -1
View File
@@ -42,7 +42,7 @@ impl PostProcess {
} }
} }
pub fn apply(&self, samples: &mut Vec<f32>, sample_rate: u32) -> Result<()> { pub fn apply(&self, samples: &mut [f32], sample_rate: u32) -> Result<()> {
if samples.is_empty() { if samples.is_empty() {
return Ok(()); return Ok(());
} }
+2
View File
@@ -2,8 +2,10 @@
//! that CSM's dual-transformer expects. //! that CSM's dual-transformer expects.
//! //!
//! Slot layout per row of the (S, cb+1) tensor: //! Slot layout per row of the (S, cb+1) tensor:
//!
//! - slots [0..cb): per-codebook audio token id (or 0 if unused) //! - slots [0..cb): per-codebook audio token id (or 0 if unused)
//! - slot cb: Llama text token id (or 0 if unused) //! - slot cb: Llama text token id (or 0 if unused)
//!
//! The mask (S, cb+1) u8 says which slots actually carry data. //! The mask (S, cb+1) u8 says which slots actually carry data.
//! //!
//! We delegate per-step tensor construction to candle's //! We delegate per-step tensor construction to candle's
+2 -3
View File
@@ -178,11 +178,10 @@ impl ProsodyDetector {
min_lag, min_lag,
max_lag, max_lag,
self.voiced_threshold, self.voiced_threshold,
) { )
if voiced && f0 >= self.min_f0_hz && f0 <= self.max_f0_hz { && voiced && f0 >= self.min_f0_hz && f0 <= self.max_f0_hz {
voiced_f0s.push(f0); voiced_f0s.push(f0);
} }
}
total_frames += 1; total_frames += 1;
t += self.hop_samples; t += self.hop_samples;
} }
+1 -1
View File
@@ -516,7 +516,7 @@ pub fn vb_from_ckpt(
/// VCTK average power baseline used by the original to pre-condition /// VCTK average power baseline used by the original to pre-condition
/// audio energy before watermarking. Source: `server.py:60` constant. /// audio energy before watermarking. Source: `server.py:60` constant.
const AVERAGE_ENERGY_VCTK: f32 = 0.002837200844477648; const AVERAGE_ENERGY_VCTK: f32 = 0.002_837_200_9;
/// SilentCipher watermarker holding all three networks + an STFT /// SilentCipher watermarker holding all three networks + an STFT
/// helper. Loads from the three released `.ckpt` files (`enc_c`, /// helper. Loads from the three released `.ckpt` files (`enc_c`,
+1 -1
View File
@@ -290,7 +290,7 @@ impl SileroVad {
padded.extend_from_slice(samples); padded.extend_from_slice(samples);
// Tail-pad to whole-chunk multiple. // Tail-pad to whole-chunk multiple.
let extra = (CHUNK_SAMPLES - (samples.len() % CHUNK_SAMPLES)) % CHUNK_SAMPLES; let extra = (CHUNK_SAMPLES - (samples.len() % CHUNK_SAMPLES)) % CHUNK_SAMPLES;
padded.extend(std::iter::repeat(0.0).take(extra)); padded.extend(std::iter::repeat_n(0.0, extra));
let mut probs = Vec::new(); let mut probs = Vec::new();
let mut i = 0; let mut i = 0;
+3 -1
View File
@@ -385,7 +385,7 @@ impl<'a> Trainer<'a> {
self.generator.model.inner.refresh_lora(self.vm)?; self.generator.model.inner.refresh_lora(self.vm)?;
step += 1; step += 1;
if step % 10 == 0 || step == total_steps - 1 { if step.is_multiple_of(10) || step == total_steps - 1 {
tracing::info!( tracing::info!(
" step {step:>4}/{total_steps} epoch {epoch} ex {idx} frame {frame_idx} \ " step {step:>4}/{total_steps} epoch {epoch} ex {idx} frame {frame_idx} \
prompt_len={prompt_len} lr={lr:.2e} loss={loss_val:.4}" prompt_len={prompt_len} lr={lr:.2e} loss={loss_val:.4}"
@@ -407,9 +407,11 @@ impl<'a> Trainer<'a> {
/// - any other string matches `ex.stage == Some(label)` exactly /// - any other string matches `ex.stage == Some(label)` exactly
/// ///
/// Per `personal_voice_training_guide.md` §4 the canonical 3-stage recipe is: /// Per `personal_voice_training_guide.md` §4 the canonical 3-stage recipe is:
///
/// - audiobook: 3 epochs, peak_lr 1e-4 /// - audiobook: 3 epochs, peak_lr 1e-4
/// - podcast: 1 epoch, peak_lr 3e-5 /// - podcast: 1 epoch, peak_lr 3e-5
/// - va: 1 epoch, peak_lr 1e-5 /// - va: 1 epoch, peak_lr 1e-5
///
/// each over a growing pool. The `CurriculumTrainer` enforces the pool growth /// each over a growing pool. The `CurriculumTrainer` enforces the pool growth
/// implicitly by virtue of how you label your manifest (a podcast example /// implicitly by virtue of how you label your manifest (a podcast example
/// labeled `stage: "podcast"` is only matched by the podcast stage; if you /// labeled `stage: "podcast"` is only matched by the podcast stage; if you
+3 -4
View File
@@ -291,7 +291,7 @@ impl ConvPosEmbedding {
/// Even kernels need asymmetric handling: pad symmetrically by k/2 and /// Even kernels need asymmetric handling: pad symmetrically by k/2 and
/// trim the trailing extra frame. /// trim the trailing extra frame.
fn pad_for(kernel: usize) -> (usize, bool) { fn pad_for(kernel: usize) -> (usize, bool) {
if kernel % 2 == 0 { if kernel.is_multiple_of(2) {
(kernel / 2, true) (kernel / 2, true)
} else { } else {
((kernel - 1) / 2, false) ((kernel - 1) / 2, false)
@@ -595,15 +595,14 @@ pub fn ctc_greedy_decode(logits: &Tensor, vocab: &[&str]) -> CsmResult<String> {
if id_i == prev { if id_i == prev {
continue; continue;
} }
if (id as usize) != CTC_BLANK_ID { if (id as usize) != CTC_BLANK_ID
if let Some(tok) = vocab.get(id as usize) { && let Some(tok) = vocab.get(id as usize) {
if *tok == "|" { if *tok == "|" {
out.push(' '); out.push(' ');
} else if !tok.starts_with('<') { } else if !tok.starts_with('<') {
out.push_str(tok); out.push_str(tok);
} }
} }
}
prev = id_i; prev = id_i;
} }
Ok(out) Ok(out)
+1 -1
View File
@@ -232,7 +232,7 @@ impl Module for PosConv {
let h = xs_bct.apply(&self.conv)?; let h = xs_bct.apply(&self.conv)?;
// SamePad strips 1 trailing frame because kernel=128 is even. // SamePad strips 1 trailing frame because kernel=128 is even.
let out_len = h.dim(D::Minus1)?; let out_len = h.dim(D::Minus1)?;
let h = if self.kernel % 2 == 0 && out_len > in_len { let h = if self.kernel.is_multiple_of(2) && out_len > in_len {
h.narrow(D::Minus1, 0, in_len)? h.narrow(D::Minus1, 0, in_len)?
} else { } else {
h h
+1
View File
@@ -79,6 +79,7 @@ fn lev_align(r: &[&str], h: &[&str]) -> (usize, usize, usize) {
} }
let mut prev = vec![Cell::new(); m + 1]; let mut prev = vec![Cell::new(); m + 1];
let mut curr = vec![Cell::new(); m + 1]; let mut curr = vec![Cell::new(); m + 1];
#[allow(clippy::needless_range_loop)]
for j in 0..=m { for j in 0..=m {
prev[j] = Cell { cost: j, s: 0, d: 0, i: j }; prev[j] = Cell { cost: j, s: 0, d: 0, i: j };
} }