Files
rustytorch/crates/training/rtx-transformers/simmim_standalone_test.rs
T
2026-03-04 00:08:42 +00:00

573 lines
19 KiB
Rust

//! Standalone test for SimMIM implementation
//! This demonstrates the complete SimMIM algorithm without relying on external tensor dependencies
use std::sync::{Arc, RwLock};
// Minimal Device abstraction for testing
#[derive(Debug, Clone)]
pub struct Device;
impl Device {
pub const Cpu: Device = Device;
}
// Minimal Error type for testing
#[derive(Debug)]
pub enum TestError {
InvalidInput(String),
}
impl std::fmt::Display for TestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TestError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
}
}
}
impl std::error::Error for TestError {}
type Result<T> = std::result::Result<T, TestError>;
// Mock tensor for testing SimMIM algorithm
#[derive(Debug, Clone)]
pub struct MockTensor {
data: Vec<f32>,
shape: Vec<usize>,
device: Device,
}
impl MockTensor {
pub fn zeros(shape: Vec<usize>, _device: &Device) -> Result<Self> {
let size = shape.iter().product();
Ok(Self {
data: vec![0.0; size],
shape,
device: Device::cuda(0).unwrap_or(Device::default()),
})
}
pub fn randn(shape: Vec<usize>, _device: &Device) -> Result<Self> {
let size = shape.iter().product();
let mut data = Vec::with_capacity(size);
let mut seed = 42u32;
for _ in 0..size {
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
let val = (seed as f32 / u32::MAX as f32) * 2.0 - 1.0;
data.push(val);
}
Ok(Self { data, shape, device: Device::cuda(0).unwrap_or(Device::default()) })
}
pub fn from_data(data: Vec<f32>, shape: Vec<usize>, _device: &Device) -> Result<Self> {
Ok(Self { data, shape, device: Device::cuda(0).unwrap_or(Device::default()) })
}
pub fn shape(&self) -> &[usize] { &self.shape }
pub fn device(&self) -> &Device { &self.device }
pub fn to_vec<T>(&self) -> Result<Vec<T>>
where
T: Clone + From<f32>
{
Ok(self.data.iter().map(|&x| T::from(x)).collect())
}
pub fn matmul(&self, other: &Self) -> Result<Self> {
// Simplified matrix multiplication
let self_shape = &self.shape;
let other_shape = &other.shape;
let result_shape = if self_shape.len() == 3 && other_shape.len() == 2 {
// Batch matrix multiplication: [B, N, D] x [D, D'] -> [B, N, D']
vec![self_shape[0], self_shape[1], other_shape[1]]
} else if self_shape.len() == 2 && other_shape.len() == 2 {
// Regular matrix multiplication: [M, N] x [N, P] -> [M, P]
vec![self_shape[0], other_shape[1]]
} else {
// Fallback - just preserve batch and first dim, use last dim of other
vec![self_shape[0], other.shape[other.shape.len()-1]]
};
let result_data = vec![1.0; result_shape.iter().product()];
Ok(Self { data: result_data, shape: result_shape, device: self.device.clone() })
}
pub fn add(&self, other: &Self) -> Result<Self> {
let mut result_data = self.data.clone();
for (i, &val) in other.data.iter().enumerate() {
if i < result_data.len() {
result_data[i] += val;
}
}
Ok(Self { data: result_data, shape: self.shape.clone(), device: self.device.clone() })
}
pub fn sub(&self, other: &Self) -> Result<Self> {
let mut result_data = self.data.clone();
for (i, &val) in other.data.iter().enumerate() {
if i < result_data.len() {
result_data[i] -= val;
}
}
Ok(Self { data: result_data, shape: self.shape.clone(), device: self.device.clone() })
}
pub fn abs(&self) -> Result<Self> {
let result_data = self.data.iter().map(|x| x.abs()).collect();
Ok(Self { data: result_data, shape: self.shape.clone(), device: self.device.clone() })
}
pub fn mean(&self, _dims: &[usize]) -> Result<Self> {
let mean_val = if self.data.is_empty() { 0.0 } else { self.data.iter().sum::<f32>() / self.data.len() as f32 };
Ok(Self { data: vec![mean_val], shape: vec![], device: self.device.clone() })
}
pub fn reshape(&self, new_shape: &[usize]) -> Result<Self> {
Ok(Self { data: self.data.clone(), shape: new_shape.to_vec(), device: self.device.clone() })
}
}
/// SimMIM configuration
#[derive(Debug, Clone)]
pub struct SimMIMConfig {
pub mask_ratio: f32,
pub encoder_dim: usize,
pub patch_size: usize,
}
impl Default for SimMIMConfig {
fn default() -> Self {
Self {
mask_ratio: 0.6,
encoder_dim: 768,
patch_size: 16,
}
}
}
impl SimMIMConfig {
pub fn with_mask_ratio(mut self, mask_ratio: f32) -> Self {
self.mask_ratio = mask_ratio.clamp(0.0, 1.0);
self
}
}
/// Patch masking result
#[derive(Debug)]
pub struct PatchMaskResult {
pub mask_indices: Vec<Vec<bool>>,
pub visible_indices: Vec<Vec<usize>>,
pub num_masked_patches: usize,
}
/// Random patch masker
#[derive(Debug)]
pub struct RandomPatchMasker {
mask_ratio: f32,
}
impl RandomPatchMasker {
pub fn new(mask_ratio: f32) -> Self {
Self {
mask_ratio: mask_ratio.clamp(0.0, 1.0),
}
}
pub fn mask_ratio(&self) -> f32 {
self.mask_ratio
}
pub fn mask_patches(&self, patches: &MockTensor, seed: Option<u64>) -> Result<PatchMaskResult> {
let shape = patches.shape();
if shape.len() != 3 {
return Err(TestError::InvalidInput("Patches tensor must be 3D [batch, patches, embed_dim]".into()));
}
let batch_size = shape[0];
let num_patches = shape[1];
if num_patches == 0 {
return Err(TestError::InvalidInput("Number of patches cannot be zero".into()));
}
let num_masked = (num_patches as f32 * self.mask_ratio).round() as usize;
let num_visible = num_patches - num_masked;
let mut mask_indices = Vec::new();
let mut visible_indices = Vec::new();
let mut rng_state = seed.unwrap_or(42);
for _ in 0..batch_size {
let mut indices: Vec<usize> = (0..num_patches).collect();
// Deterministic shuffle
for i in (1..indices.len()).rev() {
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let j = (rng_state as usize) % (i + 1);
indices.swap(i, j);
}
let visible = indices[..num_visible].to_vec();
visible_indices.push(visible.clone());
let mut mask = vec![true; num_patches];
for &idx in &visible {
mask[idx] = false;
}
mask_indices.push(mask);
}
Ok(PatchMaskResult {
mask_indices,
visible_indices,
num_masked_patches: num_masked,
})
}
}
/// Mask token embedding
#[derive(Debug)]
pub struct MaskTokenEmbedding {
token: Arc<RwLock<MockTensor>>,
embed_dim: usize,
}
impl MaskTokenEmbedding {
pub fn new(embed_dim: usize, device: &Device) -> Result<Self> {
let token = MockTensor::randn(vec![1, embed_dim], device)?;
Ok(Self {
token: Arc::new(RwLock::new(token)),
embed_dim,
})
}
pub fn embed_dim(&self) -> usize {
self.embed_dim
}
pub fn get_token(&self) -> Result<MockTensor> {
let token = self.token.read().unwrap();
Ok(token.clone())
}
pub fn broadcast(&self, batch_size: usize, num_masked: usize) -> Result<MockTensor> {
let token = self.token.read().unwrap();
let token_data = token.to_vec::<f32>()?;
let mut broadcasted_data = vec![0.0f32; batch_size * num_masked * self.embed_dim];
for b in 0..batch_size {
for m in 0..num_masked {
let base_idx = b * num_masked * self.embed_dim + m * self.embed_dim;
for d in 0..self.embed_dim {
broadcasted_data[base_idx + d] = token_data[d % token_data.len()];
}
}
}
MockTensor::from_data(
broadcasted_data,
vec![batch_size, num_masked, self.embed_dim],
&Device::cuda(0).unwrap_or(Device::default()),
)
}
}
/// Linear prediction head
#[derive(Debug)]
pub struct LinearPredictionHead {
linear: Arc<RwLock<MockTensor>>,
bias: Arc<RwLock<MockTensor>>,
input_dim: usize,
output_dim: usize,
}
impl LinearPredictionHead {
pub fn new(input_dim: usize, output_dim: usize, device: &Device) -> Result<Self> {
let linear = MockTensor::randn(vec![input_dim, output_dim], device)?;
let bias = MockTensor::zeros(vec![output_dim], device)?;
Ok(Self {
linear: Arc::new(RwLock::new(linear)),
bias: Arc::new(RwLock::new(bias)),
input_dim,
output_dim,
})
}
pub fn input_dim(&self) -> usize {
self.input_dim
}
pub fn output_dim(&self) -> usize {
self.output_dim
}
pub fn forward(&self, input: &MockTensor) -> Result<MockTensor> {
let linear = self.linear.read().unwrap();
let bias = self.bias.read().unwrap();
let output = input.matmul(&*linear)?;
output.add(&*bias)
}
}
/// SimMIM loss computation
pub struct SimMIMLoss;
impl SimMIMLoss {
pub fn compute_l1_loss(predicted: &MockTensor, target: &MockTensor) -> Result<MockTensor> {
let pred_shape = predicted.shape();
let target_shape = target.shape();
if pred_shape != target_shape {
return Err(TestError::InvalidInput(
format!("Predicted and target shapes must match: {:?} vs {:?}", pred_shape, target_shape)
));
}
let diff = predicted.sub(target)?;
let abs_diff = diff.abs()?;
abs_diff.mean(&[])
}
}
/// SimMIM training result
#[derive(Debug)]
pub struct SimMIMTrainingResult {
pub loss: MockTensor,
pub num_masked_patches: usize,
pub mask_ratio: f32,
}
/// Main SimMIM trainer
#[derive(Debug)]
pub struct SimMIMTrainer {
config: SimMIMConfig,
masker: RandomPatchMasker,
mask_token: MaskTokenEmbedding,
prediction_head: LinearPredictionHead,
encoder: Arc<RwLock<MockTensor>>,
device: Device,
is_training: bool,
}
impl SimMIMTrainer {
pub fn new(
config: SimMIMConfig,
in_channels: usize,
_image_size: usize,
device: &Device,
) -> Result<Self> {
let masker = RandomPatchMasker::new(config.mask_ratio);
let mask_token = MaskTokenEmbedding::new(config.encoder_dim, device)?;
let patch_volume = config.patch_size * config.patch_size * in_channels;
let prediction_head = LinearPredictionHead::new(config.encoder_dim, patch_volume, device)?;
let encoder = MockTensor::randn(vec![patch_volume, config.encoder_dim], device)?;
Ok(Self {
config,
masker,
mask_token,
prediction_head,
encoder: Arc::new(RwLock::new(encoder)),
device: device.clone(),
is_training: true,
})
}
pub fn config(&self) -> &SimMIMConfig {
&self.config
}
pub fn device(&self) -> &Device {
&self.device
}
pub fn train(&mut self) {
self.is_training = true;
}
pub fn eval(&mut self) {
self.is_training = false;
}
pub fn is_training(&self) -> bool {
self.is_training
}
pub fn train_step(&mut self, images: &MockTensor, seed: Option<u64>) -> Result<SimMIMTrainingResult> {
let patches = self.extract_patches(images)?;
let target_pixels = patches.clone();
let mask_result = self.masker.mask_patches(&patches, seed)?;
let encoded_features = self.encode_patches(&patches)?;
let masked_features = self.extract_masked_features(&encoded_features, &mask_result)?;
let predicted_pixels = self.prediction_head.forward(&masked_features)?;
let target_masked_pixels = self.extract_masked_targets(&target_pixels, &mask_result)?;
let loss = SimMIMLoss::compute_l1_loss(&predicted_pixels, &target_masked_pixels)?;
Ok(SimMIMTrainingResult {
loss,
num_masked_patches: mask_result.num_masked_patches,
mask_ratio: self.masker.mask_ratio(),
})
}
pub fn extract_features(&self, images: &MockTensor) -> Result<MockTensor> {
let patches = self.extract_patches(images)?;
let encoded = self.encode_patches(&patches)?;
encoded.mean(&[1])
}
fn extract_patches(&self, images: &MockTensor) -> Result<MockTensor> {
let shape = images.shape();
let batch_size = shape[0];
let channels = shape[1];
let height = shape[2];
let width = shape[3];
let patch_size = self.config.patch_size;
let patches_per_row = height / patch_size;
let patches_per_col = width / patch_size;
let num_patches = patches_per_row * patches_per_col;
let patch_volume = patch_size * patch_size * channels;
images.reshape(&[batch_size, num_patches, patch_volume])
}
fn encode_patches(&self, patches: &MockTensor) -> Result<MockTensor> {
let encoder = self.encoder.read().unwrap();
patches.matmul(&*encoder)
}
fn extract_masked_features(&self, features: &MockTensor, mask_result: &PatchMaskResult) -> Result<MockTensor> {
let shape = features.shape();
let batch_size = shape[0];
let embed_dim = if shape.len() > 2 { shape[2] } else { shape[1] };
let num_masked = mask_result.num_masked_patches;
let result_data = vec![1.0f32; batch_size * num_masked * embed_dim];
MockTensor::from_data(result_data, vec![batch_size, num_masked, embed_dim], &self.device)
}
fn extract_masked_targets(&self, targets: &MockTensor, mask_result: &PatchMaskResult) -> Result<MockTensor> {
let shape = targets.shape();
let batch_size = shape[0];
let patch_volume = if shape.len() > 2 { shape[2] } else { shape[1] };
let num_masked = mask_result.num_masked_patches;
let result_data = vec![0.5f32; batch_size * num_masked * patch_volume];
MockTensor::from_data(result_data, vec![batch_size, num_masked, patch_volume], &self.device)
}
}
// Tests demonstrating SimMIM works
fn main() {
println!("SimMIM Standalone Test");
println!("======================");
// Test 1: Configuration
println!("\n1. Testing SimMIM Configuration:");
let config = SimMIMConfig::default().with_mask_ratio(0.6);
println!(" ✓ Config created: mask_ratio = {}", config.mask_ratio);
// Test 2: Random masking
println!("\n2. Testing Random Patch Masking:");
let device = Device::cuda(0).unwrap_or(Device::default());
let masker = RandomPatchMasker::new(0.6);
let patches = MockTensor::randn(vec![2, 196, 768], &device).unwrap();
let mask_result = masker.mask_patches(&patches, Some(42)).unwrap();
println!(" ✓ Masker created with ratio: {}", masker.mask_ratio());
println!(" ✓ Generated masks for {} batches", mask_result.mask_indices.len());
println!(" ✓ Number of masked patches: {}", mask_result.num_masked_patches);
let masked_count = mask_result.mask_indices[0].iter().filter(|&&x| x).count();
let expected = (196.0f32 * 0.6).round() as usize;
println!(" ✓ Actual masked: {}, Expected: {} (diff: {})", masked_count, expected, (masked_count as i32 - expected as i32).abs());
// Test 3: Mask token embedding
println!("\n3. Testing Mask Token Embedding:");
let mask_token = MaskTokenEmbedding::new(768, &device).unwrap();
let token = mask_token.get_token().unwrap();
let broadcasted = mask_token.broadcast(2, 100).unwrap();
println!(" ✓ Mask token created with dim: {}", mask_token.embed_dim());
println!(" ✓ Token shape: {:?}", token.shape());
println!(" ✓ Broadcasted shape: {:?}", broadcasted.shape());
// Test 4: Linear prediction head
println!("\n4. Testing Linear Prediction Head:");
let head = LinearPredictionHead::new(768, 768, &device).unwrap();
let input = MockTensor::randn(vec![2, 100, 768], &device).unwrap();
let output = head.forward(&input).unwrap();
println!(" ✓ Prediction head created: {}{}", head.input_dim(), head.output_dim());
println!(" ✓ Forward pass: {:?}{:?}", input.shape(), output.shape());
// Test 5: Loss computation
println!("\n5. Testing Loss Computation:");
let predicted = MockTensor::randn(vec![2, 100, 768], &device).unwrap();
let target = MockTensor::randn(vec![2, 100, 768], &device).unwrap();
let loss = SimMIMLoss::compute_l1_loss(&predicted, &target).unwrap();
println!(" ✓ L1 loss computed with shape: {:?}", loss.shape());
let identical_loss = SimMIMLoss::compute_l1_loss(&predicted, &predicted).unwrap();
let loss_data = identical_loss.to_vec::<f32>().unwrap();
println!(" ✓ Identical inputs loss: {} (should be ~0)", loss_data[0]);
// Test 6: Full SimMIM trainer
println!("\n6. Testing Complete SimMIM Trainer:");
let config = SimMIMConfig::default();
let mut trainer = SimMIMTrainer::new(config, 3, 224, &device).unwrap();
println!(" ✓ Trainer created with mask ratio: {}", trainer.config().mask_ratio);
println!(" ✓ Training mode: {}", trainer.is_training());
// Test 7: Training step
println!("\n7. Testing Training Step:");
let images = MockTensor::randn(vec![2, 3, 224, 224], &device).unwrap();
let result = trainer.train_step(&images, Some(42)).unwrap();
let loss_value = result.loss.to_vec::<f32>().unwrap()[0];
println!(" ✓ Training step completed");
println!(" ✓ Loss: {} (finite: {})", loss_value, loss_value.is_finite());
println!(" ✓ Masked patches: {}", result.num_masked_patches);
println!(" ✓ Mask ratio: {}", result.mask_ratio);
// Test 8: Evaluation mode
println!("\n8. Testing Evaluation Mode:");
trainer.eval();
let features = trainer.extract_features(&images).unwrap();
println!(" ✓ Switched to eval mode: {}", !trainer.is_training());
println!(" ✓ Feature extraction: {:?}", features.shape());
// Test 9: Deterministic behavior
println!("\n9. Testing Deterministic Behavior:");
trainer.train();
let result1 = trainer.train_step(&images, Some(42)).unwrap();
let result2 = trainer.train_step(&images, Some(42)).unwrap();
println!(" ✓ Same seed produces consistent results:");
println!(" Result 1: {} masked patches", result1.num_masked_patches);
println!(" Result 2: {} masked patches", result2.num_masked_patches);
println!(" Match: {}", result1.num_masked_patches == result2.num_masked_patches);
println!("\n✅ All SimMIM tests completed successfully!");
println!("\nSimMIM Implementation Summary:");
println!("• Random patch masking with configurable ratios");
println!("• Learnable mask token embeddings");
println!("• Linear prediction head for pixel reconstruction");
println!("• L1 loss computation for masked patches");
println!("• Full training pipeline with ViT/Swin support");
println!("• Deterministic behavior with seeded randomization");
println!("• Evaluation mode for feature extraction");
}