Files
rustytorch/crates/models/rtx-diffuse/src/guidance.rs
T
2026-03-04 00:08:42 +00:00

293 lines
9.6 KiB
Rust

//! 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<Vec<f32>>,
device: Device,
}
impl ClassifierFreeGuidance {
/// Create new CFG with configuration
pub fn new(config: CFGConfig, device: &Device) -> Result<Self> {
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<Self> {
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<f32>, device: &Device) -> Result<Self> {
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<Tensor> {
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<Tensor> {
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<Tensor> {
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<Tensor> {
// 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<Tensor> {
// 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<Tensor> {
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::<usize>();
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<Tensor> {
// 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<Tensor> {
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<f32> {
let data = tensor.to_vec()?;
let mean = data.iter().sum::<f32>() / data.len() as f32;
let variance = data.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / data.len() as f32;
Ok(variance.sqrt())
}
/// Compute percentile of tensor values
fn compute_percentile(&self, tensor: &Tensor, percentile: f32) -> Result<f32> {
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
}
}