//! Classifier-Free Guidance (CFG) implementation for diffusion models //! //! Improves sample quality by amplifying the difference between conditional and unconditional predictions use crate::error::{DiffusionError, Result}; use rtx_tensor::{Device, Tensor}; /// Configuration for Classifier-Free Guidance #[derive(Debug, Clone)] pub struct CFGConfig { /// Main guidance scale (typically 7.5 for good results) pub guidance_scale: f32, /// Scale for unconditional model (usually 1.0) pub unconditional_guidance_scale: f32, /// Rescale factor to prevent oversaturation pub guidance_rescale: f32, /// Enable dynamic thresholding pub dynamic_thresholding: bool, /// Percentile for dynamic thresholding pub thresholding_percentile: f32, } impl Default for CFGConfig { fn default() -> Self { Self { guidance_scale: 7.5, unconditional_guidance_scale: 1.0, guidance_rescale: 0.0, dynamic_thresholding: false, thresholding_percentile: 0.995, } } } /// Guidance scale schedule types #[derive(Debug, Clone)] pub enum GuidanceScale { /// Constant scale throughout sampling Constant(f32), /// Linear interpolation from start to end Linear { start: f32, end: f32 }, /// Cosine schedule Cosine { start: f32, end: f32 }, } /// Classifier-Free Guidance for improved sample quality #[derive(Debug, Clone)] pub struct ClassifierFreeGuidance { config: CFGConfig, scale_schedule: GuidanceScale, per_channel_scales: Option>, device: Device, } impl ClassifierFreeGuidance { /// Create new CFG with configuration pub fn new(config: CFGConfig, device: &Device) -> Result { if config.guidance_scale < 0.0 { return Err(DiffusionError::Scheduler { message: "Guidance scale must be non-negative".to_string(), }); } let guidance_scale = config.guidance_scale; Ok(Self { config, scale_schedule: GuidanceScale::Constant(guidance_scale), per_channel_scales: None, device: device.clone(), }) } /// Create CFG with time-varying guidance scale pub fn with_schedule(scale_schedule: GuidanceScale, device: &Device) -> Result { let config = CFGConfig::default(); Ok(Self { config, scale_schedule, per_channel_scales: None, device: device.clone(), }) } /// Create CFG with per-channel guidance scales pub fn with_per_channel_scales(scales: Vec, device: &Device) -> Result { if scales.is_empty() { return Err(DiffusionError::Scheduler { message: "Per-channel scales cannot be empty".to_string(), }); } let config = CFGConfig::default(); let guidance_scale = config.guidance_scale; Ok(Self { config, scale_schedule: GuidanceScale::Constant(guidance_scale), per_channel_scales: Some(scales), device: device.clone(), }) } /// Apply classifier-free guidance pub fn apply(&self, conditional: &Tensor, unconditional: &Tensor) -> Result { self.apply_with_scale(conditional, unconditional, self.config.guidance_scale) } /// Apply CFG with specific timestep (for scheduled guidance) pub fn apply_with_timestep( &self, conditional: &Tensor, unconditional: &Tensor, timestep: f32, ) -> Result { let scale = self.get_scale_at_timestep(timestep); self.apply_with_scale(conditional, unconditional, scale) } /// Apply CFG with multiple conditional inputs pub fn apply_multi( &self, conditionals: &[&Tensor], unconditional: &Tensor, weights: &[f32], ) -> Result { if conditionals.len() != weights.len() { return Err(DiffusionError::Scheduler { message: "Number of conditionals must match number of weights".to_string(), }); } // Weighted combination of conditionals let mut combined = Tensor::zeros_like(conditionals[0])?; for (cond, &weight) in conditionals.iter().zip(weights.iter()) { combined = combined.add(&cond.mul_scalar(weight)?)?; } self.apply(&combined, unconditional) } /// Apply CFG with negative prompting pub fn apply_with_negative(&self, positive: &Tensor, negative: &Tensor) -> Result { // Use negative as the unconditional self.apply(positive, negative) } /// Internal: Apply CFG with specific scale fn apply_with_scale( &self, conditional: &Tensor, unconditional: &Tensor, scale: f32, ) -> Result { // CFG formula: output = unconditional + scale * (conditional - unconditional) let mut guided = if let Some(ref channel_scales) = self.per_channel_scales { self.apply_per_channel(conditional, unconditional, channel_scales)? } else { let diff = conditional.sub(unconditional)?; let scaled_diff = diff.mul_scalar(scale)?; unconditional.add(&scaled_diff)? }; // Apply guidance rescaling if enabled if self.config.guidance_rescale > 0.0 { guided = self.rescale_guidance(guided, conditional, unconditional)?; } // Apply dynamic thresholding if enabled if self.config.dynamic_thresholding { guided = self.apply_dynamic_thresholding(guided)?; } Ok(guided) } /// Apply per-channel guidance scales fn apply_per_channel( &self, conditional: &Tensor, unconditional: &Tensor, scales: &[f32], ) -> Result { let shape = conditional.shape().dims(); let channels = shape[1]; if scales.len() != channels { return Err(DiffusionError::Scheduler { message: format!("Expected {} scales, got {}", channels, scales.len()), }); } let mut guided_data = vec![0.0f32; conditional.numel()]; let cond_data = conditional.to_vec()?; let uncond_data = unconditional.to_vec()?; // Apply different scales to different channels let spatial_size = shape[2..].iter().product::(); for batch in 0..shape[0] { for c in 0..channels { let scale = scales[c]; let channel_offset = batch * channels * spatial_size + c * spatial_size; for i in 0..spatial_size { let idx = channel_offset + i; guided_data[idx] = uncond_data[idx] + scale * (cond_data[idx] - uncond_data[idx]); } } } Tensor::from_data(guided_data, shape.to_vec(), &self.device).map_err(DiffusionError::Tensor) } /// Rescale guidance to prevent oversaturation fn rescale_guidance( &self, guided: Tensor, conditional: &Tensor, unconditional: &Tensor, ) -> Result { // Compute standard deviations let guided_std = self.compute_std(&guided)?; let cond_std = self.compute_std(conditional)?; // Rescale factor let rescale_factor = self.config.guidance_rescale; let target_std = cond_std * rescale_factor + guided_std * (1.0 - rescale_factor); // Rescale guided to match target std let scale = target_std / (guided_std + 1e-8); guided.mul_scalar(scale).map_err(DiffusionError::Tensor) } /// Apply dynamic thresholding to prevent saturation fn apply_dynamic_thresholding(&self, guided: Tensor) -> Result { let percentile = self.config.thresholding_percentile; // Compute absolute values let abs_guided = guided.abs()?; // Find threshold at percentile let threshold = self.compute_percentile(&abs_guided, percentile)?; if threshold > 1.0 { // Clamp and rescale let clamped = guided.clamp(-threshold, threshold)?; clamped .div_scalar(threshold) .map_err(DiffusionError::Tensor) } else { Ok(guided) } } /// Get guidance scale at specific timestep fn get_scale_at_timestep(&self, timestep: f32) -> f32 { match &self.scale_schedule { GuidanceScale::Constant(scale) => *scale, GuidanceScale::Linear { start, end } => start + (end - start) * timestep, GuidanceScale::Cosine { start, end } => { let cos_val = ((timestep * std::f32::consts::PI).cos() + 1.0) / 2.0; start + (end - start) * (1.0 - cos_val) } } } /// Compute standard deviation of tensor fn compute_std(&self, tensor: &Tensor) -> Result { let data = tensor.to_vec()?; let mean = data.iter().sum::() / data.len() as f32; let variance = data.iter().map(|x| (x - mean).powi(2)).sum::() / data.len() as f32; Ok(variance.sqrt()) } /// Compute percentile of tensor values fn compute_percentile(&self, tensor: &Tensor, percentile: f32) -> Result { let mut data = tensor.to_vec()?; data.sort_by(|a, b| a.total_cmp(b)); let idx = ((data.len() - 1) as f32 * percentile) as usize; Ok(data[idx]) } // Getter methods for testing pub fn guidance_scale(&self) -> f32 { self.config.guidance_scale } pub fn uses_dynamic_thresholding(&self) -> bool { self.config.dynamic_thresholding } }