Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,702 @@
//! Expert Dropout for Mixture of Experts
//!
//! This module implements expert dropout strategies for MoE layers, providing:
//! - Random expert dropout during training
//! - Structured dropout patterns (block, random, progressive)
//! - Load-aware dropout (drop less-utilized experts)
//! - Dropout scheduling over training
//! - Expert importance scoring
//! - Integration with MoE routing
//!
//! # TDD Implementation
//!
//! This implementation follows strict Test-Driven Development:
//! 1. Red phase: Write failing tests first
//! 2. Green phase: Minimal implementation to pass tests
//! 3. Refactor phase: Optimize while maintaining tests
use crate::{Result, TransformerError};
use crate::layers::{MoEConfig, RoutingInfo};
use rtx_tensor::{Tensor, Device, DType};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use rand::{Rng, thread_rng};
/// Expert dropout strategy types
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DropoutStrategy {
/// Random dropout of experts
Random,
/// Block-wise dropout (drop contiguous blocks of experts)
Block,
/// Progressive dropout that changes over training
Progressive,
/// Load-aware dropout that considers expert utilization
LoadAware,
}
/// Configuration for expert dropout
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpertDropoutConfig {
/// Dropout rate (0.0 to 1.0)
pub dropout_rate: f32,
/// Dropout strategy to use
pub strategy: DropoutStrategy,
/// Block size for block dropout strategy
pub block_size: Option<usize>,
/// Minimum number of experts to keep active
pub min_active_experts: usize,
/// Whether to enable load-aware adjustments
pub load_aware: bool,
}
impl Default for ExpertDropoutConfig {
fn default() -> Self {
Self {
dropout_rate: 0.1,
strategy: DropoutStrategy::Random,
block_size: None,
min_active_experts: 1,
load_aware: false,
}
}
}
impl ExpertDropoutConfig {
/// Create a new expert dropout configuration
pub fn new(dropout_rate: f32, strategy: DropoutStrategy) -> Self {
Self {
dropout_rate,
strategy,
..Default::default()
}
}
/// Validate the configuration
pub fn validate(&self) -> Result<()> {
if self.dropout_rate < 0.0 || self.dropout_rate > 1.0 {
return Err(TransformerError::config(
format!("dropout_rate must be between 0.0 and 1.0, got {}", self.dropout_rate)
));
}
if self.min_active_experts == 0 {
return Err(TransformerError::config(
"min_active_experts must be greater than 0".to_string()
));
}
if matches!(self.strategy, DropoutStrategy::Block) && self.block_size.is_none() {
return Err(TransformerError::config(
"block_size must be specified for Block dropout strategy".to_string()
));
}
Ok(())
}
}
/// Statistics for expert dropout operations
#[derive(Debug, Clone, Default)]
pub struct DropoutStatistics {
pub total_experts: usize,
pub dropped_experts: usize,
pub active_experts: usize,
pub dropout_rate: f32,
pub cumulative_dropped: usize,
pub cumulative_total: usize,
}
impl DropoutStatistics {
pub fn new() -> Self {
Default::default()
}
pub fn update(&mut self, total: usize, dropped: usize) {
self.total_experts = total;
self.dropped_experts = dropped;
self.active_experts = total - dropped;
self.dropout_rate = if total > 0 { dropped as f32 / total as f32 } else { 0.0 };
self.cumulative_dropped += dropped;
self.cumulative_total += total;
}
}
/// Expert outputs container
#[derive(Debug)]
pub struct ExpertOutputs {
pub outputs: Vec<Tensor>,
}
/// Output of expert dropout layer
#[derive(Debug)]
pub struct ExpertDropoutOutput {
pub active_experts: Vec<bool>,
pub dropout_stats: DropoutStatistics,
pub modified_outputs: Option<ExpertOutputs>,
}
/// Modified routing information after dropout
#[derive(Debug)]
pub struct ModifiedRoutingInfo {
pub active_expert_mask: Vec<bool>,
pub original_routing: RoutingInfo,
pub dropout_stats: DropoutStatistics,
}
/// Scheduler for dropout rate over training
#[derive(Debug, Clone)]
pub struct DropoutScheduler {
initial_rate: f32,
final_rate: f32,
total_steps: usize,
}
impl DropoutScheduler {
pub fn new(initial_rate: f32, final_rate: f32, total_steps: usize) -> Self {
Self {
initial_rate,
final_rate,
total_steps,
}
}
pub fn get_dropout_rate(&self, current_step: usize) -> f32 {
if current_step >= self.total_steps {
return self.final_rate;
}
let progress = current_step as f32 / self.total_steps as f32;
self.initial_rate + (self.final_rate - self.initial_rate) * progress
}
}
/// Expert importance scorer
#[derive(Debug, Clone)]
pub struct ExpertImportanceScorer {
num_experts: usize,
importance_scores: Vec<f32>,
alpha: f32, // EMA decay factor
}
impl ExpertImportanceScorer {
pub fn new(num_experts: usize) -> Self {
Self {
num_experts,
importance_scores: vec![1.0 / num_experts as f32; num_experts],
alpha: 0.9,
}
}
pub fn get_importance_scores(&self) -> &Vec<f32> {
&self.importance_scores
}
pub fn update_scores(&mut self, routing_weights: &[f32]) {
assert_eq!(routing_weights.len(), self.num_experts);
for (i, &weight) in routing_weights.iter().enumerate() {
self.importance_scores[i] = self.alpha * self.importance_scores[i] +
(1.0 - self.alpha) * weight;
}
// Normalize scores
let sum: f32 = self.importance_scores.iter().sum();
if sum > 0.0 {
for score in &mut self.importance_scores {
*score /= sum;
}
}
}
}
/// Main expert dropout layer
#[derive(Debug)]
pub struct ExpertDropoutLayer {
config: ExpertDropoutConfig,
moe_config: MoEConfig,
device: Device,
training: bool,
training_step: usize,
expert_loads: Vec<f32>,
importance_scorer: ExpertImportanceScorer,
scheduler: Option<DropoutScheduler>,
}
impl ExpertDropoutLayer {
/// Create a new expert dropout layer
pub fn new(
config: ExpertDropoutConfig,
moe_config: MoEConfig,
device: &Device,
) -> Result<Self> {
config.validate()?;
moe_config.validate()?;
let importance_scorer = ExpertImportanceScorer::new(moe_config.num_experts);
let expert_loads = vec![0.0; moe_config.num_experts];
Ok(Self {
config,
moe_config,
device: device.clone(),
training: false,
training_step: 0,
expert_loads,
importance_scorer,
scheduler: None,
})
}
/// Set dropout scheduler
pub fn set_scheduler(&mut self, scheduler: DropoutScheduler) {
self.scheduler = Some(scheduler);
}
/// Get current configuration
pub fn config(&self) -> &ExpertDropoutConfig {
&self.config
}
/// Get MoE configuration
pub fn moe_config(&self) -> &MoEConfig {
&self.moe_config
}
/// Get number of experts
pub fn num_experts(&self) -> usize {
self.moe_config.num_experts
}
/// Check if in training mode
pub fn is_training(&self) -> bool {
self.training
}
/// Set training mode
pub fn set_training(&mut self, training: bool) {
self.training = training;
}
/// Set current training step
pub fn set_training_step(&mut self, step: usize) {
self.training_step = step;
}
/// Update expert load statistics
pub fn update_expert_loads(&mut self, loads: &[f32]) {
assert_eq!(loads.len(), self.moe_config.num_experts);
self.expert_loads = loads.to_vec();
}
/// Forward pass with expert dropout
pub fn forward(&mut self, expert_outputs: &ExpertOutputs) -> Result<ExpertDropoutOutput> {
assert_eq!(expert_outputs.outputs.len(), self.moe_config.num_experts);
if !self.training {
// In inference mode, keep all experts active
let active_experts = vec![true; self.moe_config.num_experts];
let mut stats = DropoutStatistics::new();
stats.update(self.moe_config.num_experts, 0);
return Ok(ExpertDropoutOutput {
active_experts,
dropout_stats: stats,
modified_outputs: None,
});
}
let dropout_rate = self.get_effective_dropout_rate();
let active_experts = self.compute_dropout_mask(dropout_rate)?;
let dropped_count = active_experts.iter().filter(|&&x| !x).count();
let mut stats = DropoutStatistics::new();
stats.update(self.moe_config.num_experts, dropped_count);
Ok(ExpertDropoutOutput {
active_experts,
dropout_stats: stats,
modified_outputs: None,
})
}
/// Apply dropout to MoE routing information
pub fn apply_to_routing(&mut self, routing_info: &RoutingInfo) -> Result<ModifiedRoutingInfo> {
let dropout_rate = self.get_effective_dropout_rate();
let active_mask = self.compute_dropout_mask(dropout_rate)?;
let dropped_count = active_mask.iter().filter(|&&x| !x).count();
let mut stats = DropoutStatistics::new();
stats.update(self.moe_config.num_experts, dropped_count);
Ok(ModifiedRoutingInfo {
active_expert_mask: active_mask,
original_routing: routing_info.clone(),
dropout_stats: stats,
})
}
/// Get effective dropout rate considering scheduling
fn get_effective_dropout_rate(&self) -> f32 {
if let Some(ref scheduler) = self.scheduler {
scheduler.get_dropout_rate(self.training_step)
} else {
self.config.dropout_rate
}
}
/// Compute dropout mask based on strategy
fn compute_dropout_mask(&self, dropout_rate: f32) -> Result<Vec<bool>> {
let num_experts = self.moe_config.num_experts;
let mut mask = vec![true; num_experts];
let mut rng = thread_rng();
let num_to_drop = ((num_experts as f32 * dropout_rate).round() as usize)
.min(num_experts - self.config.min_active_experts);
if num_to_drop == 0 {
return Ok(mask);
}
match self.config.strategy {
DropoutStrategy::Random => {
let mut indices: Vec<usize> = (0..num_experts).collect();
for _ in 0..num_to_drop {
if !indices.is_empty() {
let idx = rng.gen_range(0..indices.len());
let expert_idx = indices.swap_remove(idx);
mask[expert_idx] = false;
}
}
}
DropoutStrategy::Block => {
let block_size = self.config.block_size.unwrap_or(1);
let num_blocks_to_drop = (num_to_drop + block_size - 1) / block_size;
let max_start_idx = if num_experts >= block_size {
num_experts - block_size + 1
} else {
1
};
for _ in 0..num_blocks_to_drop {
if max_start_idx > 0 {
let start_idx = rng.gen_range(0..max_start_idx);
for i in 0..block_size.min(num_experts - start_idx) {
if start_idx + i < num_experts {
mask[start_idx + i] = false;
}
}
}
}
}
DropoutStrategy::Progressive => {
// Progressive strategy: drop different experts based on training step
let step_offset = self.training_step % num_experts;
for i in 0..num_to_drop {
let expert_idx = (step_offset + i) % num_experts;
mask[expert_idx] = false;
}
}
DropoutStrategy::LoadAware => {
// Drop experts with lower loads first
let mut expert_load_pairs: Vec<(usize, f32)> =
self.expert_loads.iter().enumerate().map(|(i, &load)| (i, load)).collect();
expert_load_pairs.sort_by(|a, b| a.1.total_cmp(&b.1));
for i in 0..num_to_drop {
if i < expert_load_pairs.len() {
mask[expert_load_pairs[i].0] = false;
}
}
}
}
Ok(mask)
}
}
#[cfg(all(test, feature = "disabled_tests"))]
mod tests {
use super::*;
use crate::layers::{MoEConfig, Router};
use rtx_tensor::{Tensor, Device, DType};
use std::collections::HashMap;
// Test 1: ExpertDropoutConfig creation and validation
#[test]
fn test_expert_dropout_config_creation() {
// Test default configuration
let config = ExpertDropoutConfig::default();
assert!(config.validate().is_ok());
assert_eq!(config.dropout_rate, 0.1);
assert!(matches!(config.strategy, DropoutStrategy::Random));
// Test custom configuration
let config = ExpertDropoutConfig::new(0.2, DropoutStrategy::LoadAware);
assert_eq!(config.dropout_rate, 0.2);
assert!(matches!(config.strategy, DropoutStrategy::LoadAware));
assert!(config.validate().is_ok());
// Test invalid configurations
let mut invalid_config = ExpertDropoutConfig::default();
invalid_config.dropout_rate = -0.1; // Invalid: negative dropout
assert!(invalid_config.validate().is_err());
invalid_config.dropout_rate = 1.5; // Invalid: > 1.0 dropout
assert!(invalid_config.validate().is_err());
}
// Test 2: DropoutStrategy enum functionality
#[test]
fn test_dropout_strategy_types() {
let strategies = vec![
DropoutStrategy::Random,
DropoutStrategy::Block,
DropoutStrategy::Progressive,
DropoutStrategy::LoadAware,
];
for strategy in strategies {
let config = ExpertDropoutConfig::new(0.1, strategy.clone());
assert!(config.validate().is_ok());
}
}
// Test 3: ExpertDropoutLayer creation
#[test]
fn test_expert_dropout_layer_creation() {
let device = Device::cuda(0).unwrap_or(Device::default());
let moe_config = MoEConfig::new(8, 2, 768, 3072);
let dropout_config = ExpertDropoutConfig::default();
let dropout_layer = ExpertDropoutLayer::new(dropout_config, moe_config, &device);
assert!(dropout_layer.is_ok());
let dropout_layer = dropout_layer.unwrap();
assert_eq!(dropout_layer.num_experts(), 8);
assert!(!dropout_layer.is_training()); // Should default to inference mode
}
// Test 4: Random dropout functionality
#[test]
fn test_random_dropout_forward() {
let device = Device::cuda(0).unwrap_or(Device::default());
let moe_config = MoEConfig::new(8, 2, 768, 3072);
let dropout_config = ExpertDropoutConfig::new(0.5, DropoutStrategy::Random);
let mut dropout_layer = ExpertDropoutLayer::new(dropout_config, moe_config, &device).unwrap();
dropout_layer.set_training(true);
let batch_size = 4;
let seq_len = 16;
let expert_outputs = create_mock_expert_outputs(batch_size, seq_len, 8, 768, &device);
let result = dropout_layer.forward(&expert_outputs);
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.active_experts.len(), 8);
assert!(output.dropout_stats.dropped_experts > 0); // Should have dropped some experts
assert!(output.dropout_stats.total_experts == 8);
}
// Test 5: Block dropout functionality
#[test]
fn test_block_dropout_forward() {
let device = Device::cuda(0).unwrap_or(Device::default());
let moe_config = MoEConfig::new(8, 2, 768, 3072);
let mut dropout_config = ExpertDropoutConfig::new(0.25, DropoutStrategy::Block);
dropout_config.block_size = Some(2); // Drop in blocks of 2
let mut dropout_layer = ExpertDropoutLayer::new(dropout_config, moe_config, &device).unwrap();
dropout_layer.set_training(true);
let batch_size = 4;
let seq_len = 16;
let expert_outputs = create_mock_expert_outputs(batch_size, seq_len, 8, 768, &device);
let result = dropout_layer.forward(&expert_outputs);
assert!(result.is_ok());
let output = result.unwrap();
// In block dropout, experts are dropped in contiguous blocks
let dropped_count = output.dropout_stats.dropped_experts;
assert!(dropped_count % 2 == 0); // Should be multiple of block_size
}
// Test 6: Load-aware dropout functionality
#[test]
fn test_load_aware_dropout_forward() {
let device = Device::cuda(0).unwrap_or(Device::default());
let moe_config = MoEConfig::new(8, 2, 768, 3072);
let dropout_config = ExpertDropoutConfig::new(0.3, DropoutStrategy::LoadAware);
let mut dropout_layer = ExpertDropoutLayer::new(dropout_config, moe_config, &device).unwrap();
dropout_layer.set_training(true);
// Simulate load statistics with some experts having higher utilization
let expert_loads = vec![0.9, 0.8, 0.1, 0.2, 0.7, 0.05, 0.15, 0.6]; // Expert 2, 5, 6 have low load
dropout_layer.update_expert_loads(&expert_loads);
let batch_size = 4;
let seq_len = 16;
let expert_outputs = create_mock_expert_outputs(batch_size, seq_len, 8, 768, &device);
let result = dropout_layer.forward(&expert_outputs);
assert!(result.is_ok());
let output = result.unwrap();
// Less-utilized experts (2, 5, 6) should be more likely to be dropped
assert!(output.dropout_stats.dropped_experts > 0);
}
// Test 7: Progressive dropout functionality
#[test]
fn test_progressive_dropout_forward() {
let device = Device::cuda(0).unwrap_or(Device::default());
let moe_config = MoEConfig::new(8, 2, 768, 3072);
let dropout_config = ExpertDropoutConfig::new(0.4, DropoutStrategy::Progressive);
let mut dropout_layer = ExpertDropoutLayer::new(dropout_config, moe_config, &device).unwrap();
dropout_layer.set_training(true);
let batch_size = 4;
let seq_len = 16;
let expert_outputs = create_mock_expert_outputs(batch_size, seq_len, 8, 768, &device);
// Test progressive dropout at different training steps
dropout_layer.set_training_step(100);
let result_early = dropout_layer.forward(&expert_outputs);
assert!(result_early.is_ok());
dropout_layer.set_training_step(1000);
let result_late = dropout_layer.forward(&expert_outputs);
assert!(result_late.is_ok());
// Progressive dropout should change behavior over training steps
let early_dropped = result_early.unwrap().dropout_stats.dropped_experts;
let late_dropped = result_late.unwrap().dropout_stats.dropped_experts;
assert!(early_dropped != late_dropped);
}
// Test 8: Dropout scheduler functionality
#[test]
fn test_dropout_scheduler() {
let initial_rate = 0.5;
let final_rate = 0.1;
let total_steps = 1000;
let scheduler = DropoutScheduler::new(initial_rate, final_rate, total_steps);
// Test initial dropout rate
assert_eq!(scheduler.get_dropout_rate(0), initial_rate);
// Test final dropout rate
assert_eq!(scheduler.get_dropout_rate(total_steps), final_rate);
// Test intermediate rate (should be between initial and final)
let mid_rate = scheduler.get_dropout_rate(total_steps / 2);
assert!(mid_rate > final_rate);
assert!(mid_rate < initial_rate);
// Test beyond total steps (should return final rate)
assert_eq!(scheduler.get_dropout_rate(total_steps + 100), final_rate);
}
// Test 9: Expert importance scorer
#[test]
fn test_expert_importance_scorer() {
let num_experts = 8;
let scorer = ExpertImportanceScorer::new(num_experts);
// Test initial scores (should be uniform)
let initial_scores = scorer.get_importance_scores();
assert_eq!(initial_scores.len(), num_experts);
for &score in &initial_scores {
assert!(score >= 0.0 && score <= 1.0);
}
// Test score update with routing weights
let routing_weights = vec![0.8, 0.6, 0.1, 0.2, 0.9, 0.05, 0.15, 0.7];
let mut updated_scorer = scorer.clone();
updated_scorer.update_scores(&routing_weights);
let updated_scores = updated_scorer.get_importance_scores();
assert_eq!(updated_scores.len(), num_experts);
// Expert 4 (index 4) had highest routing weight (0.9)
let max_score_idx = updated_scores.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.total_cmp(b))
.unwrap().0;
assert_eq!(max_score_idx, 4);
}
// Test 10: Dropout statistics tracking
#[test]
fn test_dropout_statistics() {
let mut stats = DropoutStatistics::new();
// Test initial state
assert_eq!(stats.total_experts, 0);
assert_eq!(stats.dropped_experts, 0);
assert_eq!(stats.active_experts, 0);
assert_eq!(stats.dropout_rate, 0.0);
// Test update
stats.update(8, 3);
assert_eq!(stats.total_experts, 8);
assert_eq!(stats.dropped_experts, 3);
assert_eq!(stats.active_experts, 5);
assert_eq!(stats.dropout_rate, 3.0 / 8.0);
// Test cumulative tracking
stats.update(8, 2);
assert_eq!(stats.cumulative_dropped, 5); // 3 + 2
assert_eq!(stats.cumulative_total, 16); // 8 + 8
}
// Test 11: Integration with MoE routing
#[test]
fn test_moe_integration() {
let device = Device::cuda(0).unwrap_or(Device::default());
let moe_config = MoEConfig::new(8, 2, 768, 3072);
let router = Router::new(moe_config.clone(), &device).unwrap();
let dropout_config = ExpertDropoutConfig::new(0.2, DropoutStrategy::Random);
let mut dropout_layer = ExpertDropoutLayer::new(dropout_config, moe_config.clone(), &device).unwrap();
dropout_layer.set_training(true);
let batch_size = 4;
let seq_len = 16;
let input = Tensor::randn(&[batch_size, seq_len, moe_config.hidden_dim], DType::F32, &device).unwrap();
// Get routing info from MoE router
let routing_info = router.route(&input).unwrap();
// Apply expert dropout to routing
let dropout_result = dropout_layer.apply_to_routing(&routing_info);
assert!(dropout_result.is_ok());
let modified_routing = dropout_result.unwrap();
// Some experts should be masked out
assert!(modified_routing.active_expert_mask.len() == moe_config.num_experts);
}
// Helper function to create mock expert outputs
fn create_mock_expert_outputs(
batch_size: usize,
seq_len: usize,
num_experts: usize,
hidden_dim: usize,
device: &Device
) -> ExpertOutputs {
let mut outputs = Vec::new();
for _ in 0..num_experts {
let expert_output = Tensor::randn(&[batch_size, seq_len, hidden_dim], DType::F32, device).unwrap();
outputs.push(expert_output);
}
ExpertOutputs { outputs }
}
}