chore(sweep): delete 43 orphaned source files; document SYCL/demo/duplication status
CI / Format Check (push) Failing after 5s
Performance Benchmarks / Run Benchmarks (push) Failing after 7s
CI / Build (macos-latest) (push) Failing after 11s
CI / Build (ubuntu-latest) (push) Failing after 2m34s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
Documentation / Build User Guide (push) Successful in 9s
GPU Tests / Check GPU Availability (push) Successful in 0s
GPU Tests / Metal Tests (push) Has been skipped
GPU Tests / CUDA Tests (11.8) (push) Has been skipped
GPU Tests / CUDA Tests (12.1) (push) Has been skipped
CI / Build CPU-Only (Explicit) (push) Failing after 3m32s
CI / Clippy Check (push) Failing after 4m9s
CI / CI Success (push) Failing after 0s
Documentation / Build API Documentation (push) Failing after 4m18s
CI / Format Check (push) Failing after 5s
Performance Benchmarks / Run Benchmarks (push) Failing after 7s
CI / Build (macos-latest) (push) Failing after 11s
CI / Build (ubuntu-latest) (push) Failing after 2m34s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
Documentation / Build User Guide (push) Successful in 9s
GPU Tests / Check GPU Availability (push) Successful in 0s
GPU Tests / Metal Tests (push) Has been skipped
GPU Tests / CUDA Tests (11.8) (push) Has been skipped
GPU Tests / CUDA Tests (12.1) (push) Has been skipped
CI / Build CPU-Only (Explicit) (push) Failing after 3m32s
CI / Clippy Check (push) Failing after 4m9s
CI / CI Success (push) Failing after 0s
Documentation / Build API Documentation (push) Failing after 4m18s
Deletions (all verified unreferenced by any mod/include/path declaration;
git history preserves them):
- rtx-transformers: entire orphaned curriculum/ split (mod.rs holds the
real inline implementation), non-_simple graph variants, superseded
simmim/jepa_integration files, layers/{sliding_window_attention,
positional_encoding,ssm_state_cache_original}, lib_full/lib_minimal/
error_full/error_minimal, orphaned MoE impls (moe_layer,
moe_integration).
- rtx-distributed/parallel_old.rs; rtx-flash-attention/{core_full,
lib_full}.rs; rtx-compress legacy_distillation + structured_pruner.
- rtx-tensor/tensor_core.rs; rtx-runtime/{cuda_kernel_ops,
cuda_backend_mock}.rs; rtx-memory/{gpu_pool_manager,allocator,
pool_type}.rs; rtx-losses/{lib_minimal,lib_full}.rs.
Docs honesty:
- rtx-backend-sycl marked EXPERIMENTAL SKELETON in crate docs and
CLAUDE.md backend table (all ops return NotImplemented).
- docs/consolidation.md records canonical MoE (layers/mixture_of_experts)
and flash-attention (rtx-flash-attention crate) implementations plus
remaining duplicates to consolidate.
- CLAUDE.md: meta-crate GPU features noted; simulation-only demos named;
serving/streaming mock removal noted.
Verified: cargo check --workspace clean (rtx-onnx-codegen pre-broken at
HEAD, unrelated); lib tests pass for all touched crates (rtx-runtime's 4
failures pre-exist at HEAD).
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -106,10 +106,12 @@ let tokens = engine.generate(&prompt, max_tokens=512)?;
|
|||||||
| CUDA | NVIDIA (RTX 4090/5090, A100, H100) | `cuda` |
|
| CUDA | NVIDIA (RTX 4090/5090, A100, H100) | `cuda` |
|
||||||
| Metal | Apple M-series (M1/M2/M3/M4/M5) | `metal` |
|
| Metal | Apple M-series (M1/M2/M3/M4/M5) | `metal` |
|
||||||
| ROCm | AMD RDNA 2/3 (RX 7900 XTX) | `rocm` |
|
| ROCm | AMD RDNA 2/3 (RX 7900 XTX) | `rocm` |
|
||||||
| SYCL | Intel Arc (A770/A750) | `sycl` |
|
| SYCL | Intel Arc (A770/A750) — **experimental skeleton, ops return NotImplemented** | `sycl` |
|
||||||
| WebGPU | Browser (WASM) | `webgpu` |
|
| WebGPU | Browser (WASM) | `webgpu` |
|
||||||
| CPU | x86/ARM with MKL | default |
|
| CPU | x86/ARM with MKL | default |
|
||||||
|
|
||||||
|
The meta-crates (`rtx-core`, `rtx-training`, `rtx-inference-stack`) expose `cuda`/`metal` features that thread GPU support through their sub-crates.
|
||||||
|
|
||||||
## Novel Features
|
## Novel Features
|
||||||
|
|
||||||
- **Speculative decoding** (`rtx-inference::speculative` + `rtx-inference::medusa`/`lookahead`): 2–3× inference speedup via draft model + target verification — GPU-native Rust implementation
|
- **Speculative decoding** (`rtx-inference::speculative` + `rtx-inference::medusa`/`lookahead`): 2–3× inference speedup via draft model + target verification — GPU-native Rust implementation
|
||||||
@@ -149,3 +151,5 @@ All in `crates/training/rtx-transformers/src/ssl/`. 163 tests passing.
|
|||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
Production-ready. 113 crates. Full transformer stack, Flash Attention (v2+v3), MoE, speculative decoding (Medusa/EAGLE/Lookahead), federated learning, NAS, medical/neuroimaging/scientific domain stacks, complete JEPA platform (Batches 20–26). 30+ demo applications. 13,000+ tests.
|
Production-ready. 113 crates. Full transformer stack, Flash Attention (v2+v3), MoE, speculative decoding (Medusa/EAGLE/Lookahead), federated learning, NAS, medical/neuroimaging/scientific domain stacks, complete JEPA platform (Batches 20–26). 30+ demo applications. 13,000+ tests.
|
||||||
|
|
||||||
|
Known honesty notes (2026-07-09 sweep): `rtx-backend-sycl` is an experimental skeleton (ops return NotImplemented). Some demos are pure simulations that don't exercise framework crates (`rtx-distllm-demo`, `rtx-model-zoo`'s MockInferenceEngine, `rtx-inference-profiler`). MoE and flash-attention have duplicated implementations pending consolidation (see `docs/consolidation.md`). Serving/streaming return 503/error until an engine+model is attached (no more mock responses); tokenization at the serving boundary is byte-level until a real tokenizer is threaded through.
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
//! Intel SYCL/oneAPI Backend for RustyTorch++
|
//! Intel SYCL/oneAPI Backend for RustyTorch++
|
||||||
//!
|
//!
|
||||||
//! This crate provides Intel GPU acceleration using SYCL/oneAPI for:
|
//! **STATUS: EXPERIMENTAL SKELETON.** Device queries, buffer create/copy,
|
||||||
|
//! and oneMKL GEMM currently return `SyclError::NotImplemented` — this
|
||||||
|
//! crate is the port target for a future SYCL implementation and does not
|
||||||
|
//! execute on Intel GPUs yet. Use the CUDA, Metal, ROCm, WebGPU, or CPU
|
||||||
|
//! backends for real workloads.
|
||||||
|
//!
|
||||||
|
//! This crate is intended to provide Intel GPU acceleration using SYCL/oneAPI for:
|
||||||
//! - Intel Arc/Xe discrete GPUs
|
//! - Intel Arc/Xe discrete GPUs
|
||||||
//! - Intel integrated GPUs (Gen11+)
|
//! - Intel integrated GPUs (Gen11+)
|
||||||
//! - Intel Data Center GPUs (Ponte Vecchio, etc.)
|
//! - Intel Data Center GPUs (Ponte Vecchio, etc.)
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
//! # RTX Losses - Comprehensive Loss Functions for Deep Learning
|
|
||||||
//!
|
|
||||||
//! This crate provides a complete collection of loss functions for deep learning applications,
|
|
||||||
//! with automatic differentiation support through RTX tensor operations.
|
|
||||||
//!
|
|
||||||
//! ## Features
|
|
||||||
//!
|
|
||||||
//! - **FocalLoss**: For imbalanced classification with configurable alpha/gamma parameters
|
|
||||||
//! - **DiceLoss**: For segmentation tasks with overlap-based metrics and smoothing
|
|
||||||
//! - **ContrastiveLoss**: For metric learning with configurable margin
|
|
||||||
//! - **CrossEntropyLoss**: Standard classification with label smoothing support
|
|
||||||
//! - **MSELoss**: Regression baseline with robust batch processing
|
|
||||||
//! - **InfoNCELoss**: For self-supervised contrastive learning (SimCLR, MoCo)
|
|
||||||
//!
|
|
||||||
//! All losses support:
|
|
||||||
//! - Multiple reduction modes (Mean, Sum, None)
|
|
||||||
//! - Automatic gradient computation through RTX autograd
|
|
||||||
//! - Numerical stability optimizations
|
|
||||||
//! - Comprehensive batch processing
|
|
||||||
//!
|
|
||||||
//! ## Usage
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! use rtx_losses::{CrossEntropyLoss, Loss, Reduction};
|
|
||||||
//! use rtx_tensor::{Tensor, Device};
|
|
||||||
//!
|
|
||||||
//! let device = Device::cpu();
|
|
||||||
//! let loss = CrossEntropyLoss::new()
|
|
||||||
//! .with_reduction(Reduction::Mean)
|
|
||||||
//! .with_label_smoothing(0.1);
|
|
||||||
//!
|
|
||||||
//! let logits = Tensor::randn([32, 10], &device)?; // Batch=32, Classes=10
|
|
||||||
//! let targets = Tensor::randint(0, 10, [32], &device)?;
|
|
||||||
//!
|
|
||||||
//! let loss_value = loss.forward(&logits, &targets)?;
|
|
||||||
//! loss_value.backward()?; // Automatic gradient computation
|
|
||||||
//! # Ok::<(), rtx_tensor::TensorError>(())
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub mod reduction;
|
|
||||||
pub mod loss_trait;
|
|
||||||
pub mod focal_loss;
|
|
||||||
pub mod dice_loss;
|
|
||||||
pub mod contrastive_loss;
|
|
||||||
pub mod cross_entropy_loss;
|
|
||||||
pub mod mse_loss;
|
|
||||||
pub mod info_nce_loss;
|
|
||||||
pub mod info_nce_simple;
|
|
||||||
pub mod minimal_tensor;
|
|
||||||
pub mod info_nce_minimal;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod standalone_test;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod simple_test;
|
|
||||||
|
|
||||||
// Re-export main types
|
|
||||||
pub use error::{LossError, Result};
|
|
||||||
pub use reduction::Reduction;
|
|
||||||
pub use loss_trait::Loss;
|
|
||||||
pub use focal_loss::FocalLoss;
|
|
||||||
pub use dice_loss::DiceLoss;
|
|
||||||
pub use contrastive_loss::ContrastiveLoss;
|
|
||||||
pub use cross_entropy_loss::CrossEntropyLoss;
|
|
||||||
pub use mse_loss::MSELoss;
|
|
||||||
pub use info_nce_loss::InfoNCELoss;
|
|
||||||
pub use info_nce_simple::InfoNCELossSimple;
|
|
||||||
pub use minimal_tensor::{MinimalTensor, MinimalDevice};
|
|
||||||
pub use info_nce_minimal::InfoNCEMinimal;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
//! Minimal RTX Losses - InfoNCE, SwAV, and SimCLR for Self-Supervised Learning
|
|
||||||
//!
|
|
||||||
//! This provides working implementations of critical contrastive learning loss functions
|
|
||||||
//! with a minimal tensor implementation to avoid ecosystem compilation issues.
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub mod reduction;
|
|
||||||
pub mod minimal_tensor;
|
|
||||||
pub mod info_nce_minimal;
|
|
||||||
|
|
||||||
// Re-export main types
|
|
||||||
pub use error::{LossError, Result};
|
|
||||||
pub use reduction::Reduction;
|
|
||||||
pub use minimal_tensor::{MinimalTensor, MinimalDevice};
|
|
||||||
pub use info_nce_minimal::InfoNCEMinimal;
|
|
||||||
|
|
||||||
// Simplified Loss trait for minimal implementation
|
|
||||||
pub trait MinimalLoss {
|
|
||||||
/// Reduction mode for this loss
|
|
||||||
fn reduction(&self) -> Reduction;
|
|
||||||
}
|
|
||||||
@@ -1,487 +0,0 @@
|
|||||||
//! Memory Allocator Implementation
|
|
||||||
//!
|
|
||||||
//! Complete memory allocator for GPU and host memory management.
|
|
||||||
//! No placeholders, full implementation following strict TDD.
|
|
||||||
|
|
||||||
use crate::error::{MemoryError, Result};
|
|
||||||
use crate::pool_type::PoolType;
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use tracing::{debug, warn};
|
|
||||||
|
|
||||||
/// Memory allocator for managing memory blocks
|
|
||||||
pub struct MemoryAllocator {
|
|
||||||
/// Total size of memory managed
|
|
||||||
total_size: usize,
|
|
||||||
/// Memory alignment requirement
|
|
||||||
alignment: usize,
|
|
||||||
/// Free blocks tracked by size
|
|
||||||
free_blocks: BTreeMap<usize, Vec<MemoryBlock>>,
|
|
||||||
/// Allocated blocks tracked by address
|
|
||||||
allocated_blocks: HashMap<usize, MemoryBlock>,
|
|
||||||
/// Next block ID
|
|
||||||
next_block_id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Memory block descriptor
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct MemoryBlock {
|
|
||||||
pub id: u64,
|
|
||||||
pub address: usize,
|
|
||||||
pub size: usize,
|
|
||||||
pub allocated: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocation information
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AllocationInfo {
|
|
||||||
pub ptr: usize,
|
|
||||||
pub size: usize,
|
|
||||||
pub pool_type: PoolType,
|
|
||||||
pub timestamp: std::time::Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoryAllocator {
|
|
||||||
/// Create a new memory allocator
|
|
||||||
pub fn new(total_size: usize, alignment: usize) -> Result<Self> {
|
|
||||||
if alignment == 0 || !alignment.is_power_of_two() {
|
|
||||||
return Err(MemoryError::InvalidConfiguration(
|
|
||||||
format!("Alignment must be a power of 2, got {}", alignment)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut allocator = Self {
|
|
||||||
total_size,
|
|
||||||
alignment,
|
|
||||||
free_blocks: BTreeMap::new(),
|
|
||||||
allocated_blocks: HashMap::new(),
|
|
||||||
next_block_id: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize with one large free block
|
|
||||||
let initial_block = MemoryBlock {
|
|
||||||
id: 0,
|
|
||||||
address: 0,
|
|
||||||
size: total_size,
|
|
||||||
allocated: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
allocator.add_free_block(initial_block);
|
|
||||||
debug!("Created allocator with {} bytes, alignment {}", total_size, alignment);
|
|
||||||
|
|
||||||
Ok(allocator)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate memory of the specified size
|
|
||||||
pub fn allocate(&mut self, size: usize) -> Result<usize> {
|
|
||||||
let aligned_size = self.align_size(size);
|
|
||||||
|
|
||||||
// Find the smallest free block that fits
|
|
||||||
let block = self.find_free_block(aligned_size)?;
|
|
||||||
|
|
||||||
// Split the block if it's larger than needed
|
|
||||||
if block.size > aligned_size {
|
|
||||||
self.split_block(block, aligned_size)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark block as allocated
|
|
||||||
let mut allocated_block = block;
|
|
||||||
allocated_block.allocated = true;
|
|
||||||
allocated_block.size = aligned_size;
|
|
||||||
|
|
||||||
self.allocated_blocks.insert(block.address, allocated_block);
|
|
||||||
|
|
||||||
debug!("Allocated {} bytes at address {:#x}", aligned_size, block.address);
|
|
||||||
Ok(block.address)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deallocate memory at the specified address
|
|
||||||
pub fn deallocate(&mut self, address: usize) -> Result<()> {
|
|
||||||
let block = self.allocated_blocks.remove(&address)
|
|
||||||
.ok_or_else(|| MemoryError::InvalidPointer(
|
|
||||||
format!("Address {:#x} not found in allocated blocks", address)
|
|
||||||
))?;
|
|
||||||
|
|
||||||
// Mark as free and try to coalesce with adjacent blocks
|
|
||||||
let mut free_block = block;
|
|
||||||
free_block.allocated = false;
|
|
||||||
|
|
||||||
self.coalesce_and_add_free(free_block);
|
|
||||||
|
|
||||||
debug!("Deallocated {} bytes at address {:#x}", block.size, address);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total allocated size
|
|
||||||
pub fn allocated_size(&self) -> usize {
|
|
||||||
self.allocated_blocks.values().map(|b| b.size).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total free size
|
|
||||||
pub fn free_size(&self) -> usize {
|
|
||||||
self.free_blocks
|
|
||||||
.values()
|
|
||||||
.flat_map(|blocks| blocks.iter())
|
|
||||||
.map(|b| b.size)
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get fragmentation ratio
|
|
||||||
pub fn fragmentation_ratio(&self) -> f64 {
|
|
||||||
let free_block_count = self.free_blocks.values().map(|v| v.len()).sum::<usize>();
|
|
||||||
if free_block_count <= 1 {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
let largest_free = self.free_blocks
|
|
||||||
.values()
|
|
||||||
.flat_map(|blocks| blocks.iter())
|
|
||||||
.map(|b| b.size)
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
let total_free = self.free_size();
|
|
||||||
if total_free == 0 {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
1.0 - (largest_free as f64 / total_free as f64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private helper methods
|
|
||||||
|
|
||||||
fn align_size(&self, size: usize) -> usize {
|
|
||||||
(size + self.alignment - 1) & !(self.alignment - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_free_block(&mut self, size: usize) -> Result<MemoryBlock> {
|
|
||||||
// Find the smallest block that fits (best fit)
|
|
||||||
for (&block_size, blocks) in &mut self.free_blocks {
|
|
||||||
if block_size >= size && !blocks.is_empty() {
|
|
||||||
return Ok(blocks.remove(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(MemoryError::OutOfMemory(format!(
|
|
||||||
"No free block found for {} bytes", size
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_free_block(&mut self, block: MemoryBlock) {
|
|
||||||
self.free_blocks
|
|
||||||
.entry(block.size)
|
|
||||||
.or_default()
|
|
||||||
.push(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_block(&mut self, block: MemoryBlock, size: usize) -> Result<()> {
|
|
||||||
if block.size <= size {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining_size = block.size - size;
|
|
||||||
let remaining_block = MemoryBlock {
|
|
||||||
id: self.next_block_id,
|
|
||||||
address: block.address + size,
|
|
||||||
size: remaining_size,
|
|
||||||
allocated: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.next_block_id += 1;
|
|
||||||
self.add_free_block(remaining_block);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn coalesce_and_add_free(&mut self, block: MemoryBlock) {
|
|
||||||
let mut coalesced = block;
|
|
||||||
|
|
||||||
// Try to coalesce with previous block
|
|
||||||
if coalesced.address > 0 {
|
|
||||||
// Check if there's a free block ending at our start address
|
|
||||||
for blocks in self.free_blocks.values_mut() {
|
|
||||||
if let Some(idx) = blocks.iter().position(|b|
|
|
||||||
b.address + b.size == coalesced.address
|
|
||||||
) {
|
|
||||||
let prev_block = blocks.remove(idx);
|
|
||||||
coalesced.address = prev_block.address;
|
|
||||||
coalesced.size += prev_block.size;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to coalesce with next block
|
|
||||||
for blocks in self.free_blocks.values_mut() {
|
|
||||||
if let Some(idx) = blocks.iter().position(|b|
|
|
||||||
b.address == coalesced.address + coalesced.size
|
|
||||||
) {
|
|
||||||
let next_block = blocks.remove(idx);
|
|
||||||
coalesced.size += next_block.size;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up empty vectors in free_blocks
|
|
||||||
self.free_blocks.retain(|_, blocks| !blocks.is_empty());
|
|
||||||
|
|
||||||
// Add the coalesced block
|
|
||||||
self.add_free_block(coalesced);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Buddy allocator for power-of-2 allocations
|
|
||||||
pub struct BuddyAllocator {
|
|
||||||
min_order: usize,
|
|
||||||
max_order: usize,
|
|
||||||
free_lists: Vec<Vec<usize>>,
|
|
||||||
block_status: HashMap<usize, BlockStatus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
struct BlockStatus {
|
|
||||||
order: usize,
|
|
||||||
allocated: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BuddyAllocator {
|
|
||||||
/// Create a new buddy allocator
|
|
||||||
pub fn new(total_size: usize) -> Result<Self> {
|
|
||||||
let max_order = (total_size as f64).log2().floor() as usize;
|
|
||||||
let min_order = 12; // 4KB minimum
|
|
||||||
|
|
||||||
if max_order < min_order {
|
|
||||||
return Err(MemoryError::InvalidConfiguration(
|
|
||||||
format!("Total size too small for buddy allocator")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut allocator = Self {
|
|
||||||
min_order,
|
|
||||||
max_order,
|
|
||||||
free_lists: vec![Vec::new(); max_order - min_order + 1],
|
|
||||||
block_status: HashMap::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the initial block to the largest order
|
|
||||||
allocator.free_lists[max_order - min_order].push(0);
|
|
||||||
allocator.block_status.insert(0, BlockStatus {
|
|
||||||
order: max_order,
|
|
||||||
allocated: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(allocator)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate memory of at least the specified size
|
|
||||||
pub fn allocate(&mut self, size: usize) -> Result<usize> {
|
|
||||||
let order = self.size_to_order(size);
|
|
||||||
|
|
||||||
if order > self.max_order {
|
|
||||||
return Err(MemoryError::OutOfMemory(
|
|
||||||
format!("Requested size {} exceeds maximum", size)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let address = self.allocate_order(order)?;
|
|
||||||
|
|
||||||
self.block_status.insert(address, BlockStatus {
|
|
||||||
order,
|
|
||||||
allocated: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deallocate memory at the specified address
|
|
||||||
pub fn deallocate(&mut self, address: usize) -> Result<()> {
|
|
||||||
let status = self.block_status.get(&address)
|
|
||||||
.ok_or_else(|| MemoryError::InvalidPointer(
|
|
||||||
format!("Address {:#x} not found", address)
|
|
||||||
))?;
|
|
||||||
|
|
||||||
if !status.allocated {
|
|
||||||
return Err(MemoryError::InvalidPointer(
|
|
||||||
format!("Address {:#x} not allocated", address)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let order = status.order;
|
|
||||||
self.deallocate_order(address, order);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size_to_order(&self, size: usize) -> usize {
|
|
||||||
let order = (size as f64).log2().ceil() as usize;
|
|
||||||
order.max(self.min_order)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allocate_order(&mut self, order: usize) -> Result<usize> {
|
|
||||||
// Find a free block of the requested order or split a larger one
|
|
||||||
for current_order in order..=self.max_order {
|
|
||||||
let idx = current_order - self.min_order;
|
|
||||||
|
|
||||||
if !self.free_lists[idx].is_empty() {
|
|
||||||
let address = self.free_lists[idx].remove(0);
|
|
||||||
|
|
||||||
// Split blocks if necessary
|
|
||||||
for split_order in (order..current_order).rev() {
|
|
||||||
let buddy_address = address ^ (1 << split_order);
|
|
||||||
self.free_lists[split_order - self.min_order].push(buddy_address);
|
|
||||||
|
|
||||||
self.block_status.insert(buddy_address, BlockStatus {
|
|
||||||
order: split_order,
|
|
||||||
allocated: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(MemoryError::OutOfMemory(
|
|
||||||
format!("No free block for order {}", order)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deallocate_order(&mut self, address: usize, order: usize) {
|
|
||||||
// Try to coalesce with buddy
|
|
||||||
let mut current_address = address;
|
|
||||||
let mut current_order = order;
|
|
||||||
|
|
||||||
while current_order < self.max_order {
|
|
||||||
let buddy_address = current_address ^ (1 << current_order);
|
|
||||||
|
|
||||||
// Check if buddy is free
|
|
||||||
if let Some(status) = self.block_status.get(&buddy_address) {
|
|
||||||
if !status.allocated && status.order == current_order {
|
|
||||||
// Remove buddy from free list
|
|
||||||
let idx = current_order - self.min_order;
|
|
||||||
if let Some(pos) = self.free_lists[idx].iter().position(|&a| a == buddy_address) {
|
|
||||||
self.free_lists[idx].remove(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove buddy from status
|
|
||||||
self.block_status.remove(&buddy_address);
|
|
||||||
|
|
||||||
// Continue coalescing at higher order
|
|
||||||
current_address = current_address.min(buddy_address);
|
|
||||||
current_order += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the coalesced block to free list
|
|
||||||
self.free_lists[current_order - self.min_order].push(current_address);
|
|
||||||
|
|
||||||
self.block_status.insert(current_address, BlockStatus {
|
|
||||||
order: current_order,
|
|
||||||
allocated: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_memory_allocator_basic() {
|
|
||||||
let mut allocator = MemoryAllocator::new(1024 * 1024, 64).unwrap();
|
|
||||||
|
|
||||||
// Allocate some memory
|
|
||||||
let addr1 = allocator.allocate(1024).unwrap();
|
|
||||||
assert_eq!(addr1, 0);
|
|
||||||
|
|
||||||
let addr2 = allocator.allocate(2048).unwrap();
|
|
||||||
assert_eq!(addr2, 1024);
|
|
||||||
|
|
||||||
// Check sizes
|
|
||||||
assert_eq!(allocator.allocated_size(), 1024 + 2048);
|
|
||||||
|
|
||||||
// Deallocate
|
|
||||||
allocator.deallocate(addr1).unwrap();
|
|
||||||
assert_eq!(allocator.allocated_size(), 2048);
|
|
||||||
|
|
||||||
// Should be able to reuse the space
|
|
||||||
let addr3 = allocator.allocate(512).unwrap();
|
|
||||||
assert_eq!(addr3, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_memory_coalescing() {
|
|
||||||
let mut allocator = MemoryAllocator::new(1024, 64).unwrap();
|
|
||||||
|
|
||||||
let addr1 = allocator.allocate(256).unwrap();
|
|
||||||
let addr2 = allocator.allocate(256).unwrap();
|
|
||||||
let addr3 = allocator.allocate(256).unwrap();
|
|
||||||
|
|
||||||
// Deallocate middle block
|
|
||||||
allocator.deallocate(addr2).unwrap();
|
|
||||||
|
|
||||||
// Deallocate first block - should not coalesce yet
|
|
||||||
allocator.deallocate(addr1).unwrap();
|
|
||||||
|
|
||||||
// Deallocate last block - should coalesce all
|
|
||||||
allocator.deallocate(addr3).unwrap();
|
|
||||||
|
|
||||||
// Should be able to allocate the full size again
|
|
||||||
let addr4 = allocator.allocate(768).unwrap();
|
|
||||||
assert_eq!(addr4, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_buddy_allocator() {
|
|
||||||
let mut buddy = BuddyAllocator::new(1024 * 1024).unwrap();
|
|
||||||
|
|
||||||
// Allocate various sizes
|
|
||||||
let addr1 = buddy.allocate(4096).unwrap();
|
|
||||||
assert_eq!(addr1, 0);
|
|
||||||
|
|
||||||
let addr2 = buddy.allocate(8192).unwrap();
|
|
||||||
assert!(addr2 > 0);
|
|
||||||
|
|
||||||
// Deallocate and check coalescing
|
|
||||||
buddy.deallocate(addr1).unwrap();
|
|
||||||
buddy.deallocate(addr2).unwrap();
|
|
||||||
|
|
||||||
// Should be able to allocate larger size after coalescing
|
|
||||||
let addr3 = buddy.allocate(16384).unwrap();
|
|
||||||
assert_eq!(addr3, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_alignment() {
|
|
||||||
let allocator = MemoryAllocator::new(1024, 256).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(allocator.align_size(1), 256);
|
|
||||||
assert_eq!(allocator.align_size(256), 256);
|
|
||||||
assert_eq!(allocator.align_size(257), 512);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fragmentation_ratio() {
|
|
||||||
let mut allocator = MemoryAllocator::new(1024, 64).unwrap();
|
|
||||||
|
|
||||||
// Initially no fragmentation
|
|
||||||
assert_eq!(allocator.fragmentation_ratio(), 0.0);
|
|
||||||
|
|
||||||
// Create fragmentation
|
|
||||||
let addr1 = allocator.allocate(256).unwrap();
|
|
||||||
let addr2 = allocator.allocate(256).unwrap();
|
|
||||||
let addr3 = allocator.allocate(256).unwrap();
|
|
||||||
|
|
||||||
allocator.deallocate(addr1).unwrap();
|
|
||||||
allocator.deallocate(addr3).unwrap();
|
|
||||||
|
|
||||||
// Now we have fragmentation
|
|
||||||
let ratio = allocator.fragmentation_ratio();
|
|
||||||
assert!(ratio > 0.0);
|
|
||||||
assert!(ratio < 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,869 +0,0 @@
|
|||||||
//! GPU Memory Pool Manager
|
|
||||||
//!
|
|
||||||
//! Integrated GPU memory management combining arena allocation, cross-device transfers,
|
|
||||||
//! memory pinning, prefetching, and OOM recovery for production-grade ML workloads.
|
|
||||||
|
|
||||||
use crate::{MemoryError, Result};
|
|
||||||
use crate::gpu_allocator::{DeviceId, GpuMemoryBlock, GpuMemoryType, GpuArenaAllocator, GpuMemoryStats};
|
|
||||||
use crate::gpu_transfer::{GpuTransferManager, TransferConfig, TransferStats, TransferOperation};
|
|
||||||
use crate::gpu_pinning::{GpuPinningManager, PinnedMemoryBlock, PrefetchConfig, PinnedMemoryStats, MemoryAccessType};
|
|
||||||
use crate::gpu_oom::{GpuOomManager, OomRecoveryConfig, OomRecoveryStats, AllocatorCallback, MemoryPressure};
|
|
||||||
|
|
||||||
use std::sync::{Arc, Weak};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use parking_lot::{RwLock, Mutex};
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
/// Comprehensive GPU memory pool configuration
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct GpuPoolConfig {
|
|
||||||
/// Arena size per device (bytes)
|
|
||||||
pub arena_size_per_device: usize,
|
|
||||||
/// Memory alignment (must be power of 2)
|
|
||||||
pub memory_alignment: usize,
|
|
||||||
/// Transfer configuration
|
|
||||||
pub transfer_config: TransferConfig,
|
|
||||||
/// Prefetch configuration
|
|
||||||
pub prefetch_config: PrefetchConfig,
|
|
||||||
/// OOM recovery configuration
|
|
||||||
pub oom_config: OomRecoveryConfig,
|
|
||||||
/// Enable cross-device peer-to-peer transfers
|
|
||||||
pub enable_p2p: bool,
|
|
||||||
/// Maximum number of devices to manage
|
|
||||||
pub max_devices: usize,
|
|
||||||
/// Enable performance monitoring
|
|
||||||
pub enable_monitoring: bool,
|
|
||||||
/// Warmup allocation sizes
|
|
||||||
pub warmup_sizes: Vec<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for GpuPoolConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
arena_size_per_device: 4 * 1024 * 1024 * 1024, // 4GB per device
|
|
||||||
memory_alignment: 256, // 256-byte alignment for GPU efficiency
|
|
||||||
transfer_config: TransferConfig::default(),
|
|
||||||
prefetch_config: PrefetchConfig::default(),
|
|
||||||
oom_config: OomRecoveryConfig::default(),
|
|
||||||
enable_p2p: true,
|
|
||||||
max_devices: 8,
|
|
||||||
enable_monitoring: true,
|
|
||||||
warmup_sizes: vec![
|
|
||||||
4096, // 4KB - small tensors
|
|
||||||
65536, // 64KB - medium tensors
|
|
||||||
1048576, // 1MB - large tensors
|
|
||||||
16777216, // 16MB - very large tensors
|
|
||||||
134217728, // 128MB - model weights
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GPU memory operation types for monitoring
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum GpuMemoryOperation {
|
|
||||||
Allocate,
|
|
||||||
Deallocate,
|
|
||||||
Transfer,
|
|
||||||
Pin,
|
|
||||||
Unpin,
|
|
||||||
Prefetch,
|
|
||||||
OomRecover,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Comprehensive GPU memory pool statistics
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct GpuPoolStats {
|
|
||||||
/// Per-device memory statistics
|
|
||||||
pub device_stats: HashMap<DeviceId, GpuMemoryStats>,
|
|
||||||
/// Transfer statistics
|
|
||||||
pub transfer_stats: TransferStats,
|
|
||||||
/// Pinned memory statistics
|
|
||||||
pub pinned_stats: PinnedMemoryStats,
|
|
||||||
/// OOM recovery statistics
|
|
||||||
pub oom_stats: OomRecoveryStats,
|
|
||||||
/// Total allocations across all devices
|
|
||||||
pub total_allocations: u64,
|
|
||||||
/// Total memory usage across all devices
|
|
||||||
pub total_memory_usage: u64,
|
|
||||||
/// Average allocation time in nanoseconds
|
|
||||||
pub average_allocation_time_ns: u64,
|
|
||||||
/// Cache hit rate across all operations
|
|
||||||
pub overall_cache_hit_rate: f64,
|
|
||||||
/// Current memory pressure level (worst across devices)
|
|
||||||
pub worst_memory_pressure: MemoryPressure,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unified GPU memory pool manager
|
|
||||||
pub struct GpuPoolManager {
|
|
||||||
/// Configuration
|
|
||||||
config: GpuPoolConfig,
|
|
||||||
|
|
||||||
/// Per-device allocators
|
|
||||||
device_allocators: RwLock<HashMap<DeviceId, Arc<GpuArenaAllocator>>>,
|
|
||||||
|
|
||||||
/// Transfer manager for cross-device operations
|
|
||||||
transfer_manager: Arc<Mutex<GpuTransferManager>>,
|
|
||||||
|
|
||||||
/// Pinning manager for host-pinned memory
|
|
||||||
pinning_manager: Arc<GpuPinningManager>,
|
|
||||||
|
|
||||||
/// OOM recovery manager
|
|
||||||
oom_manager: Arc<GpuOomManager>,
|
|
||||||
|
|
||||||
/// Operation statistics
|
|
||||||
operation_stats: RwLock<HashMap<GpuMemoryOperation, OperationMetrics>>,
|
|
||||||
|
|
||||||
/// Global allocation counter
|
|
||||||
total_allocations: AtomicU64,
|
|
||||||
|
|
||||||
/// Performance monitoring
|
|
||||||
monitoring_enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Operation-specific metrics
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct OperationMetrics {
|
|
||||||
count: u64,
|
|
||||||
total_time: Duration,
|
|
||||||
success_count: u64,
|
|
||||||
bytes_processed: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OperationMetrics {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
count: 0,
|
|
||||||
total_time: Duration::ZERO,
|
|
||||||
success_count: 0,
|
|
||||||
bytes_processed: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GpuPoolManager {
|
|
||||||
/// Create new GPU memory pool manager
|
|
||||||
pub fn new(config: GpuPoolConfig) -> Result<Self> {
|
|
||||||
let transfer_manager = Arc::new(Mutex::new(
|
|
||||||
GpuTransferManager::new(config.transfer_config.clone())
|
|
||||||
));
|
|
||||||
|
|
||||||
let pinning_manager = Arc::new(
|
|
||||||
GpuPinningManager::new(config.prefetch_config.clone())
|
|
||||||
);
|
|
||||||
|
|
||||||
let oom_manager = Arc::new(
|
|
||||||
GpuOomManager::new(config.oom_config.clone())
|
|
||||||
);
|
|
||||||
|
|
||||||
let manager = Self {
|
|
||||||
config,
|
|
||||||
device_allocators: RwLock::new(HashMap::new()),
|
|
||||||
transfer_manager,
|
|
||||||
pinning_manager,
|
|
||||||
oom_manager,
|
|
||||||
operation_stats: RwLock::new(HashMap::new()),
|
|
||||||
total_allocations: AtomicU64::new(0),
|
|
||||||
monitoring_enabled: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(manager)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize GPU device for memory management
|
|
||||||
pub fn initialize_device(&self, device_id: DeviceId) -> Result<()> {
|
|
||||||
// Check device limit
|
|
||||||
if self.device_allocators.read().len() >= self.config.max_devices {
|
|
||||||
return Err(MemoryError::configuration(
|
|
||||||
format!("Maximum devices limit ({}) reached", self.config.max_devices)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create device allocator
|
|
||||||
let allocator = Arc::new(GpuArenaAllocator::with_alignment(
|
|
||||||
device_id,
|
|
||||||
self.config.arena_size_per_device,
|
|
||||||
self.config.memory_alignment,
|
|
||||||
)?);
|
|
||||||
|
|
||||||
// Register with OOM manager
|
|
||||||
self.oom_manager.register_allocator(
|
|
||||||
Arc::downgrade(&allocator) as Weak<dyn AllocatorCallback>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize transfer manager for this device
|
|
||||||
{
|
|
||||||
let mut transfer_manager = self.transfer_manager.lock();
|
|
||||||
transfer_manager.initialize_device(device_id)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize pinning manager for this device
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
{
|
|
||||||
let mut pinning_manager = Arc::try_unwrap(Arc::clone(&self.pinning_manager))
|
|
||||||
.unwrap_or_else(|arc| (*arc).clone());
|
|
||||||
pinning_manager.initialize_device(device_id)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the allocator
|
|
||||||
{
|
|
||||||
let mut allocators = self.device_allocators.write();
|
|
||||||
allocators.insert(device_id, allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform warmup if configured
|
|
||||||
if !self.config.warmup_sizes.is_empty() {
|
|
||||||
self.warmup_device(device_id)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate GPU memory with automatic device selection and OOM recovery
|
|
||||||
pub async fn allocate(&self, size: usize, memory_type: GpuMemoryType, device_id: DeviceId) -> Result<GpuMemoryBlock> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let result = self.try_allocate(size, memory_type, device_id).await;
|
|
||||||
|
|
||||||
// Handle OOM with recovery
|
|
||||||
let final_result = match result {
|
|
||||||
Err(MemoryError::OutOfMemory { .. }) => {
|
|
||||||
// Attempt OOM recovery
|
|
||||||
match self.oom_manager.recover_from_oom(size, device_id).await {
|
|
||||||
Ok(true) => {
|
|
||||||
// Recovery successful, retry allocation
|
|
||||||
self.try_allocate(size, memory_type, device_id).await
|
|
||||||
},
|
|
||||||
Ok(false) => {
|
|
||||||
// Recovery failed
|
|
||||||
Err(MemoryError::out_of_memory(size, 0))
|
|
||||||
},
|
|
||||||
Err(recovery_error) => Err(recovery_error),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
other => other,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Record operation metrics
|
|
||||||
if self.monitoring_enabled {
|
|
||||||
let success = final_result.is_ok();
|
|
||||||
self.record_operation_metric(GpuMemoryOperation::Allocate, start_time.elapsed(), success, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track allocation for OOM management
|
|
||||||
if let Ok(ref block) = final_result {
|
|
||||||
self.oom_manager.track_allocation(*block, None);
|
|
||||||
self.total_allocations.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
final_result
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_allocate(&self, size: usize, memory_type: GpuMemoryType, device_id: DeviceId) -> Result<GpuMemoryBlock> {
|
|
||||||
let allocators = self.device_allocators.read();
|
|
||||||
|
|
||||||
if let Some(allocator) = allocators.get(&device_id) {
|
|
||||||
allocator.allocate(size, memory_type)
|
|
||||||
} else {
|
|
||||||
Err(MemoryError::configuration(
|
|
||||||
format!("Device {} not initialized", device_id.id())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deallocate GPU memory with tracking cleanup
|
|
||||||
pub async fn deallocate(&self, block: GpuMemoryBlock) -> Result<()> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
// Untrack from OOM manager
|
|
||||||
self.oom_manager.untrack_allocation(block.id);
|
|
||||||
|
|
||||||
// Deallocate from device allocator
|
|
||||||
let allocators = self.device_allocators.read();
|
|
||||||
let result = if let Some(allocator) = allocators.get(&block.device_id) {
|
|
||||||
allocator.deallocate(block)
|
|
||||||
} else {
|
|
||||||
Err(MemoryError::configuration(
|
|
||||||
format!("Device {} not initialized", block.device_id.id())
|
|
||||||
))
|
|
||||||
};
|
|
||||||
|
|
||||||
// Record metrics
|
|
||||||
if self.monitoring_enabled {
|
|
||||||
let success = result.is_ok();
|
|
||||||
self.record_operation_metric(GpuMemoryOperation::Deallocate, start_time.elapsed(), success, block.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate host-pinned memory for fast CPU-GPU transfers
|
|
||||||
pub async fn allocate_pinned(&self, size: usize, device_id: Option<DeviceId>) -> Result<PinnedMemoryBlock> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let result = self.pinning_manager.allocate_pinned(size, device_id);
|
|
||||||
|
|
||||||
if self.monitoring_enabled {
|
|
||||||
let success = result.is_ok();
|
|
||||||
self.record_operation_metric(GpuMemoryOperation::Pin, start_time.elapsed(), success, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Free host-pinned memory
|
|
||||||
pub async fn free_pinned(&self, block: PinnedMemoryBlock) -> Result<()> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let result = self.pinning_manager.free_pinned(block);
|
|
||||||
|
|
||||||
if self.monitoring_enabled {
|
|
||||||
let success = result.is_ok();
|
|
||||||
self.record_operation_metric(GpuMemoryOperation::Unpin, start_time.elapsed(), success, block.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transfer data from CPU to GPU
|
|
||||||
pub async fn transfer_host_to_device(&self, src_data: &[u8], dst_block: &GpuMemoryBlock) -> Result<TransferOperation> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let transfer_manager = self.transfer_manager.lock();
|
|
||||||
let result = transfer_manager.transfer_host_to_device(src_data, dst_block).await;
|
|
||||||
|
|
||||||
if self.monitoring_enabled {
|
|
||||||
let success = result.is_ok();
|
|
||||||
self.record_operation_metric(GpuMemoryOperation::Transfer, start_time.elapsed(), success, src_data.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transfer data from GPU to CPU
|
|
||||||
pub async fn transfer_device_to_host(&self, src_block: &GpuMemoryBlock, dst_data: &mut [u8]) -> Result<TransferOperation> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let transfer_manager = self.transfer_manager.lock();
|
|
||||||
let result = transfer_manager.transfer_device_to_host(src_block, dst_data).await;
|
|
||||||
|
|
||||||
if self.monitoring_enabled {
|
|
||||||
let success = result.is_ok();
|
|
||||||
self.record_operation_metric(GpuMemoryOperation::Transfer, start_time.elapsed(), success, src_block.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transfer data between GPU devices
|
|
||||||
pub async fn transfer_device_to_device(&self, src_block: &GpuMemoryBlock, dst_block: &GpuMemoryBlock) -> Result<TransferOperation> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let transfer_manager = self.transfer_manager.lock();
|
|
||||||
let result = transfer_manager.transfer_device_to_device(src_block, dst_block).await;
|
|
||||||
|
|
||||||
if self.monitoring_enabled {
|
|
||||||
let success = result.is_ok();
|
|
||||||
self.record_operation_metric(GpuMemoryOperation::Transfer, start_time.elapsed(), success, src_block.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record memory access pattern for prefetching optimization
|
|
||||||
pub fn record_access(&self, block_id: u64, access_type: MemoryAccessType) {
|
|
||||||
// Track access for OOM management
|
|
||||||
self.oom_manager.track_access(block_id);
|
|
||||||
|
|
||||||
// Track access for prefetching
|
|
||||||
self.pinning_manager.record_access(block_id, access_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute prefetching operations on a device
|
|
||||||
pub async fn execute_prefetch(&self, device_id: DeviceId) -> Result<usize> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let result = self.pinning_manager.execute_prefetch(device_id).await;
|
|
||||||
|
|
||||||
if self.monitoring_enabled {
|
|
||||||
let success = result.is_ok();
|
|
||||||
let prefetched = result.as_ref().unwrap_or(&0);
|
|
||||||
self.record_operation_metric(GpuMemoryOperation::Prefetch, start_time.elapsed(), success, *prefetched);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable peer-to-peer access between devices
|
|
||||||
pub fn enable_peer_access(&self, src_device: DeviceId, dst_device: DeviceId) -> Result<bool> {
|
|
||||||
let mut transfer_manager = self.transfer_manager.lock();
|
|
||||||
transfer_manager.enable_peer_access(src_device, dst_device)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check memory pressure across all devices
|
|
||||||
pub async fn check_memory_pressure(&self) -> MemoryPressure {
|
|
||||||
let allocators = self.device_allocators.read();
|
|
||||||
let mut worst_pressure = MemoryPressure::Low;
|
|
||||||
|
|
||||||
for (&device_id, _) in allocators.iter() {
|
|
||||||
let pressure = self.oom_manager.check_memory_pressure(device_id).await;
|
|
||||||
if pressure > worst_pressure {
|
|
||||||
worst_pressure = pressure;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
worst_pressure
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform proactive memory management across all devices
|
|
||||||
pub async fn proactive_memory_management(&self) -> Result<usize> {
|
|
||||||
let allocators = self.device_allocators.read();
|
|
||||||
let mut total_freed = 0;
|
|
||||||
|
|
||||||
for (&device_id, _) in allocators.iter() {
|
|
||||||
match self.oom_manager.proactive_memory_management(device_id).await {
|
|
||||||
Ok(freed) => total_freed += freed,
|
|
||||||
Err(_) => continue, // Best effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(total_freed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get comprehensive GPU memory pool statistics
|
|
||||||
pub async fn get_stats(&self) -> GpuPoolStats {
|
|
||||||
let allocators = self.device_allocators.read();
|
|
||||||
let mut device_stats = HashMap::new();
|
|
||||||
|
|
||||||
// Collect per-device statistics
|
|
||||||
for (&device_id, allocator) in allocators.iter() {
|
|
||||||
device_stats.insert(device_id, allocator.memory_stats());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get component statistics
|
|
||||||
let transfer_stats = {
|
|
||||||
let transfer_manager = self.transfer_manager.lock();
|
|
||||||
transfer_manager.get_transfer_stats()
|
|
||||||
};
|
|
||||||
|
|
||||||
let pinned_stats = self.pinning_manager.get_stats();
|
|
||||||
let oom_stats = self.oom_manager.get_stats();
|
|
||||||
|
|
||||||
// Calculate aggregated statistics
|
|
||||||
let total_memory_usage: u64 = device_stats.values().map(|s| s.current_usage as u64).sum();
|
|
||||||
let total_allocations = self.total_allocations.load(Ordering::Relaxed);
|
|
||||||
|
|
||||||
let (average_allocation_time_ns, overall_cache_hit_rate) = self.calculate_performance_metrics();
|
|
||||||
let worst_memory_pressure = self.check_memory_pressure().await;
|
|
||||||
|
|
||||||
GpuPoolStats {
|
|
||||||
device_stats,
|
|
||||||
transfer_stats,
|
|
||||||
pinned_stats,
|
|
||||||
oom_stats,
|
|
||||||
total_allocations,
|
|
||||||
total_memory_usage,
|
|
||||||
average_allocation_time_ns,
|
|
||||||
overall_cache_hit_rate,
|
|
||||||
worst_memory_pressure,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compact memory across all devices to reduce fragmentation
|
|
||||||
pub async fn compact_all_devices(&self) -> Result<HashMap<DeviceId, usize>> {
|
|
||||||
let allocators = self.device_allocators.read();
|
|
||||||
let mut compaction_results = HashMap::new();
|
|
||||||
|
|
||||||
for (&device_id, allocator) in allocators.iter() {
|
|
||||||
match allocator.compact() {
|
|
||||||
Ok(stats) => {
|
|
||||||
compaction_results.insert(device_id, stats.bytes_recovered);
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
compaction_results.insert(device_id, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(compaction_results)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all memory caches across devices and managers
|
|
||||||
pub async fn clear_all_caches(&self) -> Result<usize> {
|
|
||||||
let allocators = self.device_allocators.read();
|
|
||||||
let mut total_freed = 0;
|
|
||||||
|
|
||||||
// Clear device allocator caches
|
|
||||||
for (_, allocator) in allocators.iter() {
|
|
||||||
total_freed += allocator.clear_cache();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear pinning manager cache
|
|
||||||
total_freed += self.pinning_manager.clear_pinned_cache();
|
|
||||||
|
|
||||||
Ok(total_freed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset all statistics
|
|
||||||
pub fn reset_stats(&self) {
|
|
||||||
{
|
|
||||||
let mut stats = self.operation_stats.write();
|
|
||||||
stats.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.total_allocations.store(0, Ordering::Relaxed);
|
|
||||||
|
|
||||||
{
|
|
||||||
let transfer_manager = self.transfer_manager.lock();
|
|
||||||
transfer_manager.reset_stats();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.oom_manager.reset_stats();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get memory usage report for all devices
|
|
||||||
pub async fn generate_memory_report(&self) -> String {
|
|
||||||
let stats = self.get_stats().await;
|
|
||||||
|
|
||||||
let mut report = String::new();
|
|
||||||
report.push_str("=== GPU Memory Pool Report ===\n");
|
|
||||||
report.push_str(&format!("Total Allocations: {}\n", stats.total_allocations));
|
|
||||||
report.push_str(&format!("Total Memory Usage: {} bytes\n", stats.total_memory_usage));
|
|
||||||
report.push_str(&format!("Average Allocation Time: {}ns\n", stats.average_allocation_time_ns));
|
|
||||||
report.push_str(&format!("Overall Cache Hit Rate: {:.2}%\n", stats.overall_cache_hit_rate * 100.0));
|
|
||||||
report.push_str(&format!("Memory Pressure: {:?}\n", stats.worst_memory_pressure));
|
|
||||||
report.push_str("\n");
|
|
||||||
|
|
||||||
// Device-specific reports
|
|
||||||
for (device_id, device_stats) in &stats.device_stats {
|
|
||||||
report.push_str(&format!("Device {}: {} bytes used ({:.1}% utilization)\n",
|
|
||||||
device_id.id(),
|
|
||||||
device_stats.current_usage,
|
|
||||||
device_stats.current_usage as f64 / device_stats.arena_size as f64 * 100.0
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfer statistics
|
|
||||||
report.push_str("\n--- Transfer Statistics ---\n");
|
|
||||||
report.push_str(&format!("Total Transfers: {}\n", stats.transfer_stats.total_transfers));
|
|
||||||
report.push_str(&format!("Average Bandwidth: {:.2} GB/s\n", stats.transfer_stats.average_bandwidth_gbps));
|
|
||||||
report.push_str(&format!("P2P Transfers: {}\n", stats.transfer_stats.p2p_transfers));
|
|
||||||
|
|
||||||
// OOM statistics
|
|
||||||
report.push_str("\n--- OOM Recovery Statistics ---\n");
|
|
||||||
report.push_str(&format!("Total OOM Events: {}\n", stats.oom_stats.total_oom_events));
|
|
||||||
report.push_str(&format!("Successful Recoveries: {}\n", stats.oom_stats.successful_recoveries));
|
|
||||||
report.push_str(&format!("Average Recovery Time: {:.2}ms\n", stats.oom_stats.average_recovery_time_ms));
|
|
||||||
|
|
||||||
report
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private helper methods
|
|
||||||
|
|
||||||
fn warmup_device(&self, device_id: DeviceId) -> Result<()> {
|
|
||||||
let allocators = self.device_allocators.read();
|
|
||||||
|
|
||||||
if let Some(allocator) = allocators.get(&device_id) {
|
|
||||||
// Pre-allocate and deallocate common sizes to populate caches
|
|
||||||
let mut warmup_blocks = Vec::new();
|
|
||||||
|
|
||||||
for &size in &self.config.warmup_sizes {
|
|
||||||
for _ in 0..5 {
|
|
||||||
match allocator.allocate(size, GpuMemoryType::Device) {
|
|
||||||
Ok(block) => warmup_blocks.push(block),
|
|
||||||
Err(_) => break, // If allocation fails, skip this size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deallocate all blocks to populate free caches
|
|
||||||
for block in warmup_blocks {
|
|
||||||
let _ = allocator.deallocate(block); // Best effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_operation_metric(&self, operation: GpuMemoryOperation, duration: Duration, success: bool, bytes: usize) {
|
|
||||||
let mut stats = self.operation_stats.write();
|
|
||||||
let metric = stats.entry(operation).or_default();
|
|
||||||
|
|
||||||
metric.count += 1;
|
|
||||||
metric.total_time += duration;
|
|
||||||
metric.bytes_processed += bytes as u64;
|
|
||||||
|
|
||||||
if success {
|
|
||||||
metric.success_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_performance_metrics(&self) -> (u64, f64) {
|
|
||||||
let stats = self.operation_stats.read();
|
|
||||||
|
|
||||||
// Calculate average allocation time
|
|
||||||
let average_allocation_time_ns = if let Some(alloc_metric) = stats.get(&GpuMemoryOperation::Allocate) {
|
|
||||||
if alloc_metric.count > 0 {
|
|
||||||
(alloc_metric.total_time.as_nanos() as u64) / alloc_metric.count
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate overall cache hit rate (placeholder - would integrate with actual cache metrics)
|
|
||||||
let overall_cache_hit_rate = 0.85; // Placeholder value
|
|
||||||
|
|
||||||
(average_allocation_time_ns, overall_cache_hit_rate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement AllocatorCallback for integration with OOM manager
|
|
||||||
impl AllocatorCallback for GpuArenaAllocator {
|
|
||||||
fn get_memory_stats(&self) -> GpuMemoryStats {
|
|
||||||
self.memory_stats()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_free_memory(&self, target_bytes: usize) -> Result<usize> {
|
|
||||||
// Try to clear cache first
|
|
||||||
let cleared = self.clear_cache();
|
|
||||||
if cleared >= target_bytes {
|
|
||||||
Ok(cleared)
|
|
||||||
} else {
|
|
||||||
// Could implement more aggressive freeing strategies here
|
|
||||||
Ok(cleared)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compact_memory(&self) -> Result<usize> {
|
|
||||||
match self.compact() {
|
|
||||||
Ok(stats) => Ok(stats.bytes_recovered),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_caches(&self) -> Result<usize> {
|
|
||||||
Ok(self.clear_cache())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_evictable_blocks(&self) -> Vec<(GpuMemoryBlock, crate::gpu_oom::BlockMetadata)> {
|
|
||||||
// This would return blocks that can be evicted
|
|
||||||
// For now, return empty as this requires more complex integration
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn evict_blocks(&self, _block_ids: &[u64]) -> Result<usize> {
|
|
||||||
// This would evict specific blocks
|
|
||||||
// For now, just clear cache as best effort
|
|
||||||
Ok(self.clear_cache())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_gpu_pool_manager_creation() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let stats = manager.get_stats().await;
|
|
||||||
assert_eq!(stats.total_allocations, 0);
|
|
||||||
assert_eq!(stats.total_memory_usage, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_device_initialization() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
let result = manager.initialize_device(device_id);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
// Check device is registered
|
|
||||||
let allocators = manager.device_allocators.read();
|
|
||||||
assert!(allocators.contains_key(&device_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_basic_allocation_deallocation() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
// Allocate memory
|
|
||||||
let block = manager.allocate(1024, GpuMemoryType::Device, device_id).await.unwrap();
|
|
||||||
assert_eq!(block.size, 1024);
|
|
||||||
assert_eq!(block.device_id, device_id);
|
|
||||||
|
|
||||||
// Deallocate memory
|
|
||||||
let result = manager.deallocate(block).await;
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_pinned_memory_allocation() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
let pinned_block = manager.allocate_pinned(4096, Some(device_id)).await.unwrap();
|
|
||||||
assert_eq!(pinned_block.size, 4096);
|
|
||||||
assert_eq!(pinned_block.device_id, Some(device_id));
|
|
||||||
|
|
||||||
let result = manager.free_pinned(pinned_block).await;
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_memory_transfer() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
// Allocate GPU memory
|
|
||||||
let gpu_block = manager.allocate(1024, GpuMemoryType::Device, device_id).await.unwrap();
|
|
||||||
|
|
||||||
// Test host to device transfer
|
|
||||||
let src_data = vec![0xAB; 1024];
|
|
||||||
let transfer_result = manager.transfer_host_to_device(&src_data, &gpu_block).await;
|
|
||||||
assert!(transfer_result.is_ok());
|
|
||||||
|
|
||||||
// Test device to host transfer
|
|
||||||
let mut dst_data = vec![0u8; 1024];
|
|
||||||
let transfer_result = manager.transfer_device_to_host(&gpu_block, &mut dst_data).await;
|
|
||||||
assert!(transfer_result.is_ok());
|
|
||||||
|
|
||||||
manager.deallocate(gpu_block).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_memory_pressure_monitoring() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
let pressure = manager.check_memory_pressure().await;
|
|
||||||
// Should be low initially
|
|
||||||
assert!(matches!(pressure, MemoryPressure::Low | MemoryPressure::Medium));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_access_pattern_recording() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
// Record access patterns
|
|
||||||
manager.record_access(1, MemoryAccessType::Sequential);
|
|
||||||
manager.record_access(2, MemoryAccessType::Random);
|
|
||||||
manager.record_access(3, MemoryAccessType::Strided(8));
|
|
||||||
|
|
||||||
// Should not error
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_prefetch_execution() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
let result = manager.execute_prefetch(device_id).await;
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_cache_clearing() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
// Allocate and deallocate to populate caches
|
|
||||||
for _ in 0..5 {
|
|
||||||
let block = manager.allocate(1024, GpuMemoryType::Device, device_id).await.unwrap();
|
|
||||||
manager.deallocate(block).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = manager.clear_all_caches().await;
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(result.unwrap() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_stats_collection() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
// Perform some operations
|
|
||||||
let block = manager.allocate(1024, GpuMemoryType::Device, device_id).await.unwrap();
|
|
||||||
manager.deallocate(block).await.unwrap();
|
|
||||||
|
|
||||||
let stats = manager.get_stats().await;
|
|
||||||
assert!(stats.total_allocations > 0);
|
|
||||||
assert!(!stats.device_stats.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_memory_report_generation() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
let report = manager.generate_memory_report().await;
|
|
||||||
assert!(report.contains("GPU Memory Pool Report"));
|
|
||||||
assert!(report.contains("Total Allocations"));
|
|
||||||
assert!(report.contains("Device 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_device_limit() {
|
|
||||||
let mut config = GpuPoolConfig::default();
|
|
||||||
config.max_devices = 1;
|
|
||||||
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
// First device should succeed
|
|
||||||
let device1 = DeviceId::new(0);
|
|
||||||
assert!(manager.initialize_device(device1).is_ok());
|
|
||||||
|
|
||||||
// Second device should fail due to limit
|
|
||||||
let device2 = DeviceId::new(1);
|
|
||||||
let result = manager.initialize_device(device2);
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_proactive_memory_management() {
|
|
||||||
let config = GpuPoolConfig::default();
|
|
||||||
let manager = GpuPoolManager::new(config).unwrap();
|
|
||||||
|
|
||||||
let device_id = DeviceId::new(0);
|
|
||||||
manager.initialize_device(device_id).unwrap();
|
|
||||||
|
|
||||||
let result = manager.proactive_memory_management().await;
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
//! Memory Pool Type Implementation
|
|
||||||
//!
|
|
||||||
//! Complete implementation of different memory pool types for GPU memory management.
|
|
||||||
//! No placeholders, full functionality with strict TDD approach.
|
|
||||||
|
|
||||||
use crate::error::{MemoryError, Result};
|
|
||||||
use crate::allocator::{MemoryAllocator, AllocationInfo};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
use cudarc::driver::{CudaContext, CudaSlice};
|
|
||||||
|
|
||||||
/// Memory pool type enumeration
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum PoolType {
|
|
||||||
/// Device memory (GPU VRAM)
|
|
||||||
Device,
|
|
||||||
/// Host memory (System RAM)
|
|
||||||
Host,
|
|
||||||
/// Unified memory (accessible from both CPU and GPU)
|
|
||||||
Unified,
|
|
||||||
/// Managed memory (automatically migrated between host and device)
|
|
||||||
Managed,
|
|
||||||
/// Pinned host memory (page-locked for faster transfers)
|
|
||||||
Pinned,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PoolType {
|
|
||||||
/// Get human-readable name for the pool type
|
|
||||||
pub fn name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
PoolType::Device => "Device",
|
|
||||||
PoolType::Host => "Host",
|
|
||||||
PoolType::Unified => "Unified",
|
|
||||||
PoolType::Managed => "Managed",
|
|
||||||
PoolType::Pinned => "Pinned",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this pool type requires GPU support
|
|
||||||
pub fn requires_gpu(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
PoolType::Host => false,
|
|
||||||
_ => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get recommended alignment for this pool type
|
|
||||||
pub fn alignment(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
PoolType::Device => 256, // GPU prefers 256-byte alignment
|
|
||||||
PoolType::Unified => 256,
|
|
||||||
PoolType::Managed => 256,
|
|
||||||
PoolType::Pinned => 64, // CPU cache line
|
|
||||||
PoolType::Host => 64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get maximum allocation size hint
|
|
||||||
pub fn max_allocation_size(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
PoolType::Device => 16 * 1024 * 1024 * 1024, // 16GB typical GPU
|
|
||||||
PoolType::Host => usize::MAX, // Limited by system
|
|
||||||
PoolType::Unified => 8 * 1024 * 1024 * 1024, // 8GB typical
|
|
||||||
PoolType::Managed => 8 * 1024 * 1024 * 1024,
|
|
||||||
PoolType::Pinned => 1 * 1024 * 1024 * 1024, // 1GB pinned is reasonable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Memory pool for managing allocations of a specific type
|
|
||||||
pub struct MemoryPool {
|
|
||||||
pool_type: PoolType,
|
|
||||||
allocator: Arc<RwLock<MemoryAllocator>>,
|
|
||||||
allocations: RwLock<HashMap<usize, AllocationInfo>>,
|
|
||||||
total_allocated: RwLock<usize>,
|
|
||||||
peak_allocated: RwLock<usize>,
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
cuda_device: Option<Arc<CudaDevice>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoryPool {
|
|
||||||
/// Create a new memory pool
|
|
||||||
pub fn new(pool_type: PoolType, initial_size: usize) -> Result<Self> {
|
|
||||||
info!("Creating {} memory pool with initial size {}", pool_type.name(), initial_size);
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
let cuda_device = if pool_type.requires_gpu() {
|
|
||||||
Some(Arc::new(CudaDevice::new(0)
|
|
||||||
.map_err(|e| MemoryError::InitializationError(format!("Failed to init CUDA: {}", e)))?))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let allocator = Arc::new(RwLock::new(
|
|
||||||
MemoryAllocator::new(initial_size, pool_type.alignment())?
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
pool_type,
|
|
||||||
allocator,
|
|
||||||
allocations: RwLock::new(HashMap::new()),
|
|
||||||
total_allocated: RwLock::new(0),
|
|
||||||
peak_allocated: RwLock::new(0),
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
cuda_device,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate memory from the pool
|
|
||||||
pub fn allocate(&self, size: usize) -> Result<*mut u8> {
|
|
||||||
let aligned_size = align_size(size, self.pool_type.alignment());
|
|
||||||
|
|
||||||
debug!("Allocating {} bytes from {} pool", aligned_size, self.pool_type.name());
|
|
||||||
|
|
||||||
let ptr = match self.pool_type {
|
|
||||||
PoolType::Device => self.allocate_device(aligned_size)?,
|
|
||||||
PoolType::Host => self.allocate_host(aligned_size)?,
|
|
||||||
PoolType::Unified => self.allocate_unified(aligned_size)?,
|
|
||||||
PoolType::Managed => self.allocate_managed(aligned_size)?,
|
|
||||||
PoolType::Pinned => self.allocate_pinned(aligned_size)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Track allocation
|
|
||||||
let mut allocations = self.allocations.write();
|
|
||||||
allocations.insert(ptr as usize, AllocationInfo {
|
|
||||||
ptr: ptr as usize,
|
|
||||||
size: aligned_size,
|
|
||||||
pool_type: self.pool_type,
|
|
||||||
timestamp: std::time::Instant::now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update statistics
|
|
||||||
let mut total = self.total_allocated.write();
|
|
||||||
*total += aligned_size;
|
|
||||||
|
|
||||||
let mut peak = self.peak_allocated.write();
|
|
||||||
if *total > *peak {
|
|
||||||
*peak = *total;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deallocate memory back to the pool
|
|
||||||
pub fn deallocate(&self, ptr: *mut u8) -> Result<()> {
|
|
||||||
let mut allocations = self.allocations.write();
|
|
||||||
|
|
||||||
if let Some(info) = allocations.remove(&(ptr as usize)) {
|
|
||||||
debug!("Deallocating {} bytes from {} pool", info.size, self.pool_type.name());
|
|
||||||
|
|
||||||
match self.pool_type {
|
|
||||||
PoolType::Device => self.deallocate_device(ptr, info.size)?,
|
|
||||||
PoolType::Host => self.deallocate_host(ptr)?,
|
|
||||||
PoolType::Unified => self.deallocate_unified(ptr)?,
|
|
||||||
PoolType::Managed => self.deallocate_managed(ptr)?,
|
|
||||||
PoolType::Pinned => self.deallocate_pinned(ptr)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut total = self.total_allocated.write();
|
|
||||||
*total = total.saturating_sub(info.size);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(MemoryError::InvalidPointer(format!("Pointer {:p} not found in pool", ptr)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current allocated size
|
|
||||||
pub fn allocated_size(&self) -> usize {
|
|
||||||
*self.total_allocated.read()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get peak allocated size
|
|
||||||
pub fn peak_allocated_size(&self) -> usize {
|
|
||||||
*self.peak_allocated.read()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get number of active allocations
|
|
||||||
pub fn allocation_count(&self) -> usize {
|
|
||||||
self.allocations.read().len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private allocation methods
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn allocate_device(&self, size: usize) -> Result<*mut u8> {
|
|
||||||
let device = self.cuda_device.as_ref()
|
|
||||||
.ok_or_else(|| MemoryError::InitializationError("CUDA device not initialized".into()))?;
|
|
||||||
|
|
||||||
let slice = device.alloc_zeros::<u8>(size)
|
|
||||||
.map_err(|e| MemoryError::AllocationFailed(format!("Device allocation failed: {}", e)))?;
|
|
||||||
|
|
||||||
// Get raw pointer from CudaSlice
|
|
||||||
let ptr = slice.device_ptr() as *mut u8;
|
|
||||||
|
|
||||||
// Store the slice to keep it alive
|
|
||||||
// In production, we'd need proper lifetime management
|
|
||||||
std::mem::forget(slice);
|
|
||||||
|
|
||||||
Ok(ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cuda"))]
|
|
||||||
fn allocate_device(&self, _size: usize) -> Result<*mut u8> {
|
|
||||||
Err(MemoryError::UnsupportedOperation("CUDA not enabled".into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allocate_host(&self, size: usize) -> Result<*mut u8> {
|
|
||||||
let layout = std::alloc::Layout::from_size_align(size, self.pool_type.alignment())
|
|
||||||
.map_err(|e| MemoryError::AllocationFailed(format!("Invalid layout: {}", e)))?;
|
|
||||||
|
|
||||||
// SAFETY: alloc allocates memory with the specified layout.
|
|
||||||
// - layout is valid (created from size and alignment above)
|
|
||||||
// - The resulting pointer is checked for null before use
|
|
||||||
let ptr = unsafe { std::alloc::alloc(layout) };
|
|
||||||
|
|
||||||
if ptr.is_null() {
|
|
||||||
Err(MemoryError::AllocationFailed(format!("Failed to allocate {} bytes", size)))
|
|
||||||
} else {
|
|
||||||
Ok(ptr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn allocate_unified(&self, size: usize) -> Result<*mut u8> {
|
|
||||||
let device = self.cuda_device.as_ref()
|
|
||||||
.ok_or_else(|| MemoryError::InitializationError("CUDA device not initialized".into()))?;
|
|
||||||
|
|
||||||
// Use CUDA unified memory
|
|
||||||
let slice = device.alloc_unified_zeros::<u8>(size)
|
|
||||||
.map_err(|e| MemoryError::AllocationFailed(format!("Unified allocation failed: {}", e)))?;
|
|
||||||
|
|
||||||
let ptr = slice.as_ptr() as *mut u8;
|
|
||||||
std::mem::forget(slice);
|
|
||||||
|
|
||||||
Ok(ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cuda"))]
|
|
||||||
fn allocate_unified(&self, _size: usize) -> Result<*mut u8> {
|
|
||||||
Err(MemoryError::UnsupportedOperation("CUDA not enabled".into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allocate_managed(&self, size: usize) -> Result<*mut u8> {
|
|
||||||
// Managed memory is similar to unified in cudarc
|
|
||||||
self.allocate_unified(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn allocate_pinned(&self, size: usize) -> Result<*mut u8> {
|
|
||||||
let device = self.cuda_device.as_ref()
|
|
||||||
.ok_or_else(|| MemoryError::InitializationError("CUDA device not initialized".into()))?;
|
|
||||||
|
|
||||||
// Allocate pinned host memory
|
|
||||||
let slice = device.alloc_pinned_zeros::<u8>(size)
|
|
||||||
.map_err(|e| MemoryError::AllocationFailed(format!("Pinned allocation failed: {}", e)))?;
|
|
||||||
|
|
||||||
let ptr = slice.as_ptr() as *mut u8;
|
|
||||||
std::mem::forget(slice);
|
|
||||||
|
|
||||||
Ok(ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cuda"))]
|
|
||||||
fn allocate_pinned(&self, size: usize) -> Result<*mut u8> {
|
|
||||||
// Fall back to regular host allocation
|
|
||||||
self.allocate_host(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deallocation methods
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn deallocate_device(&self, _ptr: *mut u8, _size: usize) -> Result<()> {
|
|
||||||
// In production, we'd properly manage CudaSlice lifetimes
|
|
||||||
// For now, memory is freed when the slice is dropped
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cuda"))]
|
|
||||||
fn deallocate_device(&self, _ptr: *mut u8, _size: usize) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deallocate_host(&self, ptr: *mut u8) -> Result<()> {
|
|
||||||
if let Some(info) = self.allocations.read().get(&(ptr as usize)) {
|
|
||||||
let layout = std::alloc::Layout::from_size_align(info.size, self.pool_type.alignment())
|
|
||||||
.map_err(|e| MemoryError::DeallocationFailed(format!("Invalid layout: {}", e)))?;
|
|
||||||
|
|
||||||
// SAFETY: dealloc frees memory previously allocated with alloc.
|
|
||||||
// - ptr was obtained from alloc with the same layout (from allocations map)
|
|
||||||
// - The layout matches the original allocation
|
|
||||||
unsafe {
|
|
||||||
std::alloc::dealloc(ptr, layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deallocate_unified(&self, _ptr: *mut u8) -> Result<()> {
|
|
||||||
// Unified memory freed when CudaSlice drops
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deallocate_managed(&self, ptr: *mut u8) -> Result<()> {
|
|
||||||
self.deallocate_unified(ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deallocate_pinned(&self, ptr: *mut u8) -> Result<()> {
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
{
|
|
||||||
// Pinned memory freed when CudaSlice drops
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "cuda"))]
|
|
||||||
{
|
|
||||||
self.deallocate_host(ptr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Align size to the given alignment
|
|
||||||
fn align_size(size: usize, alignment: usize) -> usize {
|
|
||||||
(size + alignment - 1) & !(alignment - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Global pool manager for different pool types
|
|
||||||
pub struct PoolManager {
|
|
||||||
pools: RwLock<HashMap<PoolType, Arc<MemoryPool>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PoolManager {
|
|
||||||
/// Create a new pool manager
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
pools: RwLock::new(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get or create a pool for the given type
|
|
||||||
pub fn get_pool(&self, pool_type: PoolType) -> Result<Arc<MemoryPool>> {
|
|
||||||
let mut pools = self.pools.write();
|
|
||||||
|
|
||||||
if let Some(pool) = pools.get(&pool_type) {
|
|
||||||
Ok(pool.clone())
|
|
||||||
} else {
|
|
||||||
let initial_size = match pool_type {
|
|
||||||
PoolType::Device => 256 * 1024 * 1024, // 256MB initial
|
|
||||||
PoolType::Host => 64 * 1024 * 1024, // 64MB initial
|
|
||||||
_ => 128 * 1024 * 1024, // 128MB for others
|
|
||||||
};
|
|
||||||
|
|
||||||
let pool = Arc::new(MemoryPool::new(pool_type, initial_size)?);
|
|
||||||
pools.insert(pool_type, pool.clone());
|
|
||||||
|
|
||||||
info!("Created new {} pool with {} MB initial size",
|
|
||||||
pool_type.name(), initial_size / (1024 * 1024));
|
|
||||||
|
|
||||||
Ok(pool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get statistics for all pools
|
|
||||||
pub fn get_statistics(&self) -> HashMap<PoolType, PoolStatistics> {
|
|
||||||
let pools = self.pools.read();
|
|
||||||
let mut stats = HashMap::new();
|
|
||||||
|
|
||||||
for (pool_type, pool) in pools.iter() {
|
|
||||||
stats.insert(*pool_type, PoolStatistics {
|
|
||||||
allocated: pool.allocated_size(),
|
|
||||||
peak_allocated: pool.peak_allocated_size(),
|
|
||||||
allocation_count: pool.allocation_count(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
stats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pool statistics
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PoolStatistics {
|
|
||||||
pub allocated: usize,
|
|
||||||
pub peak_allocated: usize,
|
|
||||||
pub allocation_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pool_type_properties() {
|
|
||||||
assert_eq!(PoolType::Device.name(), "Device");
|
|
||||||
assert!(PoolType::Device.requires_gpu());
|
|
||||||
assert_eq!(PoolType::Device.alignment(), 256);
|
|
||||||
|
|
||||||
assert_eq!(PoolType::Host.name(), "Host");
|
|
||||||
assert!(!PoolType::Host.requires_gpu());
|
|
||||||
assert_eq!(PoolType::Host.alignment(), 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_align_size() {
|
|
||||||
assert_eq!(align_size(100, 64), 128);
|
|
||||||
assert_eq!(align_size(64, 64), 64);
|
|
||||||
assert_eq!(align_size(1, 256), 256);
|
|
||||||
assert_eq!(align_size(257, 256), 512);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_host_pool_allocation() {
|
|
||||||
let pool = MemoryPool::new(PoolType::Host, 1024 * 1024).unwrap();
|
|
||||||
|
|
||||||
let ptr = pool.allocate(1024).unwrap();
|
|
||||||
assert!(!ptr.is_null());
|
|
||||||
assert_eq!(pool.allocation_count(), 1);
|
|
||||||
assert!(pool.allocated_size() >= 1024);
|
|
||||||
|
|
||||||
pool.deallocate(ptr).unwrap();
|
|
||||||
assert_eq!(pool.allocation_count(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pool_manager() {
|
|
||||||
let manager = PoolManager::new();
|
|
||||||
|
|
||||||
let pool1 = manager.get_pool(PoolType::Host).unwrap();
|
|
||||||
let pool2 = manager.get_pool(PoolType::Host).unwrap();
|
|
||||||
|
|
||||||
// Should return the same pool
|
|
||||||
assert!(Arc::ptr_eq(&pool1, &pool2));
|
|
||||||
|
|
||||||
let stats = manager.get_statistics();
|
|
||||||
assert!(stats.contains_key(&PoolType::Host));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_allocations() {
|
|
||||||
let pool = MemoryPool::new(PoolType::Host, 10 * 1024 * 1024).unwrap();
|
|
||||||
let mut ptrs = Vec::new();
|
|
||||||
|
|
||||||
// Allocate multiple blocks
|
|
||||||
for i in 0..10 {
|
|
||||||
let size = (i + 1) * 1024;
|
|
||||||
let ptr = pool.allocate(size).unwrap();
|
|
||||||
assert!(!ptr.is_null());
|
|
||||||
ptrs.push(ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(pool.allocation_count(), 10);
|
|
||||||
|
|
||||||
// Deallocate all
|
|
||||||
for ptr in ptrs {
|
|
||||||
pool.deallocate(ptr).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(pool.allocation_count(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_peak_allocation_tracking() {
|
|
||||||
let pool = MemoryPool::new(PoolType::Host, 10 * 1024 * 1024).unwrap();
|
|
||||||
|
|
||||||
let ptr1 = pool.allocate(1024).unwrap();
|
|
||||||
let peak1 = pool.peak_allocated_size();
|
|
||||||
|
|
||||||
let ptr2 = pool.allocate(2048).unwrap();
|
|
||||||
let peak2 = pool.peak_allocated_size();
|
|
||||||
assert!(peak2 > peak1);
|
|
||||||
|
|
||||||
pool.deallocate(ptr1).unwrap();
|
|
||||||
let peak3 = pool.peak_allocated_size();
|
|
||||||
assert_eq!(peak2, peak3); // Peak should not decrease
|
|
||||||
|
|
||||||
pool.deallocate(ptr2).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
//! Production CUDA Backend Implementation
|
|
||||||
//!
|
|
||||||
//! This module implements GPU operations using cudarc 0.17.3 safe APIs, replacing
|
|
||||||
//! all unsafe sys calls with production-quality safe abstractions. It provides
|
|
||||||
//! comprehensive CUDA context management, memory operations, stream handling,
|
|
||||||
//! and kernel execution with proper error handling and resource cleanup.
|
|
||||||
//!
|
|
||||||
//! # Safety
|
|
||||||
//!
|
|
||||||
//! This implementation eliminates unsafe code by leveraging cudarc's safe APIs.
|
|
||||||
//! All GPU operations are properly encapsulated with RAII patterns and comprehensive
|
|
||||||
//! error handling using Result types.
|
|
||||||
|
|
||||||
use crate::device::*;
|
|
||||||
use crate::error::{RuntimeError, Result};
|
|
||||||
use crate::allocator::DevicePtr;
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap};
|
|
||||||
use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicBool, Ordering}};
|
|
||||||
use tracing::{debug, info, error, warn};
|
|
||||||
use anyhow::{Context, bail};
|
|
||||||
|
|
||||||
/// Global CUDA initialization state
|
|
||||||
static CUDA_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
|
||||||
|
|
||||||
/// Production CUDA backend with complete resource management
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CudaBackend {
|
|
||||||
device_id: DeviceId,
|
|
||||||
// In a real implementation, this would hold cudarc device handles
|
|
||||||
// For now, we'll use simplified placeholders that demonstrate the API
|
|
||||||
is_initialized: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CudaBackend {
|
|
||||||
/// Create a new CUDA backend for the given device
|
|
||||||
///
|
|
||||||
/// This initializes the CUDA device with proper context management and
|
|
||||||
/// creates all necessary cuBLAS and cuRAND handles with RAII cleanup.
|
|
||||||
pub fn new(device_id: DeviceId) -> Result<Self> {
|
|
||||||
initialize_cuda()?;
|
|
||||||
|
|
||||||
// In production, would initialize actual cudarc device here
|
|
||||||
info!("Initialized CUDA backend for device {}", device_id.0);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
device_id,
|
|
||||||
is_initialized: Arc::new(AtomicBool::new(true)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the device ID
|
|
||||||
pub fn device_id(&self) -> DeviceId {
|
|
||||||
self.device_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate GPU memory using cudarc's safe allocator
|
|
||||||
///
|
|
||||||
/// This method uses cudarc's built-in memory allocator which provides
|
|
||||||
/// proper RAII cleanup and eliminates manual memory management errors.
|
|
||||||
pub fn allocate_memory(&self, size: usize) -> Result<DevicePtr> {
|
|
||||||
// In production, would use real cudarc memory allocation
|
|
||||||
let device_ptr = unsafe { DevicePtr::from_raw((size as u64) + (self.device_id.0 as u64 * 0x1000000)) };
|
|
||||||
|
|
||||||
debug!("Allocated {} bytes at {:#x} on device {}",
|
|
||||||
size, device_ptr.as_raw(), self.device_id.0);
|
|
||||||
|
|
||||||
Ok(device_ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get available GPU memory in bytes
|
|
||||||
pub fn get_available_memory(&self) -> Result<usize> {
|
|
||||||
// Mock implementation - in production would query actual GPU memory
|
|
||||||
let free = 8 * 1024 * 1024 * 1024; // 8GB mock
|
|
||||||
debug!("Device {} has {} bytes free memory", self.device_id.0, free);
|
|
||||||
Ok(free)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total GPU memory in bytes
|
|
||||||
pub fn get_total_memory(&self) -> Result<usize> {
|
|
||||||
// Mock implementation - in production would query actual GPU memory
|
|
||||||
Ok(8 * 1024 * 1024 * 1024) // 8GB mock
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get device properties and capabilities
|
|
||||||
pub fn get_device_info(&self) -> Result<DeviceProperties> {
|
|
||||||
get_cuda_device_properties(self.device_id.0 as i32)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new CUDA stream
|
|
||||||
pub fn create_stream(&self) -> Result<CudaStreamHandle> {
|
|
||||||
debug!("Created new CUDA stream on device {}", self.device_id.0);
|
|
||||||
|
|
||||||
Ok(CudaStreamHandle {
|
|
||||||
device_id: self.device_id.0 as i32,
|
|
||||||
is_valid: Arc::new(AtomicBool::new(true)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new CUDA event
|
|
||||||
pub fn create_event(&self) -> Result<CudaEventHandle> {
|
|
||||||
debug!("Created new CUDA event on device {}", self.device_id.0);
|
|
||||||
|
|
||||||
Ok(CudaEventHandle {
|
|
||||||
device_id: self.device_id.0 as i32,
|
|
||||||
is_valid: Arc::new(AtomicBool::new(true)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Synchronize the device
|
|
||||||
pub fn synchronize(&self) -> Result<()> {
|
|
||||||
debug!("Synchronized device {}", self.device_id.0);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get cuBLAS handle for matrix operations (placeholder)
|
|
||||||
pub fn cublas_handle(&self) -> Option<Arc<Mutex<u64>>> {
|
|
||||||
Some(Arc::new(Mutex::new(0x1000))) // Placeholder handle
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get cuRAND handle for random number generation (placeholder)
|
|
||||||
pub fn curand_handle(&self) -> Option<Arc<Mutex<u64>>> {
|
|
||||||
Some(Arc::new(Mutex::new(0x2000))) // Placeholder handle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Device manager for handling multiple GPU devices with proper resource management
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DeviceManager {
|
|
||||||
devices: RwLock<BTreeMap<DeviceId, Arc<CudaBackend>>>,
|
|
||||||
device_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeviceManager {
|
|
||||||
/// Create a new device manager and discover all available CUDA devices
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
initialize_cuda()?;
|
|
||||||
|
|
||||||
// Mock device discovery - in production would use cudarc device enumeration
|
|
||||||
let device_count = 1; // Assume at least one device for testing
|
|
||||||
info!("Discovered {} CUDA devices", device_count);
|
|
||||||
|
|
||||||
let mut devices = BTreeMap::new();
|
|
||||||
|
|
||||||
// Initialize mock devices
|
|
||||||
for i in 0..device_count {
|
|
||||||
let device_id = DeviceId(i as u32);
|
|
||||||
match CudaBackend::new(device_id) {
|
|
||||||
Ok(backend) => {
|
|
||||||
devices.insert(device_id, Arc::new(backend));
|
|
||||||
debug!("Successfully initialized device {}", i);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to initialize device {}: {}", i, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
devices: RwLock::new(devices),
|
|
||||||
device_count,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the number of available devices
|
|
||||||
pub fn device_count(&self) -> usize {
|
|
||||||
self.device_count
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a backend for the specified device
|
|
||||||
pub fn get_backend(&self, device_id: DeviceId) -> Result<Arc<CudaBackend>> {
|
|
||||||
let devices = self.devices.read().unwrap();
|
|
||||||
devices.get(&device_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| RuntimeError::device_error(
|
|
||||||
device_id.0,
|
|
||||||
format!("Device {} not available", device_id.0)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all available device backends
|
|
||||||
pub fn get_all_backends(&self) -> Vec<(DeviceId, Arc<CudaBackend>)> {
|
|
||||||
let devices = self.devices.read().unwrap();
|
|
||||||
devices.iter().map(|(&id, backend)| (id, backend.clone())).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DeviceManager {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new().expect("Failed to initialize device manager")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CUDA stream wrapper with proper RAII cleanup
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CudaStreamHandle {
|
|
||||||
device_id: i32,
|
|
||||||
is_valid: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CudaStreamHandle {
|
|
||||||
/// Get the device ID this stream belongs to
|
|
||||||
pub fn device_id(&self) -> i32 {
|
|
||||||
self.device_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Synchronize this stream
|
|
||||||
pub fn synchronize(&self) -> Result<()> {
|
|
||||||
if !self.is_valid.load(Ordering::Acquire) {
|
|
||||||
return Err(RuntimeError::stream_error(
|
|
||||||
self.device_id as u32,
|
|
||||||
"Stream is no longer valid".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Synchronized stream on device {}", self.device_id);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if stream is completed
|
|
||||||
pub fn query(&self) -> Result<bool> {
|
|
||||||
if !self.is_valid.load(Ordering::Acquire) {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock implementation - in production would query actual stream
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make this stream wait for an event
|
|
||||||
pub fn wait_event(&self, event: &CudaEventHandle) -> Result<()> {
|
|
||||||
debug!("Stream on device {} waiting for event", self.device_id);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for CudaStreamHandle {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
device_id: self.device_id,
|
|
||||||
is_valid: self.is_valid.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CUDA event wrapper with proper RAII cleanup
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CudaEventHandle {
|
|
||||||
device_id: i32,
|
|
||||||
is_valid: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CudaEventHandle {
|
|
||||||
/// Get the device ID this event belongs to
|
|
||||||
pub fn device_id(&self) -> i32 {
|
|
||||||
self.device_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record this event on a stream
|
|
||||||
pub fn record(&self, stream: &CudaStreamHandle) -> Result<()> {
|
|
||||||
debug!("Recorded event on stream for device {}", self.device_id);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Synchronize on this event (wait for completion)
|
|
||||||
pub fn synchronize(&self) -> Result<()> {
|
|
||||||
debug!("Synchronized event on device {}", self.device_id);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if event has completed
|
|
||||||
pub fn query(&self) -> Result<bool> {
|
|
||||||
if !self.is_valid.load(Ordering::Acquire) {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate elapsed time between two events in milliseconds
|
|
||||||
pub fn elapsed_time(&self, end_event: &CudaEventHandle) -> Result<f32> {
|
|
||||||
// Mock implementation
|
|
||||||
Ok(0.5) // 0.5ms mock execution time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for CudaEventHandle {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
device_id: self.device_id,
|
|
||||||
is_valid: self.is_valid.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize CUDA driver with proper error handling
|
|
||||||
pub fn initialize_cuda() -> Result<()> {
|
|
||||||
if CUDA_INITIALIZED.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
|
|
||||||
// Already initialized
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock initialization - in production would use cudarc::driver::init()
|
|
||||||
info!("CUDA driver initialized successfully (mock implementation)");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Discover CUDA devices using cudarc safe APIs
|
|
||||||
pub fn discover_cuda_devices(devices: &mut BTreeMap<DeviceId, Device>) -> Result<usize> {
|
|
||||||
initialize_cuda()?;
|
|
||||||
|
|
||||||
let device_count = 1; // Mock discovery
|
|
||||||
info!("Found {} CUDA devices", device_count);
|
|
||||||
|
|
||||||
for cuda_device_id in 0..device_count {
|
|
||||||
let device_id = DeviceId(cuda_device_id as u32);
|
|
||||||
let properties = get_cuda_device_properties(cuda_device_id as i32)?;
|
|
||||||
let device_name = properties.name.clone();
|
|
||||||
let device = Device::new(device_id, properties)?;
|
|
||||||
devices.insert(device_id, device);
|
|
||||||
debug!("Added CUDA device {}: {}", device_id, device_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(device_count)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get CUDA device properties using mock data (in production would use cudarc)
|
|
||||||
fn get_cuda_device_properties(cuda_device_id: i32) -> Result<DeviceProperties> {
|
|
||||||
let name = format!("Mock CUDA Device {}", cuda_device_id);
|
|
||||||
let total_memory: u64 = 8 * 1024 * 1024 * 1024; // 8GB
|
|
||||||
let memory_bandwidth_gb_s = 900.0;
|
|
||||||
|
|
||||||
Ok(DeviceProperties {
|
|
||||||
name,
|
|
||||||
backend: BackendType::Cuda,
|
|
||||||
compute_capability: (8, 9), // SM 89 for RTX 4090/5090
|
|
||||||
total_memory: total_memory as u64,
|
|
||||||
memory_bandwidth_gb_s,
|
|
||||||
multiprocessor_count: 128,
|
|
||||||
max_threads_per_block: 1024,
|
|
||||||
shared_memory_per_block: 49152,
|
|
||||||
warp_size: 32,
|
|
||||||
supports_unified_memory: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Launch a CUDA kernel with the specified configuration
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// This function is unsafe as it directly interfaces with CUDA runtime
|
|
||||||
pub unsafe fn cuda_launch_kernel(
|
|
||||||
module: *const std::ffi::c_void,
|
|
||||||
kernel_name: &str,
|
|
||||||
grid_dims: (u32, u32, u32),
|
|
||||||
block_dims: (u32, u32, u32),
|
|
||||||
shared_mem_bytes: u32,
|
|
||||||
stream: &CudaStreamHandle,
|
|
||||||
params: &[u8],
|
|
||||||
) -> Result<()> {
|
|
||||||
// In production, this would use cudarc::driver::launch_kernel
|
|
||||||
// For now, we provide a stub implementation
|
|
||||||
debug!("Launching kernel {} with grid {:?} block {:?}",
|
|
||||||
kernel_name, grid_dims, block_dims);
|
|
||||||
|
|
||||||
if !stream.is_valid.load(Ordering::Acquire) {
|
|
||||||
return Err(RuntimeError::kernel_error("Invalid CUDA stream"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate kernel launch - in production would use cudarc
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load a CUDA module from PTX data
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// This function is unsafe as it directly loads CUDA code
|
|
||||||
pub unsafe fn cuda_module_load_data(ptx_data: *const std::ffi::c_char) -> Result<*const std::ffi::c_void> {
|
|
||||||
// In production, this would use cudarc::driver::Module::load_ptx
|
|
||||||
// For now, we provide a stub implementation
|
|
||||||
debug!("Loading CUDA module from PTX");
|
|
||||||
|
|
||||||
if ptx_data.is_null() {
|
|
||||||
return Err(RuntimeError::kernel_error("PTX data is null"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a dummy pointer - in production would return actual module handle
|
|
||||||
Ok(1 as *const std::ffi::c_void)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cuda_initialization() {
|
|
||||||
let result = initialize_cuda();
|
|
||||||
assert!(result.is_ok(), "CUDA initialization should succeed");
|
|
||||||
|
|
||||||
// Second call should also succeed (already initialized)
|
|
||||||
let result2 = initialize_cuda();
|
|
||||||
assert!(result2.is_ok(), "Second CUDA initialization should succeed");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_device_discovery() {
|
|
||||||
let mut devices = BTreeMap::new();
|
|
||||||
let count = discover_cuda_devices(&mut devices);
|
|
||||||
|
|
||||||
assert!(count.is_ok(), "Device discovery should succeed");
|
|
||||||
let device_count = count.unwrap();
|
|
||||||
assert!(device_count > 0, "Should discover at least one CUDA device");
|
|
||||||
assert_eq!(devices.len(), device_count, "Device map should contain all discovered devices");
|
|
||||||
|
|
||||||
// Check that we got real device properties
|
|
||||||
for device in devices.values() {
|
|
||||||
assert_eq!(device.properties.backend, BackendType::Cuda);
|
|
||||||
assert!(!device.properties.name.is_empty());
|
|
||||||
assert!(device.properties.total_memory > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_device_manager_creation() {
|
|
||||||
let result = DeviceManager::new();
|
|
||||||
assert!(result.is_ok(), "DeviceManager creation should succeed");
|
|
||||||
let manager = result.unwrap();
|
|
||||||
assert!(manager.device_count() > 0, "Should have at least one device");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cuda_backend_creation() {
|
|
||||||
let result = CudaBackend::new(DeviceId(0));
|
|
||||||
assert!(result.is_ok(), "Should be able to create CUDA backend");
|
|
||||||
|
|
||||||
if let Ok(backend) = result {
|
|
||||||
assert_eq!(backend.device_id(), DeviceId(0));
|
|
||||||
|
|
||||||
// Test memory operations
|
|
||||||
let memory_result = backend.get_available_memory();
|
|
||||||
assert!(memory_result.is_ok(), "Should be able to query memory");
|
|
||||||
|
|
||||||
// Test stream creation
|
|
||||||
let stream_result = backend.create_stream();
|
|
||||||
assert!(stream_result.is_ok(), "Should be able to create stream");
|
|
||||||
|
|
||||||
// Test event creation
|
|
||||||
let event_result = backend.create_event();
|
|
||||||
assert!(event_result.is_ok(), "Should be able to create event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,554 +0,0 @@
|
|||||||
//! CUDA Kernel Operations using cudarc v0.17.3
|
|
||||||
//!
|
|
||||||
//! This module provides comprehensive kernel management and execution
|
|
||||||
//! using cudarc's safe APIs. No placeholders, full implementations only.
|
|
||||||
|
|
||||||
use crate::error::{RuntimeError, Result};
|
|
||||||
use crate::cuda_backend::CudaStreamHandle;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::sync::atomic::AtomicU64;
|
|
||||||
use tracing::{info};
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
use cudarc::driver::{CudaContext, CudaModule, CudaFunction, CudaStream, LaunchConfig as CudarcLaunchConfig};
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
use cudarc::nvrtc::{compile_ptx, CompileOptions};
|
|
||||||
|
|
||||||
/// Global module registry for managing loaded PTX modules
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref GLOBAL_MODULES: RwLock<HashMap<u64, Arc<LoadedModule>>> = RwLock::new(HashMap::new());
|
|
||||||
static ref MODULE_COUNTER: AtomicU64 = AtomicU64::new(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loaded CUDA module with metadata
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct LoadedModule {
|
|
||||||
pub id: u64,
|
|
||||||
pub name: String,
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
pub module: CudaModule,
|
|
||||||
pub functions: HashMap<String, FunctionInfo>,
|
|
||||||
pub ptx_source: String,
|
|
||||||
pub compile_time_ms: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Function information within a module
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct FunctionInfo {
|
|
||||||
pub name: String,
|
|
||||||
pub max_threads_per_block: u32,
|
|
||||||
pub shared_mem_bytes: u32,
|
|
||||||
pub const_mem_bytes: u32,
|
|
||||||
pub local_mem_bytes: u32,
|
|
||||||
pub num_regs: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Kernel launch configuration
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct KernelLaunchConfig {
|
|
||||||
pub grid_dim: (u32, u32, u32),
|
|
||||||
pub block_dim: (u32, u32, u32),
|
|
||||||
pub shared_mem_bytes: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for KernelLaunchConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
grid_dim: (1, 1, 1),
|
|
||||||
block_dim: (256, 1, 1),
|
|
||||||
shared_mem_bytes: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Launch a CUDA kernel with full parameter support
|
|
||||||
///
|
|
||||||
/// This function provides complete kernel launching using cudarc's safe APIs.
|
|
||||||
/// It handles PTX compilation, module loading, and parameter marshalling.
|
|
||||||
pub fn cuda_launch_kernel(
|
|
||||||
_binary_handle: u64,
|
|
||||||
_function_name: &str,
|
|
||||||
_grid_size: (u32, u32, u32),
|
|
||||||
_block_size: (u32, u32, u32),
|
|
||||||
_shared_memory_bytes: u32,
|
|
||||||
_stream: &CudaStreamHandle,
|
|
||||||
_params: &[u8],
|
|
||||||
) -> Result<()> {
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
{
|
|
||||||
// Retrieve the loaded module from global registry
|
|
||||||
let modules = GLOBAL_MODULES.read()
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Failed to acquire module lock: {}", e)))?;
|
|
||||||
|
|
||||||
let loaded_module = modules.get(&binary_handle)
|
|
||||||
.ok_or_else(|| RuntimeError::kernel_error(format!("Module handle {} not found", binary_handle)))?;
|
|
||||||
|
|
||||||
// Get the function from the module
|
|
||||||
let func_info = loaded_module.functions.get(function_name)
|
|
||||||
.ok_or_else(|| RuntimeError::kernel_error(format!("Function {} not found in module", function_name)))?;
|
|
||||||
|
|
||||||
// Validate launch configuration
|
|
||||||
validate_launch_config(grid_size, block_size, shared_memory_bytes, func_info)?;
|
|
||||||
|
|
||||||
// Get the actual CUDA function
|
|
||||||
let cuda_func = loaded_module.module.get_function(function_name)
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Failed to get function: {}", e)))?;
|
|
||||||
|
|
||||||
// Parse and prepare parameters
|
|
||||||
let kernel_params = parse_kernel_params(params)?;
|
|
||||||
|
|
||||||
// Build launch configuration
|
|
||||||
let config = CudarcLaunchConfig {
|
|
||||||
grid_dim: grid_size,
|
|
||||||
block_dim: block_size,
|
|
||||||
shared_mem_bytes: shared_memory_bytes,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Launch the kernel using cudarc's safe API
|
|
||||||
let stream_ptr = stream.as_cuda_stream()?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
// Launch with parsed parameters
|
|
||||||
launch_with_params(&cuda_func, config, &kernel_params, stream_ptr)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Launched kernel {} with grid {:?}, block {:?}",
|
|
||||||
function_name, grid_size, block_size);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cuda"))]
|
|
||||||
{
|
|
||||||
Err(RuntimeError::backend_error(
|
|
||||||
"CUDA",
|
|
||||||
Box::new(std::io::Error::new(std::io::ErrorKind::Other, "CUDA support not compiled in"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load a PTX module and return a handle
|
|
||||||
pub fn cuda_module_load_data(ptx_data: *const std::ffi::c_char) -> Result<u64> {
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
{
|
|
||||||
// Convert C string to Rust string
|
|
||||||
let ptx_str = unsafe {
|
|
||||||
std::ffi::CStr::from_ptr(ptx_data)
|
|
||||||
.to_str()
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Invalid PTX string: {}", e)))?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the current CUDA device
|
|
||||||
let device = get_current_cuda_device()?;
|
|
||||||
|
|
||||||
// Load the PTX module
|
|
||||||
let module_name = format!("module_{}", MODULE_COUNTER.fetch_add(1, Ordering::SeqCst));
|
|
||||||
let module = device.load_ptx(ptx_str.into(), &module_name, &[])
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Failed to load PTX: {}", e)))?;
|
|
||||||
|
|
||||||
// Extract function information from the module
|
|
||||||
let functions = extract_function_info(&module, ptx_str)?;
|
|
||||||
|
|
||||||
// Create module handle
|
|
||||||
let handle = MODULE_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
||||||
|
|
||||||
// Store in global registry
|
|
||||||
let loaded_module = Arc::new(LoadedModule {
|
|
||||||
id: handle,
|
|
||||||
name: module_name,
|
|
||||||
module,
|
|
||||||
functions,
|
|
||||||
ptx_source: ptx_str.to_string(),
|
|
||||||
compile_time_ms: 0, // Already compiled
|
|
||||||
});
|
|
||||||
|
|
||||||
GLOBAL_MODULES.write()
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Failed to acquire module lock: {}", e)))?
|
|
||||||
.insert(handle, loaded_module);
|
|
||||||
|
|
||||||
info!("Loaded PTX module with handle {}", handle);
|
|
||||||
Ok(handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cuda"))]
|
|
||||||
{
|
|
||||||
Err(RuntimeError::backend_error(
|
|
||||||
"CUDA",
|
|
||||||
Box::new(std::io::Error::new(std::io::ErrorKind::Other, "CUDA support not compiled in"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compile CUDA source code to PTX and load it
|
|
||||||
pub fn compile_and_load_cuda(cuda_source: &str, function_names: &[&str]) -> Result<u64> {
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
{
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
|
|
||||||
// Compile CUDA to PTX using nvrtc
|
|
||||||
let compile_opts = CompileOptions {
|
|
||||||
arch: Some("sm_70".to_string()), // Minimum for modern GPUs
|
|
||||||
include_paths: vec![],
|
|
||||||
definitions: vec![],
|
|
||||||
disable_warnings: false,
|
|
||||||
disable_optimizations: false,
|
|
||||||
max_register_count: None,
|
|
||||||
use_fast_math: true,
|
|
||||||
extra_options: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let ptx = compile_ptx(cuda_source, compile_opts)
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("PTX compilation failed: {}", e)))?;
|
|
||||||
|
|
||||||
let compile_time_ms = start.elapsed().as_millis() as u64;
|
|
||||||
|
|
||||||
// Load the compiled PTX
|
|
||||||
let device = get_current_cuda_device()?;
|
|
||||||
let module_name = format!("compiled_{}", MODULE_COUNTER.fetch_add(1, Ordering::SeqCst));
|
|
||||||
|
|
||||||
let module = device.load_ptx(ptx.clone(), &module_name, function_names)
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Failed to load compiled PTX: {}", e)))?;
|
|
||||||
|
|
||||||
// Extract function information
|
|
||||||
let mut functions = HashMap::new();
|
|
||||||
for func_name in function_names {
|
|
||||||
if let Ok(func) = module.get_function(func_name) {
|
|
||||||
let info = get_function_attributes(&func)?;
|
|
||||||
functions.insert(func_name.to_string(), info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create module handle
|
|
||||||
let handle = MODULE_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
||||||
|
|
||||||
// Store in global registry
|
|
||||||
let loaded_module = Arc::new(LoadedModule {
|
|
||||||
id: handle,
|
|
||||||
name: module_name.clone(),
|
|
||||||
module,
|
|
||||||
functions,
|
|
||||||
ptx_source: ptx,
|
|
||||||
compile_time_ms,
|
|
||||||
});
|
|
||||||
|
|
||||||
GLOBAL_MODULES.write()
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Failed to acquire module lock: {}", e)))?
|
|
||||||
.insert(handle, loaded_module);
|
|
||||||
|
|
||||||
info!("Compiled and loaded CUDA module {} in {}ms", module_name, compile_time_ms);
|
|
||||||
Ok(handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cuda"))]
|
|
||||||
{
|
|
||||||
Err(RuntimeError::backend_error(
|
|
||||||
"CUDA",
|
|
||||||
Box::new(std::io::Error::new(std::io::ErrorKind::Other, "CUDA support not compiled in"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get information about a loaded module
|
|
||||||
pub fn get_module_info(handle: u64) -> Result<ModuleInfo> {
|
|
||||||
let modules = GLOBAL_MODULES.read()
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Failed to acquire module lock: {}", e)))?;
|
|
||||||
|
|
||||||
let module = modules.get(&handle)
|
|
||||||
.ok_or_else(|| RuntimeError::kernel_error(format!("Module {} not found", handle)))?;
|
|
||||||
|
|
||||||
Ok(ModuleInfo {
|
|
||||||
id: module.id,
|
|
||||||
name: module.name.clone(),
|
|
||||||
function_count: module.functions.len(),
|
|
||||||
function_names: module.functions.keys().cloned().collect(),
|
|
||||||
ptx_size: module.ptx_source.len(),
|
|
||||||
compile_time_ms: module.compile_time_ms,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Module information structure
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ModuleInfo {
|
|
||||||
pub id: u64,
|
|
||||||
pub name: String,
|
|
||||||
pub function_count: usize,
|
|
||||||
pub function_names: Vec<String>,
|
|
||||||
pub ptx_size: usize,
|
|
||||||
pub compile_time_ms: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unload a module and free resources
|
|
||||||
pub fn unload_module(handle: u64) -> Result<()> {
|
|
||||||
let mut modules = GLOBAL_MODULES.write()
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Failed to acquire module lock: {}", e)))?;
|
|
||||||
|
|
||||||
if modules.remove(&handle).is_some() {
|
|
||||||
info!("Unloaded module {}", handle);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(RuntimeError::kernel_error(format!("Module {} not found", handle)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CudaStreamHandle is imported directly from cuda_backend module
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn get_current_cuda_device() -> Result<Arc<CudaDevice>> {
|
|
||||||
// Get device 0 for now, can be extended to support multiple devices
|
|
||||||
thread_local! {
|
|
||||||
static DEVICE: std::cell::RefCell<Option<Arc<CudaDevice>>> = std::cell::RefCell::new(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
DEVICE.with(|d| {
|
|
||||||
let mut device = d.borrow_mut();
|
|
||||||
if device.is_none() {
|
|
||||||
*device = Some(Arc::new(
|
|
||||||
CudaDevice::new(0)
|
|
||||||
.map_err(|e| RuntimeError::backend_error(format!("Failed to create CUDA device: {}", e)))?
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(device.as_ref().unwrap().clone())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn validate_launch_config(
|
|
||||||
grid: (u32, u32, u32),
|
|
||||||
block: (u32, u32, u32),
|
|
||||||
shared_mem: u32,
|
|
||||||
func_info: &FunctionInfo,
|
|
||||||
) -> Result<()> {
|
|
||||||
let total_threads = block.0 * block.1 * block.2;
|
|
||||||
|
|
||||||
if total_threads > func_info.max_threads_per_block {
|
|
||||||
return Err(RuntimeError::config_error(format!(
|
|
||||||
"Block size {} exceeds maximum {} for function",
|
|
||||||
total_threads, func_info.max_threads_per_block
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if shared_mem > 48 * 1024 { // 48KB typical shared memory limit
|
|
||||||
warn!("Shared memory {} may exceed device limits", shared_mem);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn extract_function_info(module: &CudaModule, ptx_source: &str) -> Result<HashMap<String, FunctionInfo>> {
|
|
||||||
let mut functions = HashMap::new();
|
|
||||||
|
|
||||||
// Parse PTX to find function declarations
|
|
||||||
for line in ptx_source.lines() {
|
|
||||||
if line.contains(".entry") || line.contains(".func") {
|
|
||||||
if let Some(name) = parse_function_name(line) {
|
|
||||||
// Default attributes, can be queried from module if available
|
|
||||||
let info = FunctionInfo {
|
|
||||||
name: name.clone(),
|
|
||||||
max_threads_per_block: 1024,
|
|
||||||
shared_mem_bytes: 0,
|
|
||||||
const_mem_bytes: 0,
|
|
||||||
local_mem_bytes: 0,
|
|
||||||
num_regs: 32,
|
|
||||||
};
|
|
||||||
functions.insert(name, info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(functions)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn parse_function_name(line: &str) -> Option<String> {
|
|
||||||
// Find function name in PTX declaration
|
|
||||||
if let Some(pos) = line.find(".entry") {
|
|
||||||
let after = &line[pos + 6..].trim();
|
|
||||||
if let Some(paren) = after.find('(') {
|
|
||||||
return Some(after[..paren].trim().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(pos) = line.find(".func") {
|
|
||||||
let after = &line[pos + 5..].trim();
|
|
||||||
if let Some(paren) = after.find('(') {
|
|
||||||
return Some(after[..paren].trim().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn get_function_attributes(func: &CudaFunction) -> Result<FunctionInfo> {
|
|
||||||
// Query function attributes using cudarc
|
|
||||||
// These would come from actual CUDA driver API queries
|
|
||||||
Ok(FunctionInfo {
|
|
||||||
name: String::new(), // Would be set by caller
|
|
||||||
max_threads_per_block: 1024, // Default, query from device
|
|
||||||
shared_mem_bytes: 0,
|
|
||||||
const_mem_bytes: 0,
|
|
||||||
local_mem_bytes: 0,
|
|
||||||
num_regs: 32,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn parse_kernel_params(params: &[u8]) -> Result<Vec<Box<dyn AsKernelParam>>> {
|
|
||||||
// Parse byte array into kernel parameters
|
|
||||||
// This is a simplified version - real implementation would handle various types
|
|
||||||
let mut parsed = Vec::new();
|
|
||||||
|
|
||||||
let mut offset = 0;
|
|
||||||
while offset < params.len() {
|
|
||||||
// Read parameter type and size from byte stream
|
|
||||||
// For now, assume 8-byte aligned parameters
|
|
||||||
if offset + 8 <= params.len() {
|
|
||||||
let param_bytes = ¶ms[offset..offset + 8];
|
|
||||||
let value = u64::from_le_bytes(param_bytes.try_into().unwrap());
|
|
||||||
parsed.push(Box::new(value) as Box<dyn AsKernelParam>);
|
|
||||||
offset += 8;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
fn launch_with_params(
|
|
||||||
func: &CudaFunction,
|
|
||||||
config: CudarcLaunchConfig,
|
|
||||||
params: &[Box<dyn AsKernelParam>],
|
|
||||||
stream: &CudaStream,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Use cudarc's launch builder
|
|
||||||
let mut builder = func.launch_builder();
|
|
||||||
|
|
||||||
for param in params {
|
|
||||||
builder.arg(param.as_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
builder.launch_on_stream(config, stream)
|
|
||||||
.map_err(|e| RuntimeError::kernel_error(format!("Kernel launch failed: {}", e)))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trait for kernel parameters
|
|
||||||
trait AsKernelParam {
|
|
||||||
fn as_kernel_param(&self) -> *const std::ffi::c_void;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsKernelParam for u32 {
|
|
||||||
fn as_kernel_param(&self) -> *const std::ffi::c_void {
|
|
||||||
self as *const _ as *const std::ffi::c_void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsKernelParam for u64 {
|
|
||||||
fn as_kernel_param(&self) -> *const std::ffi::c_void {
|
|
||||||
self as *const _ as *const std::ffi::c_void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsKernelParam for f32 {
|
|
||||||
fn as_kernel_param(&self) -> *const std::ffi::c_void {
|
|
||||||
self as *const _ as *const std::ffi::c_void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsKernelParam for f64 {
|
|
||||||
fn as_kernel_param(&self) -> *const std::ffi::c_void {
|
|
||||||
self as *const _ as *const std::ffi::c_void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_kernel_launch_config_default() {
|
|
||||||
let config = KernelLaunchConfig::default();
|
|
||||||
assert_eq!(config.grid_dim, (1, 1, 1));
|
|
||||||
assert_eq!(config.block_dim, (256, 1, 1));
|
|
||||||
assert_eq!(config.shared_mem_bytes, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_module_info_creation() {
|
|
||||||
let info = ModuleInfo {
|
|
||||||
id: 1,
|
|
||||||
name: "test_module".to_string(),
|
|
||||||
function_count: 2,
|
|
||||||
function_names: vec!["func1".to_string(), "func2".to_string()],
|
|
||||||
ptx_size: 1024,
|
|
||||||
compile_time_ms: 100,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(info.function_count, 2);
|
|
||||||
assert_eq!(info.function_names.len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
#[test]
|
|
||||||
fn test_parse_function_name() {
|
|
||||||
let line1 = ".entry vector_add(";
|
|
||||||
assert_eq!(parse_function_name(line1), Some("vector_add".to_string()));
|
|
||||||
|
|
||||||
let line2 = ".func matrix_mul(";
|
|
||||||
assert_eq!(parse_function_name(line2), Some("matrix_mul".to_string()));
|
|
||||||
|
|
||||||
let line3 = "// Just a comment";
|
|
||||||
assert_eq!(parse_function_name(line3), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validate_module_handle() {
|
|
||||||
// Test that invalid handles return errors
|
|
||||||
let result = get_module_info(99999);
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
#[test]
|
|
||||||
fn test_compile_simple_kernel() {
|
|
||||||
let cuda_source = r#"
|
|
||||||
extern "C" __global__ void add_one(float* data, int n) {
|
|
||||||
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
||||||
if (idx < n) {
|
|
||||||
data[idx] += 1.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let result = compile_and_load_cuda(cuda_source, &["add_one"]);
|
|
||||||
|
|
||||||
// This will fail without CUDA, but tests compilation logic
|
|
||||||
if cfg!(feature = "cuda") {
|
|
||||||
match result {
|
|
||||||
Ok(handle) => {
|
|
||||||
// Verify module was loaded
|
|
||||||
let info = get_module_info(handle);
|
|
||||||
assert!(info.is_ok());
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
let _ = unload_module(handle);
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
// Expected if no CUDA device available
|
|
||||||
println!("CUDA compilation test skipped: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,815 +0,0 @@
|
|||||||
//! Core Tensor Structure and Fundamental Operations
|
|
||||||
//!
|
|
||||||
//! This module provides the main `Tensor` type and its core functionality including:
|
|
||||||
//! - Tensor structure definition
|
|
||||||
//! - Constructor functions (zeros, ones, eye, from_data)
|
|
||||||
//! - Basic properties (shape, dtype, device, etc.)
|
|
||||||
//! - Memory management and views
|
|
||||||
//! - Autograd integration infrastructure
|
|
||||||
|
|
||||||
use crate::{Device, DType, Shape, Storage, TensorError, Result};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
/// Global atomic counter for autograd node IDs
|
|
||||||
static NEXT_NODE_ID: AtomicUsize = AtomicUsize::new(0);
|
|
||||||
/// Secondary atomic counter for tensor creation with requires_grad
|
|
||||||
static NEXT_GRAD_NODE_ID: AtomicUsize = AtomicUsize::new(1000);
|
|
||||||
|
|
||||||
/// Simple node ID for autograd integration (temporary until proper integration)
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct NodeId(pub usize);
|
|
||||||
|
|
||||||
impl NodeId {
|
|
||||||
pub fn new(id: usize) -> Self {
|
|
||||||
NodeId(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the underlying ID value
|
|
||||||
pub fn id(&self) -> usize {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Core tensor type with GPU memory backing and PyTorch-compatible API
|
|
||||||
///
|
|
||||||
/// # Design Principles
|
|
||||||
/// - GPU memory backed by rtx-runtime allocator
|
|
||||||
/// - Reference counting for efficient memory usage
|
|
||||||
/// - Zero-copy views via shared storage with offset/stride
|
|
||||||
/// - Automatic gradient computation support (Phase 2)
|
|
||||||
///
|
|
||||||
/// # Field Ordering (Cache Locality Optimization)
|
|
||||||
/// Fields are ordered by access pattern to maximize cache locality:
|
|
||||||
/// 1. Hot path (memory access): storage, strides, offset
|
|
||||||
/// 2. Hot path (compatibility): shape, device
|
|
||||||
/// 3. Warm (autograd check): dtype, requires_grad
|
|
||||||
/// 4. Cold (backward only): node_id, grad
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Tensor {
|
|
||||||
// === Hot: Memory access fields (accessed together for data operations) ===
|
|
||||||
/// Underlying storage with GPU memory
|
|
||||||
storage: Arc<Storage>,
|
|
||||||
/// Memory stride for each dimension
|
|
||||||
strides: Vec<usize>,
|
|
||||||
/// Offset into storage buffer
|
|
||||||
offset: usize,
|
|
||||||
|
|
||||||
// === Hot: Shape/type/device (compatibility checks on every operation) ===
|
|
||||||
/// Tensor shape (dimensions)
|
|
||||||
shape: Shape,
|
|
||||||
/// Device location
|
|
||||||
device: Device,
|
|
||||||
|
|
||||||
// === Warm: Type and autograd flag (checked frequently) ===
|
|
||||||
/// Data type
|
|
||||||
dtype: DType,
|
|
||||||
/// Whether gradient computation is required
|
|
||||||
requires_grad: bool,
|
|
||||||
|
|
||||||
// === Cold: Backward pass only (rarely accessed during forward) ===
|
|
||||||
/// Node ID in computation graph for autograd
|
|
||||||
node_id: Option<NodeId>,
|
|
||||||
/// Gradient tensor (lazily allocated)
|
|
||||||
grad: Arc<Mutex<Option<Tensor>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tensor {
|
|
||||||
/// Create a new tensor with zeros
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `shape` - Tensor dimensions
|
|
||||||
/// * `device` - Target device for memory allocation
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// New zero-initialized tensor
|
|
||||||
pub fn zeros<S: Into<Shape>>(shape: S, device: &Device) -> Result<Self> {
|
|
||||||
let shape = shape.into();
|
|
||||||
let numel = shape.numel();
|
|
||||||
let storage = Arc::new(Storage::zeros(numel, DType::F32, device)?);
|
|
||||||
let strides = shape.default_strides();
|
|
||||||
|
|
||||||
Ok(Tensor {
|
|
||||||
storage,
|
|
||||||
shape,
|
|
||||||
strides,
|
|
||||||
offset: 0,
|
|
||||||
dtype: DType::F32,
|
|
||||||
device: device.clone(),
|
|
||||||
requires_grad: false,
|
|
||||||
grad: Arc::new(Mutex::new(None)),
|
|
||||||
node_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new tensor with ones
|
|
||||||
pub fn ones<S: Into<Shape>>(shape: S, device: &Device) -> Result<Self> {
|
|
||||||
let shape = shape.into();
|
|
||||||
let numel = shape.numel();
|
|
||||||
let storage = Arc::new(Storage::full(numel, 1.0, DType::F32, device)?);
|
|
||||||
let strides = shape.default_strides();
|
|
||||||
|
|
||||||
Ok(Tensor {
|
|
||||||
storage,
|
|
||||||
shape,
|
|
||||||
strides,
|
|
||||||
offset: 0,
|
|
||||||
dtype: DType::F32,
|
|
||||||
device: device.clone(),
|
|
||||||
requires_grad: false,
|
|
||||||
grad: Arc::new(Mutex::new(None)),
|
|
||||||
node_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an identity matrix
|
|
||||||
pub fn eye(n: usize, device: &Device) -> Result<Self> {
|
|
||||||
let mut data = vec![0.0f32; n * n];
|
|
||||||
for i in 0..n {
|
|
||||||
data[i * n + i] = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let storage = Arc::new(Storage::from_data(data, DType::F32, device)?);
|
|
||||||
let shape = Shape::from(vec![n, n]);
|
|
||||||
let strides = shape.default_strides();
|
|
||||||
|
|
||||||
Ok(Tensor {
|
|
||||||
storage,
|
|
||||||
shape,
|
|
||||||
strides,
|
|
||||||
offset: 0,
|
|
||||||
dtype: DType::F32,
|
|
||||||
device: device.clone(),
|
|
||||||
requires_grad: false,
|
|
||||||
grad: Arc::new(Mutex::new(None)),
|
|
||||||
node_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a tensor from raw data
|
|
||||||
pub fn from_data<S: Into<Shape>>(
|
|
||||||
data: Vec<f32>,
|
|
||||||
shape: S,
|
|
||||||
device: &Device,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let shape = shape.into();
|
|
||||||
if data.len() != shape.numel() {
|
|
||||||
return Err(TensorError::shape(format!(
|
|
||||||
"Data length {} doesn't match shape size {}",
|
|
||||||
data.len(),
|
|
||||||
shape.numel()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let storage = Arc::new(Storage::from_vec(data, DType::F32, device)?);
|
|
||||||
let strides = shape.default_strides();
|
|
||||||
|
|
||||||
Ok(Tensor {
|
|
||||||
storage,
|
|
||||||
shape,
|
|
||||||
strides,
|
|
||||||
offset: 0,
|
|
||||||
dtype: DType::F32,
|
|
||||||
device: device.clone(),
|
|
||||||
requires_grad: false,
|
|
||||||
grad: Arc::new(Mutex::new(None)),
|
|
||||||
node_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a tensor with random values from uniform distribution [0, 1)
|
|
||||||
pub fn rand<S: Into<Shape>>(shape: S, device: &Device) -> Result<Self> {
|
|
||||||
let shape = shape.into();
|
|
||||||
let numel = shape.numel();
|
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
let data: Vec<f32> = (0..numel).map(|_| rng.r#gen()).collect();
|
|
||||||
|
|
||||||
Self::from_data(data, shape, device)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a tensor with random values from standard normal distribution
|
|
||||||
pub fn randn<S: Into<Shape>>(shape: S, device: &Device) -> Result<Self> {
|
|
||||||
let shape = shape.into();
|
|
||||||
let numel = shape.numel();
|
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
let data: Vec<f32> = (0..numel)
|
|
||||||
.map(|_| {
|
|
||||||
// Box-Muller transform for normal distribution
|
|
||||||
let u1: f32 = rng.r#gen();
|
|
||||||
let u2: f32 = rng.r#gen();
|
|
||||||
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self::from_data(data, shape, device)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get tensor shape
|
|
||||||
///
|
|
||||||
/// Marked inline as called frequently in hot paths (shape checks, broadcasting).
|
|
||||||
#[inline]
|
|
||||||
pub fn shape(&self) -> &Shape {
|
|
||||||
&self.shape
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get number of dimensions
|
|
||||||
///
|
|
||||||
/// Marked inline as frequently chained with shape access.
|
|
||||||
#[inline]
|
|
||||||
pub fn ndim(&self) -> usize {
|
|
||||||
self.shape.ndim()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total number of elements
|
|
||||||
///
|
|
||||||
/// Marked inline as used in tight loops for size calculations.
|
|
||||||
#[inline]
|
|
||||||
pub fn numel(&self) -> usize {
|
|
||||||
self.shape.numel()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get device location
|
|
||||||
///
|
|
||||||
/// Marked inline as called for every cross-device check.
|
|
||||||
#[inline]
|
|
||||||
pub fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get data type
|
|
||||||
///
|
|
||||||
/// Marked inline as called for every type compatibility check.
|
|
||||||
#[inline]
|
|
||||||
pub fn dtype(&self) -> DType {
|
|
||||||
self.dtype
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if gradients are required
|
|
||||||
///
|
|
||||||
/// Marked inline as checked frequently during forward/backward passes.
|
|
||||||
#[inline]
|
|
||||||
pub fn requires_grad(&self) -> bool {
|
|
||||||
self.requires_grad
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set gradient requirement
|
|
||||||
pub fn set_requires_grad(&mut self, requires_grad: bool) {
|
|
||||||
self.requires_grad = requires_grad;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the gradient tensor
|
|
||||||
pub fn grad(&self) -> Option<Tensor> {
|
|
||||||
// Use unwrap_or_else to recover data even if mutex was poisoned by another thread
|
|
||||||
self.grad.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the gradient tensor
|
|
||||||
pub fn set_grad(&self, grad: Option<Tensor>) {
|
|
||||||
// Use unwrap_or_else to recover data even if mutex was poisoned by another thread
|
|
||||||
*self.grad.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = grad;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the node ID for autograd
|
|
||||||
pub fn node_id(&self) -> Option<NodeId> {
|
|
||||||
self.node_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the node ID for autograd (internal use)
|
|
||||||
pub(crate) fn set_node_id(&mut self, node_id: NodeId) {
|
|
||||||
self.node_id = Some(node_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a tensor that requires gradients and has a node ID
|
|
||||||
pub fn require_grad(mut self) -> Self {
|
|
||||||
self.requires_grad = true;
|
|
||||||
// Node ID will be set when added to tape
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get or create a node ID for this tensor (simplified for now)
|
|
||||||
pub(crate) fn get_or_create_node_id(&mut self) -> Result<NodeId> {
|
|
||||||
if let Some(node_id) = self.node_id {
|
|
||||||
return Ok(node_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use atomic counter for thread-safe node ID generation
|
|
||||||
let node_id = NodeId::new(NEXT_NODE_ID.fetch_add(1, Ordering::Relaxed));
|
|
||||||
self.node_id = Some(node_id);
|
|
||||||
Ok(node_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute gradients via backward pass starting from this tensor (placeholder)
|
|
||||||
pub fn backward(&self) -> Result<()> {
|
|
||||||
if self.node_id.is_none() {
|
|
||||||
return Err(TensorError::autograd("Cannot backward: tensor has no node ID".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a placeholder implementation
|
|
||||||
// The actual backward pass will be implemented in the autograd integration layer
|
|
||||||
// For now, just return success to allow compilation
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a view of this tensor with new shape
|
|
||||||
pub fn view<S: Into<Shape>>(&self, shape: S) -> Result<Self> {
|
|
||||||
let new_shape = shape.into();
|
|
||||||
|
|
||||||
// Check that the new shape has the same number of elements
|
|
||||||
if !self.shape.can_reshape_to(&new_shape) {
|
|
||||||
return Err(TensorError::shape(format!(
|
|
||||||
"Cannot view tensor with {} elements as shape with {} elements",
|
|
||||||
self.shape.numel(),
|
|
||||||
new_shape.numel()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if tensor is contiguous for zero-copy view
|
|
||||||
// For simplicity, we assume contiguous layout for now
|
|
||||||
// In practice, this would check strides vs. default strides
|
|
||||||
let new_strides = new_shape.default_strides();
|
|
||||||
|
|
||||||
// Create view sharing the same storage
|
|
||||||
Ok(Tensor {
|
|
||||||
storage: self.storage.clone(),
|
|
||||||
shape: new_shape,
|
|
||||||
strides: new_strides,
|
|
||||||
offset: self.offset,
|
|
||||||
dtype: self.dtype,
|
|
||||||
device: self.device.clone(),
|
|
||||||
requires_grad: self.requires_grad,
|
|
||||||
grad: Arc::new(Mutex::new(None)), // New tensor starts with no gradient
|
|
||||||
node_id: None, // Views get new node IDs when used in operations
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reshape tensor (may require copy if not contiguous)
|
|
||||||
pub fn reshape<S: Into<Shape>>(&self, shape: S) -> Result<Self> {
|
|
||||||
let new_shape = shape.into();
|
|
||||||
|
|
||||||
// Check that the new shape has the same number of elements
|
|
||||||
if !self.shape.can_reshape_to(&new_shape) {
|
|
||||||
return Err(TensorError::shape(format!(
|
|
||||||
"Cannot reshape tensor with {} elements to shape with {} elements",
|
|
||||||
self.shape.numel(),
|
|
||||||
new_shape.numel()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, always create a copy since we're not tracking contiguity
|
|
||||||
// In practice, this would try view() first and fall back to copy if needed
|
|
||||||
let new_numel = new_shape.numel();
|
|
||||||
let new_storage = Arc::new(Storage::zeros(new_numel, self.dtype, &self.device)?);
|
|
||||||
let new_strides = new_shape.default_strides();
|
|
||||||
|
|
||||||
// Copy data from current tensor
|
|
||||||
let data = self.to_cpu()?;
|
|
||||||
|
|
||||||
let mut result_tensor = Tensor {
|
|
||||||
storage: new_storage,
|
|
||||||
shape: new_shape,
|
|
||||||
strides: new_strides,
|
|
||||||
offset: 0,
|
|
||||||
dtype: self.dtype,
|
|
||||||
device: self.device.clone(),
|
|
||||||
requires_grad: self.requires_grad,
|
|
||||||
grad: Arc::new(Mutex::new(None)), // New tensor starts with no gradient
|
|
||||||
node_id: None, // Reshaping creates new node ID when used in operations
|
|
||||||
};
|
|
||||||
|
|
||||||
Arc::get_mut(&mut result_tensor.storage)
|
|
||||||
.expect("Internal error: newly created tensor should have exclusive storage access")
|
|
||||||
.copy_from_cpu(&data)?;
|
|
||||||
|
|
||||||
Ok(result_tensor)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move tensor to CPU and return data as Vec<f32>
|
|
||||||
pub fn to_cpu(&self) -> Result<Vec<f32>> {
|
|
||||||
self.storage.to_cpu()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move tensor to specified device
|
|
||||||
pub fn to_device(&self, device: &Device) -> Result<Self> {
|
|
||||||
if device == &self.device {
|
|
||||||
return Ok(self.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfer data via CPU (simplified implementation)
|
|
||||||
let data = self.to_cpu()?;
|
|
||||||
let new_storage = Arc::new(Storage::from_vec(data, self.dtype, device)?);
|
|
||||||
|
|
||||||
Ok(Tensor {
|
|
||||||
storage: new_storage,
|
|
||||||
shape: self.shape.clone(),
|
|
||||||
strides: self.strides.clone(),
|
|
||||||
offset: self.offset,
|
|
||||||
dtype: self.dtype,
|
|
||||||
device: device.clone(),
|
|
||||||
requires_grad: self.requires_grad,
|
|
||||||
grad: Arc::new(Mutex::new(None)), // New tensor starts with no gradient
|
|
||||||
node_id: None, // Device transfer creates new node ID when used in operations
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the CUDA device pointer (`CUdeviceptr`) for use with GPU kernel launches.
|
|
||||||
///
|
|
||||||
/// Returns `Err` when the tensor is not on a CUDA device. The returned
|
|
||||||
/// value is a raw GPU virtual address (a `u64`) that can be passed to
|
|
||||||
/// `cudarc`'s `launch_builder().arg(&ptr)` as a kernel argument.
|
|
||||||
#[cfg(feature = "cuda")]
|
|
||||||
pub fn cuda_device_ptr(&self) -> Result<cudarc::driver::sys::CUdeviceptr> {
|
|
||||||
self.storage.cuda_device_ptr()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get raw data pointer for CUDA kernel access
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// The returned pointer is only valid as long as the tensor exists and is not modified.
|
|
||||||
/// This is primarily for interfacing with CUDA kernels.
|
|
||||||
pub unsafe fn data_ptr(&self) -> *const u8 {
|
|
||||||
// SAFETY: Pointer access for CUDA kernel interfacing:
|
|
||||||
// - Caller guarantees tensor lifetime and no modifications per API contract
|
|
||||||
// - Storage is cloned to avoid borrow checker conflicts
|
|
||||||
// - Underlying GPU memory is valid (managed by Arc<Storage>)
|
|
||||||
// - Used primarily for passing pointers to CUDA kernels
|
|
||||||
let mut storage_clone = (*self.storage).clone();
|
|
||||||
storage_clone.data_ptr()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get mutable raw data pointer for CUDA kernel access
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// The returned pointer is only valid as long as the tensor exists.
|
|
||||||
/// Modifications through this pointer will affect the tensor data.
|
|
||||||
/// This is primarily for interfacing with CUDA kernels.
|
|
||||||
pub unsafe fn data_ptr_mut(&mut self) -> *mut u8 {
|
|
||||||
// SAFETY: Mutable pointer access for CUDA kernel interfacing:
|
|
||||||
// - Caller guarantees exclusive access and proper lifetime per API contract
|
|
||||||
// - Copy-on-write: if Arc refcount > 1, storage is cloned first
|
|
||||||
// - After clone, Arc::get_mut is guaranteed to succeed (refcount == 1)
|
|
||||||
// - Underlying GPU memory is valid (managed by Arc<Storage>)
|
|
||||||
// Get mutable access to the storage through Arc
|
|
||||||
Arc::get_mut(&mut self.storage)
|
|
||||||
.map(|s| s.data_ptr_mut())
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
// If we can't get exclusive access, clone the storage
|
|
||||||
let new_storage = (*self.storage).clone();
|
|
||||||
self.storage = Arc::new(new_storage);
|
|
||||||
Arc::get_mut(&mut self.storage)
|
|
||||||
.expect("Internal error: freshly cloned Arc should have exclusive access")
|
|
||||||
.data_ptr_mut()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert tensor to specified data type
|
|
||||||
pub fn to_dtype(&self, dtype: DType) -> Result<Self> {
|
|
||||||
if dtype == self.dtype {
|
|
||||||
return Ok(self.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, only support f32 (simplified implementation)
|
|
||||||
match dtype {
|
|
||||||
DType::F32 => Ok(self.clone()),
|
|
||||||
_ => Err(TensorError::type_error(format!(
|
|
||||||
"Data type conversion to {:?} not yet implemented",
|
|
||||||
dtype
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a scalar value (for tensors with single element)
|
|
||||||
pub fn item(&self) -> Result<f32> {
|
|
||||||
if self.numel() != 1 {
|
|
||||||
return Err(TensorError::shape(format!(
|
|
||||||
"item() can only be called on tensors with a single element, got {} elements",
|
|
||||||
self.numel()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = self.to_cpu()?;
|
|
||||||
Ok(data[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clone tensor with new storage (deep copy)
|
|
||||||
pub fn clone_detached(&self) -> Result<Self> {
|
|
||||||
let data = self.to_cpu()?;
|
|
||||||
let new_storage = Arc::new(Storage::from_vec(data, self.dtype, &self.device)?);
|
|
||||||
|
|
||||||
Ok(Tensor {
|
|
||||||
storage: new_storage,
|
|
||||||
shape: self.shape.clone(),
|
|
||||||
strides: self.strides.clone(),
|
|
||||||
offset: self.offset,
|
|
||||||
dtype: self.dtype,
|
|
||||||
device: self.device.clone(),
|
|
||||||
requires_grad: false, // Detached tensors don't require gradients
|
|
||||||
grad: Arc::new(Mutex::new(None)),
|
|
||||||
node_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility functions for indexing and broadcasting
|
|
||||||
|
|
||||||
/// Convert flat index to multi-dimensional coordinates
|
|
||||||
pub(crate) fn unravel_index(index: usize, shape: &Shape) -> Vec<usize> {
|
|
||||||
let dims = shape.dims();
|
|
||||||
let mut coords = Vec::with_capacity(dims.len());
|
|
||||||
let mut remaining = index;
|
|
||||||
|
|
||||||
for &dim_size in dims.iter().rev() {
|
|
||||||
coords.push(remaining % dim_size);
|
|
||||||
remaining /= dim_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
coords.reverse();
|
|
||||||
coords
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert multi-dimensional coordinates to flat index
|
|
||||||
pub(crate) fn ravel_index(coords: &[usize], strides: &[usize]) -> usize {
|
|
||||||
coords.iter().zip(strides.iter()).map(|(coord, stride)| coord * stride).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute broadcast index for given coordinates
|
|
||||||
pub(crate) fn broadcast_index(coords: &[usize], src_shape: &Shape, _result_shape: &Shape) -> usize {
|
|
||||||
let src_dims = src_shape.dims();
|
|
||||||
let src_strides = src_shape.default_strides();
|
|
||||||
|
|
||||||
// Handle broadcasting by mapping coordinates to source tensor dimensions
|
|
||||||
let mut src_coords = Vec::with_capacity(src_dims.len());
|
|
||||||
let offset = coords.len() - src_dims.len();
|
|
||||||
|
|
||||||
for (i, &dim_size) in src_dims.iter().enumerate() {
|
|
||||||
let coord_idx = offset + i;
|
|
||||||
if coord_idx < coords.len() {
|
|
||||||
let coord = coords[coord_idx];
|
|
||||||
src_coords.push(if dim_size == 1 { 0 } else { coord });
|
|
||||||
} else {
|
|
||||||
src_coords.push(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::ravel_index(&src_coords, &src_strides)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal utility to create a tensor with automatic node ID assignment
|
|
||||||
pub(crate) fn create_with_autograd(
|
|
||||||
storage: Arc<Storage>,
|
|
||||||
shape: Shape,
|
|
||||||
dtype: DType,
|
|
||||||
device: Device,
|
|
||||||
requires_grad: bool,
|
|
||||||
base_node_id: usize,
|
|
||||||
) -> Self {
|
|
||||||
let mut tensor = Tensor {
|
|
||||||
storage,
|
|
||||||
shape,
|
|
||||||
strides: shape.default_strides(),
|
|
||||||
offset: 0,
|
|
||||||
dtype,
|
|
||||||
device,
|
|
||||||
requires_grad,
|
|
||||||
grad: Arc::new(Mutex::new(None)),
|
|
||||||
node_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if requires_grad {
|
|
||||||
let id = base_node_id + NEXT_GRAD_NODE_ID.fetch_add(1, Ordering::Relaxed);
|
|
||||||
tensor.node_id = Some(NodeId::new(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
tensor
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advanced tensor core optimization utilities
|
|
||||||
pub fn optimize_for_tensor_cores(&self) -> Result<TensorCoreOptimization> {
|
|
||||||
let shape = self.shape().dims();
|
|
||||||
let device = self.device();
|
|
||||||
|
|
||||||
if !device.is_gpu() {
|
|
||||||
return Ok(TensorCoreOptimization {
|
|
||||||
can_use_tensor_cores: false,
|
|
||||||
recommended_precision: MixedPrecision::FP32,
|
|
||||||
optimal_tile_size: None,
|
|
||||||
memory_layout: MemoryLayout::RowMajor,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check tensor core compatibility
|
|
||||||
let can_use_tensor_cores = check_tensor_core_compatibility(shape, self.dtype());
|
|
||||||
let recommended_precision = if can_use_tensor_cores {
|
|
||||||
select_optimal_precision(shape, self.dtype())
|
|
||||||
} else {
|
|
||||||
MixedPrecision::FP32
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(TensorCoreOptimization {
|
|
||||||
can_use_tensor_cores,
|
|
||||||
recommended_precision,
|
|
||||||
optimal_tile_size: None, // Simplified for now
|
|
||||||
memory_layout: MemoryLayout::RowMajor,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mixed precision matrix multiplication
|
|
||||||
pub fn mixed_precision_matmul(&self, other: &Tensor, precision: MixedPrecision) -> Result<Tensor> {
|
|
||||||
// For now, all precision modes fall back to FP32
|
|
||||||
// Full implementation would handle actual precision conversion
|
|
||||||
self.matmul(other)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// IN-PLACE OPERATION SUPPORT (Copy-on-Write)
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
/// Check if this tensor has exclusive ownership of its storage.
|
|
||||||
/// Returns true if safe to mutate in-place (refcount == 1).
|
|
||||||
pub fn is_exclusive(&self) -> bool {
|
|
||||||
Arc::strong_count(&self.storage) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure exclusive ownership of storage by cloning if needed (Copy-on-Write).
|
|
||||||
/// After this call, in-place modifications are safe.
|
|
||||||
pub fn make_exclusive(&mut self) -> Result<()> {
|
|
||||||
if !self.is_exclusive() {
|
|
||||||
// Clone the storage data
|
|
||||||
let data = self.to_cpu()?;
|
|
||||||
let new_storage = Storage::from_vec(data, self.dtype, &self.device)?;
|
|
||||||
self.storage = Arc::new(new_storage);
|
|
||||||
self.offset = 0; // Reset offset since we have fresh storage
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tensor core optimization analysis
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TensorCoreOptimization {
|
|
||||||
pub can_use_tensor_cores: bool,
|
|
||||||
pub recommended_precision: MixedPrecision,
|
|
||||||
pub optimal_tile_size: Option<(usize, usize, usize)>,
|
|
||||||
pub memory_layout: MemoryLayout,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mixed precision options
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum MixedPrecision {
|
|
||||||
FP32,
|
|
||||||
FP16,
|
|
||||||
BF16,
|
|
||||||
INT8,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Memory layout options
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum MemoryLayout {
|
|
||||||
RowMajor,
|
|
||||||
ColumnMajor,
|
|
||||||
TensorCore,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check tensor core compatibility
|
|
||||||
fn check_tensor_core_compatibility(shape: &[usize], dtype: crate::DType) -> bool {
|
|
||||||
if shape.len() < 2 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let m = shape[shape.len() - 2];
|
|
||||||
let n = shape[shape.len() - 1];
|
|
||||||
(m % 16 == 0) && (n % 16 == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select optimal precision
|
|
||||||
fn select_optimal_precision(shape: &[usize], _dtype: crate::DType) -> MixedPrecision {
|
|
||||||
let total_elements: usize = shape.iter().product();
|
|
||||||
if total_elements > 1_000_000 {
|
|
||||||
MixedPrecision::FP16
|
|
||||||
} else {
|
|
||||||
MixedPrecision::FP32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional tensor operations for autodiff support
|
|
||||||
impl Tensor {
|
|
||||||
/// Create a scalar tensor
|
|
||||||
pub fn scalar(value: f32, device: &Device) -> Self {
|
|
||||||
Self::from_data(&[value], &[], device).unwrap_or_else(|_| {
|
|
||||||
Self::zeros(&[], device).expect("Failed to create fallback zero scalar tensor")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create ones_like tensor
|
|
||||||
pub fn ones_like(tensor: &Tensor) -> Self {
|
|
||||||
Self::ones(tensor.shape().dims(), tensor.device()).unwrap_or_else(|_| {
|
|
||||||
tensor.clone()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create zeros_like tensor
|
|
||||||
pub fn zeros_like(tensor: &Tensor) -> Self {
|
|
||||||
Self::zeros(tensor.shape().dims(), tensor.device()).unwrap_or_else(|_| {
|
|
||||||
tensor.clone()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create tensor filled with specific value
|
|
||||||
pub fn full<S: Into<Shape>>(shape: S, value: f32, device: &Device) -> Result<Self> {
|
|
||||||
let shape = shape.into();
|
|
||||||
let numel = shape.numel();
|
|
||||||
let storage = Arc::new(Storage::full(numel, value, DType::F32, device)?);
|
|
||||||
let strides = shape.default_strides();
|
|
||||||
|
|
||||||
Ok(Tensor {
|
|
||||||
storage,
|
|
||||||
shape,
|
|
||||||
strides,
|
|
||||||
offset: 0,
|
|
||||||
dtype: DType::F32,
|
|
||||||
device: device.clone(),
|
|
||||||
requires_grad: false,
|
|
||||||
grad: Arc::new(Mutex::new(None)),
|
|
||||||
node_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create tensor from vector
|
|
||||||
pub fn from_vec(data: Vec<f32>, shape: &[usize], device: &Device) -> Self {
|
|
||||||
Self::from_data(&data, shape, device).unwrap_or_else(|_| {
|
|
||||||
Self::zeros(shape, device).expect("Failed to create fallback zero tensor")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Element-wise sine
|
|
||||||
pub fn sin(&self) -> Self {
|
|
||||||
let data = self.to_cpu().unwrap_or_else(|_| vec![]);
|
|
||||||
let result: Vec<f32> = data.iter().map(|x| x.sin()).collect();
|
|
||||||
Self::from_vec(result, self.shape().dims(), self.device())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Element-wise cosine
|
|
||||||
pub fn cos(&self) -> Self {
|
|
||||||
let data = self.to_cpu().unwrap_or_else(|_| vec![]);
|
|
||||||
let result: Vec<f32> = data.iter().map(|x| x.cos()).collect();
|
|
||||||
Self::from_vec(result, self.shape().dims(), self.device())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Element-wise ReLU
|
|
||||||
pub fn relu(&self) -> Self {
|
|
||||||
let data = self.to_cpu().unwrap_or_else(|_| vec![]);
|
|
||||||
let result: Vec<f32> = data.iter().map(|x| x.max(0.0)).collect();
|
|
||||||
Self::from_vec(result, self.shape().dims(), self.device())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute mean
|
|
||||||
pub fn mean(&self, dim: Option<usize>, _keepdim: bool) -> Self {
|
|
||||||
let data = self.to_cpu().unwrap_or_else(|_| vec![]);
|
|
||||||
|
|
||||||
if data.is_empty() {
|
|
||||||
return Self::scalar(0.0, self.device());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(_d) = dim {
|
|
||||||
// Mean along specific dimension - simplified for now
|
|
||||||
let sum: f32 = data.iter().sum();
|
|
||||||
let count = data.len() as f32;
|
|
||||||
Self::scalar(sum / count, self.device())
|
|
||||||
} else {
|
|
||||||
// Mean of all elements
|
|
||||||
let sum: f32 = data.iter().sum();
|
|
||||||
let count = data.len() as f32;
|
|
||||||
Self::scalar(sum / count, self.device())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multiply by scalar
|
|
||||||
pub fn mul_scalar(&self, scalar: f32) -> Self {
|
|
||||||
let data = self.to_cpu().unwrap_or_else(|_| vec![]);
|
|
||||||
let result: Vec<f32> = data.iter().map(|x| x * scalar).collect();
|
|
||||||
Self::from_vec(result, self.shape().dims(), self.device())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subtract tensors
|
|
||||||
pub fn sub(&self, other: &Tensor) -> Self {
|
|
||||||
let self_data = self.to_cpu().unwrap_or_else(|_| vec![]);
|
|
||||||
let other_data = other.to_cpu().unwrap_or_else(|_| vec![]);
|
|
||||||
|
|
||||||
if self_data.len() != other_data.len() {
|
|
||||||
return self.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: Vec<f32> = self_data
|
|
||||||
.iter()
|
|
||||||
.zip(other_data.iter())
|
|
||||||
.map(|(a, b)| a - b)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self::from_vec(result, self.shape().dims(), self.device())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert to vector (alias for to_cpu for compatibility)
|
|
||||||
pub fn to_vec(&self) -> Vec<f32> {
|
|
||||||
self.to_cpu().unwrap_or_else(|_| vec![])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
//! Knowledge distillation for model compression
|
|
||||||
|
|
||||||
use crate::error::{CompressionError, Result};
|
|
||||||
use rtx_tensor::{Device, Tensor};
|
|
||||||
|
|
||||||
/// Distillation loss type
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum DistillationLoss {
|
|
||||||
KLDivergence,
|
|
||||||
MSE,
|
|
||||||
CrossEntropy,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for knowledge distillation
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DistillationConfig {
|
|
||||||
pub temperature: f32,
|
|
||||||
pub alpha: f32,
|
|
||||||
pub beta: f32,
|
|
||||||
pub loss_type: DistillationLoss,
|
|
||||||
pub use_attention_transfer: bool,
|
|
||||||
pub use_feature_matching: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DistillationConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
temperature: 3.0,
|
|
||||||
alpha: 0.7,
|
|
||||||
beta: 0.3,
|
|
||||||
loss_type: DistillationLoss::KLDivergence,
|
|
||||||
use_attention_transfer: false,
|
|
||||||
use_feature_matching: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Knowledge distiller for teacher-student compression
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct KnowledgeDistiller {
|
|
||||||
config: DistillationConfig,
|
|
||||||
device: Device,
|
|
||||||
schedule_steps: Option<usize>,
|
|
||||||
initial_temp: f32,
|
|
||||||
final_temp: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KnowledgeDistiller {
|
|
||||||
pub fn new(config: DistillationConfig, device: &Device) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
schedule_steps: None,
|
|
||||||
initial_temp: config.temperature,
|
|
||||||
final_temp: config.temperature,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default(device: &Device) -> Result<Self> {
|
|
||||||
Self::new(DistillationConfig::default(), device)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_schedule(
|
|
||||||
initial_temp: f32,
|
|
||||||
final_temp: f32,
|
|
||||||
steps: usize,
|
|
||||||
device: &Device,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let config = DistillationConfig {
|
|
||||||
temperature: initial_temp,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
schedule_steps: Some(steps),
|
|
||||||
initial_temp,
|
|
||||||
final_temp,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compute_loss(&self, teacher_logits: &Tensor, student_logits: &Tensor) -> Result<Tensor> {
|
|
||||||
let teacher_soft = self.apply_temperature(teacher_logits)?;
|
|
||||||
let student_soft = self.apply_temperature(student_logits)?;
|
|
||||||
|
|
||||||
match self.config.loss_type {
|
|
||||||
DistillationLoss::KLDivergence => self.kl_divergence(&teacher_soft, &student_soft),
|
|
||||||
DistillationLoss::MSE => self.mse_loss(&teacher_soft, &student_soft),
|
|
||||||
DistillationLoss::CrossEntropy => self.cross_entropy(&teacher_soft, &student_soft),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_temperature(&self, logits: &Tensor) -> Result<Tensor> {
|
|
||||||
logits.div_scalar(self.config.temperature)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_soft_targets(&self, logits: &Tensor) -> Result<Tensor> {
|
|
||||||
let scaled = self.apply_temperature(logits)?;
|
|
||||||
self.softmax(&scaled)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compute_combined_loss(
|
|
||||||
&self,
|
|
||||||
teacher_logits: &Tensor,
|
|
||||||
student_logits: &Tensor,
|
|
||||||
labels: &Tensor,
|
|
||||||
) -> Result<Tensor> {
|
|
||||||
let distill_loss = self.compute_loss(teacher_logits, student_logits)?;
|
|
||||||
let student_loss = self.cross_entropy(student_logits, labels)?;
|
|
||||||
|
|
||||||
let weighted_distill = distill_loss.mul_scalar(self.config.alpha)?;
|
|
||||||
let weighted_student = student_loss.mul_scalar(self.config.beta)?;
|
|
||||||
|
|
||||||
weighted_distill.add(&weighted_student)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attention_transfer_loss(
|
|
||||||
&self,
|
|
||||||
teacher_attn: &Tensor,
|
|
||||||
student_attn: &Tensor,
|
|
||||||
) -> Result<Tensor> {
|
|
||||||
// Simplified: MSE between attention maps
|
|
||||||
self.mse_loss(teacher_attn, student_attn)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn feature_matching_loss(
|
|
||||||
&self,
|
|
||||||
teacher_features: &[Tensor],
|
|
||||||
student_features: &[Tensor],
|
|
||||||
) -> Result<Tensor> {
|
|
||||||
if teacher_features.len() != student_features.len() {
|
|
||||||
return Err(CompressionError::CompressionFailed(
|
|
||||||
"Feature lists must have same length".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut total_loss = Tensor::zeros(&[1], &self.device)?;
|
|
||||||
for (t, s) in teacher_features.iter().zip(student_features.iter()) {
|
|
||||||
let loss = self.mse_loss(t, s)?;
|
|
||||||
total_loss = total_loss.add(&loss)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
total_loss.div_scalar(teacher_features.len() as f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compute_compression_ratio(&self, teacher_params: usize, student_params: usize) -> f32 {
|
|
||||||
teacher_params as f32 / student_params as f32
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_temperature_at_step(&self, step: usize) -> f32 {
|
|
||||||
if let Some(total_steps) = self.schedule_steps {
|
|
||||||
let progress = (step as f32) / (total_steps as f32).max(1.0);
|
|
||||||
self.initial_temp + (self.final_temp - self.initial_temp) * progress
|
|
||||||
} else {
|
|
||||||
self.config.temperature
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compute_metrics(
|
|
||||||
&self,
|
|
||||||
teacher_preds: &Tensor,
|
|
||||||
student_preds: &Tensor,
|
|
||||||
labels: &Tensor,
|
|
||||||
) -> Result<DistillationMetrics> {
|
|
||||||
let teacher_correct = self.count_correct(teacher_preds, labels)?;
|
|
||||||
let student_correct = self.count_correct(student_preds, labels)?;
|
|
||||||
let agreement = self.count_agreement(teacher_preds, student_preds)?;
|
|
||||||
let total = labels.numel() as f32;
|
|
||||||
|
|
||||||
Ok(DistillationMetrics {
|
|
||||||
teacher_accuracy: teacher_correct / total,
|
|
||||||
student_accuracy: student_correct / total,
|
|
||||||
agreement_rate: agreement / total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
fn kl_divergence(&self, p: &Tensor, q: &Tensor) -> Result<Tensor> {
|
|
||||||
// Simplified KL divergence
|
|
||||||
let log_p = p.log()?;
|
|
||||||
let log_q = q.log()?;
|
|
||||||
let diff = log_p.sub(&log_q)?;
|
|
||||||
let kl = p.mul(&diff)?;
|
|
||||||
kl.sum()?.div_scalar(p.numel() as f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mse_loss(&self, pred: &Tensor, target: &Tensor) -> Result<Tensor> {
|
|
||||||
let diff = pred.sub(target)?;
|
|
||||||
let squared = diff.mul(&diff)?;
|
|
||||||
squared.sum()?.div_scalar(pred.numel() as f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cross_entropy(&self, logits: &Tensor, labels: &Tensor) -> Result<Tensor> {
|
|
||||||
// Simplified cross entropy
|
|
||||||
let probs = self.softmax(logits)?;
|
|
||||||
let log_probs = probs.log()?;
|
|
||||||
log_probs.sum()?.mul_scalar(-1.0)?.div_scalar(logits.shape().dims()[0] as f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn softmax(&self, logits: &Tensor) -> Result<Tensor> {
|
|
||||||
let exp = logits.exp()?;
|
|
||||||
let sum = exp.sum()?;
|
|
||||||
exp.div(&sum)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_correct(&self, preds: &Tensor, labels: &Tensor) -> Result<f32> {
|
|
||||||
let pred_data = preds.to_vec()?;
|
|
||||||
let label_data = labels.to_vec()?;
|
|
||||||
let correct = pred_data.iter()
|
|
||||||
.zip(label_data.iter())
|
|
||||||
.filter(|(p, l)| (p - l).abs() < 1e-5)
|
|
||||||
.count();
|
|
||||||
Ok(correct as f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_agreement(&self, preds1: &Tensor, preds2: &Tensor) -> Result<f32> {
|
|
||||||
let data1 = preds1.to_vec()?;
|
|
||||||
let data2 = preds2.to_vec()?;
|
|
||||||
let agree = data1.iter()
|
|
||||||
.zip(data2.iter())
|
|
||||||
.filter(|(p1, p2)| (p1 - p2).abs() < 1e-5)
|
|
||||||
.count();
|
|
||||||
Ok(agree as f32)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metrics for distillation
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DistillationMetrics {
|
|
||||||
pub teacher_accuracy: f32,
|
|
||||||
pub student_accuracy: f32,
|
|
||||||
pub agreement_rate: f32,
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
//! Structured pruning implementation
|
|
||||||
|
|
||||||
use crate::error::{CompressionError, Result};
|
|
||||||
use crate::pruning::config::{PruningConfig, PruningGranularity};
|
|
||||||
use rtx_tensor::{Device, Tensor};
|
|
||||||
|
|
||||||
/// Structured pruner for channel/filter pruning
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct StructuredPruner {
|
|
||||||
config: PruningConfig,
|
|
||||||
device: Device,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StructuredPruner {
|
|
||||||
/// Create a new structured pruner
|
|
||||||
pub fn new(config: PruningConfig, device: &Device) -> Result<Self> {
|
|
||||||
if !config.structured {
|
|
||||||
return Err(CompressionError::CompressionFailed(
|
|
||||||
"Config must have structured=true for StructuredPruner".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute pruning mask for structured pruning
|
|
||||||
pub fn compute_mask(&self, weights: &Tensor) -> Result<Tensor> {
|
|
||||||
match self.config.granularity {
|
|
||||||
PruningGranularity::Channel => self.compute_channel_mask(weights),
|
|
||||||
PruningGranularity::Filter => self.compute_filter_mask(weights),
|
|
||||||
_ => Err(CompressionError::CompressionFailed(
|
|
||||||
"Unsupported granularity for structured pruning".to_string()
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply mask to weights
|
|
||||||
pub fn apply_mask(&self, weights: &Tensor, mask: &Tensor) -> Result<Tensor> {
|
|
||||||
weights.mul(mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prune weights directly
|
|
||||||
pub fn prune(&self, weights: &Tensor) -> Result<Tensor> {
|
|
||||||
let mask = self.compute_mask(weights)?;
|
|
||||||
self.apply_mask(weights, &mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get indices of pruned channels/filters
|
|
||||||
pub fn get_pruned_indices(&self, weights: &Tensor) -> Result<Vec<usize>> {
|
|
||||||
let shape = weights.shape().dims();
|
|
||||||
let num_filters = shape[0];
|
|
||||||
let num_to_prune = (num_filters as f32 * self.config.sparsity) as usize;
|
|
||||||
|
|
||||||
// Compute L2 norm of each filter
|
|
||||||
let data = weights.to_vec()?;
|
|
||||||
let filter_size = data.len() / num_filters;
|
|
||||||
|
|
||||||
let mut norms: Vec<(f32, usize)> = Vec::new();
|
|
||||||
for i in 0..num_filters {
|
|
||||||
let start = i * filter_size;
|
|
||||||
let end = start + filter_size;
|
|
||||||
let norm: f32 = data[start..end].iter()
|
|
||||||
.map(|x| x * x)
|
|
||||||
.sum::<f32>()
|
|
||||||
.sqrt();
|
|
||||||
norms.push((norm, i));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by norm and select smallest
|
|
||||||
norms.sort_by(|a, b| a.0.total_cmp(&b.0));
|
|
||||||
|
|
||||||
Ok(norms.iter().take(num_to_prune).map(|(_, idx)| *idx).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_channel_mask(&self, weights: &Tensor) -> Result<Tensor> {
|
|
||||||
let shape = weights.shape().dims();
|
|
||||||
if shape.len() != 4 {
|
|
||||||
return Err(CompressionError::CompressionFailed(
|
|
||||||
"Channel pruning requires 4D tensor [out, in, h, w]".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let out_channels = shape[0];
|
|
||||||
let channel_size = shape[1] * shape[2] * shape[3];
|
|
||||||
let num_to_prune = (out_channels as f32 * self.config.sparsity) as usize;
|
|
||||||
|
|
||||||
// Compute channel importance (L2 norm)
|
|
||||||
let data = weights.to_vec()?;
|
|
||||||
let mut channel_norms: Vec<(f32, usize)> = Vec::new();
|
|
||||||
|
|
||||||
for ch in 0..out_channels {
|
|
||||||
let start = ch * channel_size;
|
|
||||||
let end = start + channel_size;
|
|
||||||
let norm: f32 = data[start..end].iter()
|
|
||||||
.map(|x| x * x)
|
|
||||||
.sum::<f32>()
|
|
||||||
.sqrt();
|
|
||||||
channel_norms.push((norm, ch));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort and select channels to prune
|
|
||||||
channel_norms.sort_by(|a, b| a.0.total_cmp(&b.0));
|
|
||||||
let pruned_channels: Vec<usize> = channel_norms.iter()
|
|
||||||
.take(num_to_prune)
|
|
||||||
.map(|(_, idx)| *idx)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Create mask
|
|
||||||
let mut mask = vec![1.0f32; data.len()];
|
|
||||||
for &ch in &pruned_channels {
|
|
||||||
let start = ch * channel_size;
|
|
||||||
let end = start + channel_size;
|
|
||||||
for i in start..end {
|
|
||||||
mask[i] = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tensor::from_data(mask, shape.to_vec(), &self.device)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_filter_mask(&self, weights: &Tensor) -> Result<Tensor> {
|
|
||||||
// Similar to channel mask but for entire filters
|
|
||||||
self.compute_channel_mask(weights)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,687 +0,0 @@
|
|||||||
//! Parallelism strategies for distributed training
|
|
||||||
//!
|
|
||||||
//! This module implements various parallelism approaches including:
|
|
||||||
//! - Data Parallel (DP)
|
|
||||||
//! - Tensor Parallel (TP)
|
|
||||||
//! - Pipeline Parallel (PP)
|
|
||||||
//! - Fully Sharded Data Parallel (FSDP/ZeRO)
|
|
||||||
|
|
||||||
use crate::comm::CommunicationPrimitive;
|
|
||||||
use crate::error::{DistributedError, Result};
|
|
||||||
use crate::group::ProcessGroup;
|
|
||||||
use crate::tensor_ext::{TensorExt, TensorShapeExt};
|
|
||||||
use rtx_tensor::Tensor;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Data Parallel implementation
|
|
||||||
pub struct DataParallel {
|
|
||||||
/// Process group for data parallel communication
|
|
||||||
pub process_group: ProcessGroup,
|
|
||||||
/// Gradient accumulation steps
|
|
||||||
pub accumulation_steps: usize,
|
|
||||||
/// Current accumulation step
|
|
||||||
current_step: usize,
|
|
||||||
/// Whether gradients are currently being accumulated
|
|
||||||
accumulating: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataParallel {
|
|
||||||
/// Create new data parallel instance
|
|
||||||
pub fn new(process_group: ProcessGroup, accumulation_steps: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
process_group,
|
|
||||||
accumulation_steps: accumulation_steps.max(1),
|
|
||||||
current_step: 0,
|
|
||||||
accumulating: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start gradient accumulation
|
|
||||||
pub fn start_accumulation(&mut self) {
|
|
||||||
self.accumulating = true;
|
|
||||||
self.current_step = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Accumulate gradients (called after backward pass)
|
|
||||||
pub async fn accumulate_gradients(&mut self, gradients: &mut [Tensor]) -> Result<bool> {
|
|
||||||
if !self.accumulating {
|
|
||||||
return Err(DistributedError::parallelism(
|
|
||||||
"DataParallel",
|
|
||||||
"accumulation not started"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.current_step += 1;
|
|
||||||
|
|
||||||
// If we've accumulated enough steps, average gradients
|
|
||||||
if self.current_step >= self.accumulation_steps {
|
|
||||||
self.average_gradients(gradients).await?;
|
|
||||||
self.accumulating = false;
|
|
||||||
self.current_step = 0;
|
|
||||||
Ok(true) // Ready for optimizer step
|
|
||||||
} else {
|
|
||||||
Ok(false) // Continue accumulating
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Average gradients across all processes
|
|
||||||
async fn average_gradients(&self, gradients: &mut [Tensor]) -> Result<()> {
|
|
||||||
use crate::comm::ReduceOp;
|
|
||||||
|
|
||||||
for gradient in gradients.iter_mut() {
|
|
||||||
// AllReduce sum
|
|
||||||
self.process_group.allreduce(gradient, ReduceOp::Sum).await?;
|
|
||||||
|
|
||||||
// Divide by world size and accumulation steps
|
|
||||||
let scale = 1.0 / (self.process_group.world_size() as f32 * self.accumulation_steps as f32);
|
|
||||||
gradient.mul_scalar_(scale)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current accumulation progress
|
|
||||||
pub fn accumulation_progress(&self) -> f32 {
|
|
||||||
if self.accumulation_steps == 0 {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
self.current_step as f32 / self.accumulation_steps as f32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tensor Parallel implementation
|
|
||||||
pub struct TensorParallel {
|
|
||||||
/// Process group for tensor parallel communication
|
|
||||||
pub process_group: ProcessGroup,
|
|
||||||
/// Tensor sharding configuration
|
|
||||||
pub sharding_config: TensorShardingConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for tensor sharding
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct TensorShardingConfig {
|
|
||||||
/// Dimension along which to shard tensors
|
|
||||||
pub shard_dim: usize,
|
|
||||||
/// Whether to shard attention weights
|
|
||||||
pub shard_attention: bool,
|
|
||||||
/// Whether to shard feed-forward weights
|
|
||||||
pub shard_feedforward: bool,
|
|
||||||
/// Overlap communication with computation
|
|
||||||
pub overlap_comm: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TensorShardingConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
shard_dim: 0, // Shard along first dimension
|
|
||||||
shard_attention: true,
|
|
||||||
shard_feedforward: true,
|
|
||||||
overlap_comm: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TensorParallel {
|
|
||||||
/// Create new tensor parallel instance
|
|
||||||
pub fn new(process_group: ProcessGroup, sharding_config: TensorShardingConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
process_group,
|
|
||||||
sharding_config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shard a tensor across processes
|
|
||||||
pub async fn shard_tensor(&self, tensor: &Tensor) -> Result<Tensor> {
|
|
||||||
let world_size = self.process_group.world_size() as usize;
|
|
||||||
let shard_dim = self.sharding_config.shard_dim;
|
|
||||||
|
|
||||||
// Check if tensor can be sharded
|
|
||||||
if shard_dim >= tensor.shape().dims().len() {
|
|
||||||
return Err(DistributedError::tensor(
|
|
||||||
"shard dimension exceeds tensor dimensions"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let dim_size = tensor.shape().dims()[shard_dim];
|
|
||||||
if dim_size % world_size != 0 {
|
|
||||||
return Err(DistributedError::tensor(
|
|
||||||
format!("tensor dimension {} not divisible by world size {}", dim_size, world_size)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate shard size
|
|
||||||
let shard_size = dim_size / world_size;
|
|
||||||
let my_rank = self.process_group.rank() as usize;
|
|
||||||
let start_idx = my_rank * shard_size;
|
|
||||||
let end_idx = start_idx + shard_size;
|
|
||||||
|
|
||||||
// Create sharded tensor view
|
|
||||||
tensor.slice(shard_dim, start_idx, end_idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gather tensor shards from all processes
|
|
||||||
pub async fn gather_tensor_shards(&self, shard: &Tensor) -> Result<Tensor> {
|
|
||||||
use crate::comm::AllGatherOutput;
|
|
||||||
|
|
||||||
let gathered = self.process_group.allgather(shard).await?;
|
|
||||||
|
|
||||||
match gathered {
|
|
||||||
AllGatherOutput::Tensor(tensor) => Ok(tensor),
|
|
||||||
AllGatherOutput::TensorList(tensors) => {
|
|
||||||
// Concatenate tensors along shard dimension
|
|
||||||
Tensor::cat(&tensors, self.sharding_config.shard_dim)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pipeline Parallel implementation
|
|
||||||
pub struct PipelineParallel {
|
|
||||||
/// Process group for pipeline parallel communication
|
|
||||||
pub process_group: ProcessGroup,
|
|
||||||
/// Pipeline stage configuration
|
|
||||||
pub stage_config: PipelineStageConfig,
|
|
||||||
/// Number of microbatches for pipeline
|
|
||||||
pub num_microbatches: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for pipeline stages
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PipelineStageConfig {
|
|
||||||
/// Stage ID (0-indexed)
|
|
||||||
pub stage_id: usize,
|
|
||||||
/// Total number of stages
|
|
||||||
pub num_stages: usize,
|
|
||||||
/// Layers assigned to this stage
|
|
||||||
pub layer_range: (usize, usize), // (start, end)
|
|
||||||
/// Use asynchronous pipeline schedule
|
|
||||||
pub async_schedule: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PipelineParallel {
|
|
||||||
/// Create new pipeline parallel instance
|
|
||||||
pub fn new(
|
|
||||||
process_group: ProcessGroup,
|
|
||||||
stage_config: PipelineStageConfig,
|
|
||||||
num_microbatches: usize
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
process_group,
|
|
||||||
stage_config,
|
|
||||||
num_microbatches,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute forward pass for pipeline stage
|
|
||||||
pub async fn forward_stage(&self, input: &Tensor, stage_layers: &[Box<dyn LayerTrait>]) -> Result<Tensor> {
|
|
||||||
let mut output = input.clone();
|
|
||||||
|
|
||||||
// Apply layers for this stage
|
|
||||||
for layer in stage_layers {
|
|
||||||
output = layer.forward(&output)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send to next stage if not the last stage
|
|
||||||
if self.stage_config.stage_id < self.stage_config.num_stages - 1 {
|
|
||||||
let next_rank = self.process_group.rank() + 1;
|
|
||||||
self.process_group.send(&output, next_rank).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute backward pass for pipeline stage
|
|
||||||
pub async fn backward_stage(&self, grad_output: &Tensor, stage_layers: &[Box<dyn LayerTrait>]) -> Result<Tensor> {
|
|
||||||
let mut grad_input = grad_output.clone();
|
|
||||||
|
|
||||||
// Apply backward pass for layers in reverse order
|
|
||||||
for layer in stage_layers.iter().rev() {
|
|
||||||
grad_input = layer.backward(&grad_input)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send to previous stage if not the first stage
|
|
||||||
if self.stage_config.stage_id > 0 {
|
|
||||||
let prev_rank = self.process_group.rank() - 1;
|
|
||||||
self.process_group.send(&grad_input, prev_rank).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(grad_input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this is the first stage
|
|
||||||
pub fn is_first_stage(&self) -> bool {
|
|
||||||
self.stage_config.stage_id == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this is the last stage
|
|
||||||
pub fn is_last_stage(&self) -> bool {
|
|
||||||
self.stage_config.stage_id == self.stage_config.num_stages - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Placeholder trait for neural network layers
|
|
||||||
pub trait LayerTrait {
|
|
||||||
fn forward(&self, input: &Tensor) -> Result<Tensor>;
|
|
||||||
fn backward(&self, grad_output: &Tensor) -> Result<Tensor>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fully Sharded Data Parallel (FSDP) implementation
|
|
||||||
pub struct Fsdp {
|
|
||||||
/// Process group for FSDP communication
|
|
||||||
pub process_group: ProcessGroup,
|
|
||||||
/// FSDP configuration
|
|
||||||
pub config: FsdpConfig,
|
|
||||||
/// Sharded parameters
|
|
||||||
sharded_params: HashMap<String, ShardedParameter>,
|
|
||||||
/// Memory usage statistics
|
|
||||||
memory_stats: FsdpMemoryStats,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FSDP configuration
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct FsdpConfig {
|
|
||||||
/// Sharding strategy
|
|
||||||
pub sharding_strategy: ShardingStrategy,
|
|
||||||
/// Minimum parameter size to shard (in elements)
|
|
||||||
pub min_param_size: usize,
|
|
||||||
/// CPU offloading enabled
|
|
||||||
pub cpu_offload: bool,
|
|
||||||
/// Mixed precision configuration
|
|
||||||
pub mixed_precision: bool,
|
|
||||||
/// Flatten parameters for sharding
|
|
||||||
pub flatten_parameters: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sharding strategies for FSDP
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum ShardingStrategy {
|
|
||||||
/// Full sharding (ZeRO-3)
|
|
||||||
FullShard,
|
|
||||||
/// Shard gradients only (ZeRO-2)
|
|
||||||
ShardGradOp,
|
|
||||||
/// No sharding, replicate parameters
|
|
||||||
NoShard,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sharded parameter representation
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ShardedParameter {
|
|
||||||
/// Parameter name/identifier
|
|
||||||
pub name: String,
|
|
||||||
/// Local shard of the parameter
|
|
||||||
pub local_shard: Tensor,
|
|
||||||
/// Full parameter shape
|
|
||||||
pub full_shape: Vec<usize>,
|
|
||||||
/// Shard metadata
|
|
||||||
pub shard_metadata: ShardMetadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata for parameter sharding
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ShardMetadata {
|
|
||||||
/// Rank that owns this shard
|
|
||||||
pub owner_rank: i32,
|
|
||||||
/// Start index in flattened parameter
|
|
||||||
pub start_idx: usize,
|
|
||||||
/// End index in flattened parameter
|
|
||||||
pub end_idx: usize,
|
|
||||||
/// Original parameter offset
|
|
||||||
pub param_offset: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Memory usage statistics for FSDP
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct FsdpMemoryStats {
|
|
||||||
/// Total parameter memory (MB)
|
|
||||||
pub total_param_memory_mb: f32,
|
|
||||||
/// Local shard memory (MB)
|
|
||||||
pub local_shard_memory_mb: f32,
|
|
||||||
/// Peak memory during all-gather (MB)
|
|
||||||
pub peak_allgather_memory_mb: f32,
|
|
||||||
/// Memory saved compared to non-sharded (MB)
|
|
||||||
pub memory_saved_mb: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FsdpConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
sharding_strategy: ShardingStrategy::FullShard,
|
|
||||||
min_param_size: 1000, // Only shard parameters with >1K elements
|
|
||||||
cpu_offload: false,
|
|
||||||
mixed_precision: true,
|
|
||||||
flatten_parameters: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Fsdp {
|
|
||||||
/// Create new FSDP instance with default config
|
|
||||||
pub fn new(process_group: ProcessGroup) -> Result<Self> {
|
|
||||||
let config = FsdpConfig::default();
|
|
||||||
Ok(Self {
|
|
||||||
process_group,
|
|
||||||
config,
|
|
||||||
sharded_params: HashMap::new(),
|
|
||||||
memory_stats: FsdpMemoryStats::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new FSDP instance with custom config
|
|
||||||
pub fn new_with_config(process_group: ProcessGroup, config: FsdpConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
process_group,
|
|
||||||
config,
|
|
||||||
sharded_params: HashMap::new(),
|
|
||||||
memory_stats: FsdpMemoryStats::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shard model parameters across the process group
|
|
||||||
pub fn shard_parameters(&mut self, parameters: &Tensor) -> Result<Tensor> {
|
|
||||||
let world_size = self.process_group.world_size();
|
|
||||||
let rank = self.process_group.rank();
|
|
||||||
|
|
||||||
if world_size == 1 {
|
|
||||||
// No sharding needed for single GPU
|
|
||||||
return Ok(parameters.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate shard size
|
|
||||||
let total_elements = parameters.element_count();
|
|
||||||
let elements_per_shard = (total_elements + world_size - 1) / world_size; // Round up
|
|
||||||
let start_idx = rank * elements_per_shard;
|
|
||||||
let end_idx = (start_idx + elements_per_shard).min(total_elements);
|
|
||||||
let shard_size = end_idx - start_idx;
|
|
||||||
|
|
||||||
// Create local shard (simplified - in real implementation would slice the actual tensor)
|
|
||||||
let shard_shape = rtx_tensor::TensorShape::new(vec![shard_size]);
|
|
||||||
let local_shard = Tensor::zeros(shard_shape);
|
|
||||||
|
|
||||||
// Update memory statistics
|
|
||||||
let total_memory_mb = (total_elements * 4) as f32 / (1024.0 * 1024.0); // f32 = 4 bytes
|
|
||||||
let shard_memory_mb = (shard_size * 4) as f32 / (1024.0 * 1024.0);
|
|
||||||
|
|
||||||
self.memory_stats.total_param_memory_mb = total_memory_mb;
|
|
||||||
self.memory_stats.local_shard_memory_mb = shard_memory_mb;
|
|
||||||
self.memory_stats.memory_saved_mb = total_memory_mb - shard_memory_mb;
|
|
||||||
|
|
||||||
tracing::debug!("FSDP sharded {:.2}MB -> {:.2}MB ({:.1}% reduction)",
|
|
||||||
total_memory_mb, shard_memory_mb,
|
|
||||||
(self.memory_stats.memory_saved_mb / total_memory_mb) * 100.0);
|
|
||||||
|
|
||||||
Ok(local_shard)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Synchronize gradients across all processes
|
|
||||||
pub fn sync_gradients(&self, gradients: &mut Tensor) -> Result<()> {
|
|
||||||
use crate::comm::ReduceOp;
|
|
||||||
|
|
||||||
// Perform AllReduce to sum gradients across all processes
|
|
||||||
self.process_group.all_reduce(gradients, ReduceOp::Sum)?;
|
|
||||||
|
|
||||||
// Average the gradients
|
|
||||||
let world_size = self.process_group.world_size() as f32;
|
|
||||||
*gradients = gradients.clone() / world_size;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current memory statistics
|
|
||||||
pub fn memory_stats(&self) -> &FsdpMemoryStats {
|
|
||||||
&self.memory_stats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let world_size = self.process_group.world_size() as usize;
|
|
||||||
let my_rank = self.process_group.rank() as usize;
|
|
||||||
|
|
||||||
for (param_idx, param) in parameters.iter().enumerate() {
|
|
||||||
let param_name = format!("param_{}", param_idx);
|
|
||||||
|
|
||||||
// Skip small parameters if configured
|
|
||||||
let param_size = param.shape().dims().iter().product::<usize>();
|
|
||||||
if param_size < self.config.min_param_size {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flatten parameter if configured
|
|
||||||
let flattened_param = if self.config.flatten_parameters {
|
|
||||||
param.flatten()?
|
|
||||||
} else {
|
|
||||||
param.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate shard boundaries
|
|
||||||
let total_elements = flattened_param.shape().dims().iter().product::<usize>();
|
|
||||||
let elements_per_shard = (total_elements + world_size - 1) / world_size; // Ceiling division
|
|
||||||
let start_idx = my_rank * elements_per_shard;
|
|
||||||
let end_idx = (start_idx + elements_per_shard).min(total_elements);
|
|
||||||
|
|
||||||
// Create local shard
|
|
||||||
let local_shard = if start_idx < total_elements {
|
|
||||||
flattened_param.slice(0, start_idx, end_idx)?
|
|
||||||
} else {
|
|
||||||
// Empty shard for ranks with no data
|
|
||||||
Tensor::zeros(rtx_tensor::TensorShape::new(vec![0]))?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create shard metadata
|
|
||||||
let shard_metadata = ShardMetadata {
|
|
||||||
owner_rank: my_rank as i32,
|
|
||||||
start_idx,
|
|
||||||
end_idx,
|
|
||||||
param_offset: param_idx,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store sharded parameter
|
|
||||||
let sharded_param = ShardedParameter {
|
|
||||||
name: param_name.clone(),
|
|
||||||
local_shard,
|
|
||||||
full_shape: param.shape().dims().to_vec(),
|
|
||||||
shard_metadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.sharded_params.insert(param_name, sharded_param);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update memory statistics
|
|
||||||
self.update_memory_stats(parameters).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All-gather parameters for forward pass
|
|
||||||
pub async fn allgather_parameters(&self, param_name: &str) -> Result<Tensor> {
|
|
||||||
let sharded_param = self.sharded_params.get(param_name)
|
|
||||||
.ok_or_else(|| DistributedError::tensor(
|
|
||||||
format!("parameter {} not found", param_name)
|
|
||||||
))?;
|
|
||||||
|
|
||||||
// All-gather the sharded parameter
|
|
||||||
use crate::comm::AllGatherOutput;
|
|
||||||
let gathered = self.process_group.allgather(&sharded_param.local_shard).await?;
|
|
||||||
|
|
||||||
match gathered {
|
|
||||||
AllGatherOutput::Tensor(tensor) => {
|
|
||||||
// Reshape to original shape if flattened
|
|
||||||
if self.config.flatten_parameters && sharded_param.full_shape.len() > 1 {
|
|
||||||
tensor.reshape(&sharded_param.full_shape)
|
|
||||||
} else {
|
|
||||||
Ok(tensor)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
AllGatherOutput::TensorList(_) => {
|
|
||||||
Err(DistributedError::tensor("unexpected tensor list in all-gather"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reduce-scatter gradients after backward pass
|
|
||||||
pub async fn reduce_scatter_gradients(&self, param_name: &str, full_gradient: &Tensor) -> Result<()> {
|
|
||||||
let _sharded_param = self.sharded_params.get(param_name)
|
|
||||||
.ok_or_else(|| DistributedError::tensor(
|
|
||||||
format!("parameter {} not found", param_name)
|
|
||||||
))?;
|
|
||||||
|
|
||||||
// Flatten gradient if needed
|
|
||||||
let flattened_grad = if self.config.flatten_parameters {
|
|
||||||
full_gradient.flatten()?
|
|
||||||
} else {
|
|
||||||
full_gradient.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reduce-scatter the gradient
|
|
||||||
use crate::comm::ReduceOp;
|
|
||||||
let _local_grad_shard = self.process_group
|
|
||||||
.reduce_scatter(&flattened_grad, ReduceOp::Sum).await?;
|
|
||||||
|
|
||||||
// In a real implementation, would update the local parameter shard here
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update memory usage statistics
|
|
||||||
async fn update_memory_stats(&mut self, original_params: &[Tensor]) -> Result<()> {
|
|
||||||
// Calculate total parameter memory
|
|
||||||
let mut total_param_memory = 0.0;
|
|
||||||
for param in original_params {
|
|
||||||
let param_size = param.shape().dims().iter().product::<usize>();
|
|
||||||
total_param_memory += param_size as f32 * 4.0; // Assuming f32
|
|
||||||
}
|
|
||||||
self.memory_stats.total_param_memory_mb = total_param_memory / (1024.0 * 1024.0);
|
|
||||||
|
|
||||||
// Calculate local shard memory
|
|
||||||
let mut local_shard_memory = 0.0;
|
|
||||||
for sharded_param in self.sharded_params.values() {
|
|
||||||
let shard_size = sharded_param.local_shard.shape().dims().iter().product::<usize>();
|
|
||||||
local_shard_memory += shard_size as f32 * 4.0; // Assuming f32
|
|
||||||
}
|
|
||||||
self.memory_stats.local_shard_memory_mb = local_shard_memory / (1024.0 * 1024.0);
|
|
||||||
|
|
||||||
// Calculate memory savings
|
|
||||||
self.memory_stats.memory_saved_mb =
|
|
||||||
self.memory_stats.total_param_memory_mb - self.memory_stats.local_shard_memory_mb;
|
|
||||||
|
|
||||||
// Peak all-gather memory is approximately total parameter memory
|
|
||||||
self.memory_stats.peak_allgather_memory_mb = self.memory_stats.total_param_memory_mb;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get memory usage statistics
|
|
||||||
pub fn memory_stats(&self) -> &FsdpMemoryStats {
|
|
||||||
&self.memory_stats
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate memory reduction percentage
|
|
||||||
pub fn memory_reduction_percent(&self) -> f32 {
|
|
||||||
if self.memory_stats.total_param_memory_mb > 0.0 {
|
|
||||||
(self.memory_stats.memory_saved_mb / self.memory_stats.total_param_memory_mb) * 100.0
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{Backend, BackendConfig};
|
|
||||||
use rtx_tensor::TensorShape;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_data_parallel() {
|
|
||||||
let config = BackendConfig::cpu();
|
|
||||||
let pg = ProcessGroup::new(Backend::Cpu, 2, 0, config).await.unwrap();
|
|
||||||
let mut dp = DataParallel::new(pg, 2);
|
|
||||||
|
|
||||||
dp.start_accumulation();
|
|
||||||
assert_eq!(dp.accumulation_progress(), 0.0);
|
|
||||||
|
|
||||||
// Create dummy gradients
|
|
||||||
let shape = TensorShape::new(vec![4]);
|
|
||||||
let mut gradients = vec![Tensor::ones(shape).unwrap()];
|
|
||||||
|
|
||||||
// First accumulation
|
|
||||||
let ready = dp.accumulate_gradients(&mut gradients).await.unwrap();
|
|
||||||
assert!(!ready);
|
|
||||||
assert_eq!(dp.accumulation_progress(), 0.5);
|
|
||||||
|
|
||||||
// Second accumulation should complete
|
|
||||||
let ready = dp.accumulate_gradients(&mut gradients).await.unwrap();
|
|
||||||
assert!(ready);
|
|
||||||
assert_eq!(dp.accumulation_progress(), 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_tensor_parallel() {
|
|
||||||
let config = BackendConfig::cpu();
|
|
||||||
let pg = ProcessGroup::new(Backend::Cpu, 2, 0, config).await.unwrap();
|
|
||||||
let sharding_config = TensorShardingConfig::default();
|
|
||||||
let tp = TensorParallel::new(pg, sharding_config);
|
|
||||||
|
|
||||||
// Create tensor that can be sharded (dimension 0 divisible by world size)
|
|
||||||
let shape = TensorShape::new(vec![4, 3]); // 4 is divisible by 2
|
|
||||||
let tensor = Tensor::ones(shape).unwrap();
|
|
||||||
|
|
||||||
let shard = tp.shard_tensor(&tensor).await.unwrap();
|
|
||||||
assert_eq!(shard.shape().dims()[0], 2); // 4/2 = 2
|
|
||||||
assert_eq!(shard.shape().dims()[1], 3); // Unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_pipeline_parallel() {
|
|
||||||
let config = BackendConfig::cpu();
|
|
||||||
let pg = ProcessGroup::new(Backend::Cpu, 2, 0, config).await.unwrap();
|
|
||||||
let stage_config = PipelineStageConfig {
|
|
||||||
stage_id: 0,
|
|
||||||
num_stages: 2,
|
|
||||||
layer_range: (0, 2),
|
|
||||||
async_schedule: false,
|
|
||||||
};
|
|
||||||
let pp = PipelineParallel::new(pg, stage_config, 4);
|
|
||||||
|
|
||||||
assert!(pp.is_first_stage());
|
|
||||||
assert!(!pp.is_last_stage());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_fsdp_sharding() {
|
|
||||||
let config = BackendConfig::cpu();
|
|
||||||
let pg = ProcessGroup::new(Backend::Cpu, 2, 0, config).await.unwrap();
|
|
||||||
let fsdp_config = FsdpConfig::default();
|
|
||||||
let mut fsdp = Fsdp::new(pg, fsdp_config);
|
|
||||||
|
|
||||||
// Create parameters
|
|
||||||
let shape1 = TensorShape::new(vec![2000]); // Large enough to shard
|
|
||||||
let shape2 = TensorShape::new(vec![100]); // Too small to shard
|
|
||||||
let params = vec![
|
|
||||||
Tensor::ones(shape1).unwrap(),
|
|
||||||
Tensor::ones(shape2).unwrap(),
|
|
||||||
];
|
|
||||||
|
|
||||||
fsdp.shard_parameters(¶ms).await.unwrap();
|
|
||||||
|
|
||||||
// Should only shard the first parameter (large enough)
|
|
||||||
assert_eq!(fsdp.sharded_params.len(), 1);
|
|
||||||
|
|
||||||
let memory_reduction = fsdp.memory_reduction_percent();
|
|
||||||
assert!(memory_reduction > 0.0);
|
|
||||||
assert!(memory_reduction < 100.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sharding_strategy() {
|
|
||||||
assert_eq!(ShardingStrategy::FullShard, ShardingStrategy::FullShard);
|
|
||||||
assert_ne!(ShardingStrategy::FullShard, ShardingStrategy::ShardGradOp);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fsdp_config_default() {
|
|
||||||
let config = FsdpConfig::default();
|
|
||||||
assert_eq!(config.sharding_strategy, ShardingStrategy::FullShard);
|
|
||||||
assert_eq!(config.min_param_size, 1000);
|
|
||||||
assert!(config.mixed_precision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,529 +0,0 @@
|
|||||||
//! Core Flash Attention implementation
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::FlashAttentionConfig,
|
|
||||||
error::{FlashError, FlashResult},
|
|
||||||
memory::SRAMManager,
|
|
||||||
kernels::FlashCudaKernels,
|
|
||||||
FlashOutput, FlashGradOutput, FlashStats,
|
|
||||||
};
|
|
||||||
use rtx_tensor::Tensor;
|
|
||||||
use rtx_runtime::{CudaBackend, Stream};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use tracing::{info, debug, warn, error};
|
|
||||||
|
|
||||||
/// Flash Attention backend trait for different implementations
|
|
||||||
#[async_trait]
|
|
||||||
pub trait FlashAttentionBackend {
|
|
||||||
/// Forward pass of Flash Attention
|
|
||||||
async fn forward(
|
|
||||||
&self,
|
|
||||||
q: &Tensor, // Query tensor [batch, heads, seq_len, head_dim]
|
|
||||||
k: &Tensor, // Key tensor [batch, heads, seq_len, head_dim]
|
|
||||||
v: &Tensor, // Value tensor [batch, heads, seq_len, head_dim]
|
|
||||||
causal: bool,
|
|
||||||
softmax_scale: f32,
|
|
||||||
) -> FlashResult<FlashOutput>;
|
|
||||||
|
|
||||||
/// Backward pass of Flash Attention
|
|
||||||
async fn backward(
|
|
||||||
&self,
|
|
||||||
dout: &Tensor, // Gradient w.r.t output [batch, heads, seq_len, head_dim]
|
|
||||||
q: &Tensor, // Query tensor
|
|
||||||
k: &Tensor, // Key tensor
|
|
||||||
v: &Tensor, // Value tensor
|
|
||||||
output: &Tensor, // Forward output
|
|
||||||
lse: &Tensor, // Log-sum-exp from forward pass
|
|
||||||
causal: bool,
|
|
||||||
softmax_scale: f32,
|
|
||||||
) -> FlashResult<FlashGradOutput>;
|
|
||||||
|
|
||||||
/// Get backend name for identification
|
|
||||||
fn name(&self) -> &str;
|
|
||||||
|
|
||||||
/// Check if backend supports the given configuration
|
|
||||||
fn supports_config(&self, config: &FlashAttentionConfig) -> bool;
|
|
||||||
|
|
||||||
/// Get optimal configuration for this backend
|
|
||||||
fn optimize_config(&self, config: FlashAttentionConfig) -> FlashResult<FlashAttentionConfig>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main Flash Attention implementation
|
|
||||||
pub struct FlashAttention {
|
|
||||||
config: FlashAttentionConfig,
|
|
||||||
cuda_backend: Arc<CudaBackend>,
|
|
||||||
kernels: Arc<FlashCudaKernels>,
|
|
||||||
sram_manager: Arc<SRAMManager>,
|
|
||||||
streams: Arc<RwLock<Vec<Stream>>>,
|
|
||||||
stats: Arc<RwLock<FlashStats>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlashAttention {
|
|
||||||
/// Create a new Flash Attention instance
|
|
||||||
pub fn new(config: FlashAttentionConfig) -> FlashResult<Self> {
|
|
||||||
// Validate configuration
|
|
||||||
config.validate()?;
|
|
||||||
|
|
||||||
info!("Initializing Flash Attention with config: {:?}", config);
|
|
||||||
|
|
||||||
// Initialize CUDA backend
|
|
||||||
let cuda_backend = Arc::new(CudaBackend::new(config.device_id)
|
|
||||||
.map_err(|e| FlashError::backend_init(format!("Failed to initialize CUDA backend: {}", e)))?);
|
|
||||||
|
|
||||||
// Initialize CUDA kernels
|
|
||||||
let kernels = Arc::new(FlashCudaKernels::new(&cuda_backend, &config)?);
|
|
||||||
|
|
||||||
// Initialize SRAM manager
|
|
||||||
let sram_manager = Arc::new(SRAMManager::new(&config, &cuda_backend)?);
|
|
||||||
|
|
||||||
// Initialize CUDA streams
|
|
||||||
let mut streams = Vec::new();
|
|
||||||
for i in 0..config.backend_config.cuda.stream_pool_size {
|
|
||||||
let stream = cuda_backend.create_stream()
|
|
||||||
.map_err(|e| FlashError::backend_init(format!("Failed to create stream {}: {}", i, e)))?;
|
|
||||||
streams.push(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stats = Arc::new(RwLock::new(FlashStats {
|
|
||||||
forward_time_us: 0,
|
|
||||||
backward_time_us: 0,
|
|
||||||
memory_usage: 0,
|
|
||||||
sram_efficiency: 0.0,
|
|
||||||
kernel_occupancy: 0.0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
cuda_backend,
|
|
||||||
kernels,
|
|
||||||
sram_manager,
|
|
||||||
streams: Arc::new(RwLock::new(streams)),
|
|
||||||
stats,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get configuration
|
|
||||||
pub fn config(&self) -> &FlashAttentionConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get execution statistics
|
|
||||||
pub fn stats(&self) -> FlashStats {
|
|
||||||
self.stats.read().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate input tensors
|
|
||||||
fn validate_inputs(&self, q: &Tensor, k: &Tensor, v: &Tensor) -> FlashResult<()> {
|
|
||||||
// Check tensor devices
|
|
||||||
if q.device() != k.device() || k.device() != v.device() {
|
|
||||||
return Err(FlashError::config("All tensors must be on the same device"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check tensor dtypes
|
|
||||||
if q.dtype() != k.dtype() || k.dtype() != v.dtype() {
|
|
||||||
return Err(FlashError::config("All tensors must have the same dtype"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check tensor shapes
|
|
||||||
let q_shape = q.shape();
|
|
||||||
let k_shape = k.shape();
|
|
||||||
let v_shape = v.shape();
|
|
||||||
|
|
||||||
if q_shape.len() != 4 || k_shape.len() != 4 || v_shape.len() != 4 {
|
|
||||||
return Err(FlashError::config("Input tensors must be 4D [batch, heads, seq_len, head_dim]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let [batch_q, heads_q, seq_q, dim_q] = q_shape[..] else {
|
|
||||||
return Err(FlashError::config("Invalid Q tensor shape"));
|
|
||||||
};
|
|
||||||
let [batch_k, heads_k, seq_k, dim_k] = k_shape[..] else {
|
|
||||||
return Err(FlashError::config("Invalid K tensor shape"));
|
|
||||||
};
|
|
||||||
let [batch_v, heads_v, seq_v, dim_v] = v_shape[..] else {
|
|
||||||
return Err(FlashError::config("Invalid V tensor shape"));
|
|
||||||
};
|
|
||||||
|
|
||||||
if batch_q != batch_k || batch_k != batch_v {
|
|
||||||
return Err(FlashError::shape_mismatch(
|
|
||||||
vec![batch_q, heads_q, seq_q, dim_q],
|
|
||||||
vec![batch_k, heads_k, seq_k, dim_k],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if heads_q != heads_k || heads_k != heads_v {
|
|
||||||
return Err(FlashError::shape_mismatch(
|
|
||||||
vec![batch_q, heads_q, seq_q, dim_q],
|
|
||||||
vec![batch_k, heads_k, seq_k, dim_k],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if seq_q != seq_k || seq_k != seq_v {
|
|
||||||
return Err(FlashError::shape_mismatch(
|
|
||||||
vec![batch_q, heads_q, seq_q, dim_q],
|
|
||||||
vec![batch_k, heads_k, seq_k, dim_k],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if dim_q != dim_k || dim_k != dim_v {
|
|
||||||
return Err(FlashError::shape_mismatch(
|
|
||||||
vec![batch_q, heads_q, seq_q, dim_q],
|
|
||||||
vec![batch_k, heads_k, seq_k, dim_k],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check dimensions match configuration
|
|
||||||
if heads_q != self.config.num_heads {
|
|
||||||
return Err(FlashError::config(format!(
|
|
||||||
"Number of heads mismatch: expected {}, got {}",
|
|
||||||
self.config.num_heads, heads_q
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if dim_q != self.config.head_dim {
|
|
||||||
return Err(FlashError::config(format!(
|
|
||||||
"Head dimension mismatch: expected {}, got {}",
|
|
||||||
self.config.head_dim, dim_q
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check sequence length constraints
|
|
||||||
if seq_q > self.config.max_seq_len {
|
|
||||||
return Err(FlashError::config(format!(
|
|
||||||
"Sequence length {} exceeds maximum {}",
|
|
||||||
seq_q, self.config.max_seq_len
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get next available stream
|
|
||||||
fn get_stream(&self) -> Stream {
|
|
||||||
let streams = self.streams.read();
|
|
||||||
// Simple round-robin selection for now
|
|
||||||
// In the future, we could implement more sophisticated stream selection
|
|
||||||
streams[0].clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update statistics
|
|
||||||
fn update_stats(&self, forward_time: u64, memory_usage: usize, sram_efficiency: f32, occupancy: f32) {
|
|
||||||
let mut stats = self.stats.write();
|
|
||||||
stats.forward_time_us = forward_time;
|
|
||||||
stats.memory_usage = memory_usage;
|
|
||||||
stats.sram_efficiency = sram_efficiency;
|
|
||||||
stats.kernel_occupancy = occupancy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl FlashAttentionBackend for FlashAttention {
|
|
||||||
async fn forward(
|
|
||||||
&self,
|
|
||||||
q: &Tensor,
|
|
||||||
k: &Tensor,
|
|
||||||
v: &Tensor,
|
|
||||||
causal: bool,
|
|
||||||
softmax_scale: f32,
|
|
||||||
) -> FlashResult<FlashOutput> {
|
|
||||||
debug!("Starting Flash Attention forward pass");
|
|
||||||
let start_time = std::time::Instant::now();
|
|
||||||
|
|
||||||
// Validate inputs
|
|
||||||
self.validate_inputs(q, k, v)?;
|
|
||||||
|
|
||||||
let batch_size = q.shape()[0];
|
|
||||||
let seq_len = q.shape()[2];
|
|
||||||
|
|
||||||
// Get stream for execution
|
|
||||||
let stream = self.get_stream();
|
|
||||||
|
|
||||||
// Allocate output tensors
|
|
||||||
let output_shape = q.shape().to_vec();
|
|
||||||
let lse_shape = vec![batch_size, self.config.num_heads, seq_len];
|
|
||||||
|
|
||||||
let output = Tensor::zeros(&output_shape, q.dtype(), q.device())?;
|
|
||||||
let lse = Tensor::zeros(&lse_shape, q.dtype(), q.device())?;
|
|
||||||
|
|
||||||
// Execute Flash Attention kernel
|
|
||||||
let kernel_result = self.kernels.flash_attention_forward(
|
|
||||||
q, k, v, &output, &lse,
|
|
||||||
causal, softmax_scale, &stream
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
let elapsed = start_time.elapsed();
|
|
||||||
let forward_time_us = elapsed.as_micros() as u64;
|
|
||||||
|
|
||||||
// Calculate memory usage
|
|
||||||
let memory_usage = self.config.estimate_memory_usage(batch_size, seq_len);
|
|
||||||
|
|
||||||
// Get SRAM efficiency from manager
|
|
||||||
let sram_efficiency = self.sram_manager.get_efficiency();
|
|
||||||
|
|
||||||
// Get kernel occupancy from result
|
|
||||||
let kernel_occupancy = kernel_result.occupancy;
|
|
||||||
|
|
||||||
// Update statistics
|
|
||||||
self.update_stats(forward_time_us, memory_usage, sram_efficiency, kernel_occupancy);
|
|
||||||
|
|
||||||
let stats = FlashStats {
|
|
||||||
forward_time_us,
|
|
||||||
backward_time_us: 0,
|
|
||||||
memory_usage,
|
|
||||||
sram_efficiency,
|
|
||||||
kernel_occupancy,
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("Flash Attention forward completed in {}μs", forward_time_us);
|
|
||||||
|
|
||||||
Ok(FlashOutput {
|
|
||||||
output,
|
|
||||||
lse,
|
|
||||||
stats,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn backward(
|
|
||||||
&self,
|
|
||||||
dout: &Tensor,
|
|
||||||
q: &Tensor,
|
|
||||||
k: &Tensor,
|
|
||||||
v: &Tensor,
|
|
||||||
output: &Tensor,
|
|
||||||
lse: &Tensor,
|
|
||||||
causal: bool,
|
|
||||||
softmax_scale: f32,
|
|
||||||
) -> FlashResult<FlashGradOutput> {
|
|
||||||
debug!("Starting Flash Attention backward pass");
|
|
||||||
let start_time = std::time::Instant::now();
|
|
||||||
|
|
||||||
// Validate inputs
|
|
||||||
self.validate_inputs(q, k, v)?;
|
|
||||||
self.validate_inputs(dout, q, k)?; // dout should match q shape
|
|
||||||
|
|
||||||
let batch_size = q.shape()[0];
|
|
||||||
let seq_len = q.shape()[2];
|
|
||||||
|
|
||||||
// Get stream for execution
|
|
||||||
let stream = self.get_stream();
|
|
||||||
|
|
||||||
// Allocate gradient tensors
|
|
||||||
let dq = Tensor::zeros(q.shape(), q.dtype(), q.device())?;
|
|
||||||
let dk = Tensor::zeros(k.shape(), k.dtype(), k.device())?;
|
|
||||||
let dv = Tensor::zeros(v.shape(), v.dtype(), v.device())?;
|
|
||||||
|
|
||||||
// Execute Flash Attention backward kernel
|
|
||||||
let kernel_result = self.kernels.flash_attention_backward(
|
|
||||||
dout, q, k, v, output, lse,
|
|
||||||
&dq, &dk, &dv,
|
|
||||||
causal, softmax_scale, &stream
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
let elapsed = start_time.elapsed();
|
|
||||||
let backward_time_us = elapsed.as_micros() as u64;
|
|
||||||
|
|
||||||
// Calculate memory usage
|
|
||||||
let memory_usage = self.config.estimate_memory_usage(batch_size, seq_len);
|
|
||||||
|
|
||||||
// Get SRAM efficiency from manager
|
|
||||||
let sram_efficiency = self.sram_manager.get_efficiency();
|
|
||||||
|
|
||||||
// Get kernel occupancy from result
|
|
||||||
let kernel_occupancy = kernel_result.occupancy;
|
|
||||||
|
|
||||||
let stats = FlashStats {
|
|
||||||
forward_time_us: 0,
|
|
||||||
backward_time_us,
|
|
||||||
memory_usage,
|
|
||||||
sram_efficiency,
|
|
||||||
kernel_occupancy,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update global statistics
|
|
||||||
{
|
|
||||||
let mut global_stats = self.stats.write();
|
|
||||||
global_stats.backward_time_us = backward_time_us;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Flash Attention backward completed in {}μs", backward_time_us);
|
|
||||||
|
|
||||||
Ok(FlashGradOutput {
|
|
||||||
dq,
|
|
||||||
dk,
|
|
||||||
dv,
|
|
||||||
stats,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"FlashAttention"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supports_config(&self, config: &FlashAttentionConfig) -> bool {
|
|
||||||
// Check if configuration is compatible with this backend
|
|
||||||
config.validate().is_ok() &&
|
|
||||||
config.num_heads <= 128 && // Reasonable upper limit
|
|
||||||
config.head_dim <= 512 && // Reasonable upper limit
|
|
||||||
config.max_seq_len <= 65_536 // 64K context limit
|
|
||||||
}
|
|
||||||
|
|
||||||
fn optimize_config(&self, mut config: FlashAttentionConfig) -> FlashResult<FlashAttentionConfig> {
|
|
||||||
// Optimize block sizes based on GPU memory hierarchy
|
|
||||||
let gpu_info = self.cuda_backend.get_device_info()
|
|
||||||
.map_err(|e| FlashError::backend_init(format!("Failed to get GPU info: {}", e)))?;
|
|
||||||
|
|
||||||
let shared_memory_size = gpu_info.shared_memory_per_block;
|
|
||||||
let max_threads_per_block = gpu_info.max_threads_per_block;
|
|
||||||
|
|
||||||
// Calculate optimal block sizes based on shared memory constraints
|
|
||||||
let element_size = match config.precision {
|
|
||||||
crate::config::PrecisionMode::FP32 => 4,
|
|
||||||
crate::config::PrecisionMode::FP16 |
|
|
||||||
crate::config::PrecisionMode::BF16 => 2,
|
|
||||||
crate::config::PrecisionMode::FP8E4M3 { .. } |
|
|
||||||
crate::config::PrecisionMode::FP8E5M2 { .. } => 1,
|
|
||||||
crate::config::PrecisionMode::Mixed { storage_precision, .. } => {
|
|
||||||
match storage_precision {
|
|
||||||
crate::config::Precision::FP32 => 4,
|
|
||||||
crate::config::Precision::FP16 |
|
|
||||||
crate::config::Precision::BF16 => 2,
|
|
||||||
crate::config::Precision::INT8 |
|
|
||||||
crate::config::Precision::FP8E4M3 |
|
|
||||||
crate::config::Precision::FP8E5M2 => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate block sizes that fit in shared memory
|
|
||||||
let memory_per_element = element_size * config.head_dim;
|
|
||||||
let max_block_size = (shared_memory_size / (2 * memory_per_element)).min(256);
|
|
||||||
|
|
||||||
// Round down to nearest multiple of 32 for coalescing
|
|
||||||
let optimal_block_size = (max_block_size / 32) * 32;
|
|
||||||
|
|
||||||
if optimal_block_size >= 32 {
|
|
||||||
config.block_size_q = optimal_block_size;
|
|
||||||
config.block_size_kv = optimal_block_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimize memory pool based on estimated usage
|
|
||||||
let estimated_memory = config.estimate_memory_usage(8, config.max_seq_len); // Estimate for batch size 8
|
|
||||||
config.backend_config.cuda.memory_pool.initial_size = estimated_memory.max(
|
|
||||||
config.backend_config.cuda.memory_pool.initial_size
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("Optimized Flash Attention config: block_size={}x{}, memory_pool={}MB",
|
|
||||||
config.block_size_q, config.block_size_kv,
|
|
||||||
config.backend_config.cuda.memory_pool.initial_size / (1024 * 1024));
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flash Attention factory for creating different backend implementations
|
|
||||||
pub struct FlashAttentionFactory;
|
|
||||||
|
|
||||||
impl FlashAttentionFactory {
|
|
||||||
/// Create the best available Flash Attention backend for the given configuration
|
|
||||||
pub fn create_backend(config: FlashAttentionConfig) -> FlashResult<Box<dyn FlashAttentionBackend>> {
|
|
||||||
// For now, we only have the standard CUDA backend
|
|
||||||
// In the future, this will select between CUDA, Quantum, Neuromorphic, and Edge backends
|
|
||||||
let backend = FlashAttention::new(config)?;
|
|
||||||
Ok(Box::new(backend))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a quantum-enhanced Flash Attention backend
|
|
||||||
#[cfg(feature = "quantum")]
|
|
||||||
pub fn create_quantum_backend(config: FlashAttentionConfig) -> FlashResult<Box<dyn FlashAttentionBackend>> {
|
|
||||||
use crate::variants::quantum::QuantumFlashAttention;
|
|
||||||
let backend = QuantumFlashAttention::new(config)?;
|
|
||||||
Ok(Box::new(backend))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a neuromorphic Flash Attention backend
|
|
||||||
#[cfg(feature = "neuromorphic")]
|
|
||||||
pub fn create_neuromorphic_backend(config: FlashAttentionConfig) -> FlashResult<Box<dyn FlashAttentionBackend>> {
|
|
||||||
use crate::variants::neuromorphic::NeuromorphicFlashAttention;
|
|
||||||
let backend = NeuromorphicFlashAttention::new(config)?;
|
|
||||||
Ok(Box::new(backend))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an edge-optimized Flash Attention backend
|
|
||||||
#[cfg(feature = "edge")]
|
|
||||||
pub fn create_edge_backend(config: FlashAttentionConfig) -> FlashResult<Box<dyn FlashAttentionBackend>> {
|
|
||||||
use crate::variants::edge::EdgeFlashAttention;
|
|
||||||
let backend = EdgeFlashAttention::new(config)?;
|
|
||||||
Ok(Box::new(backend))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List all available backends
|
|
||||||
pub fn available_backends() -> Vec<&'static str> {
|
|
||||||
let mut backends = vec!["FlashAttention"];
|
|
||||||
|
|
||||||
#[cfg(feature = "quantum")]
|
|
||||||
backends.push("QuantumFlashAttention");
|
|
||||||
|
|
||||||
#[cfg(feature = "neuromorphic")]
|
|
||||||
backends.push("NeuromorphicFlashAttention");
|
|
||||||
|
|
||||||
#[cfg(feature = "edge")]
|
|
||||||
backends.push("EdgeFlashAttention");
|
|
||||||
|
|
||||||
backends
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rtx_tensor::{Device, DType};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_flash_attention_creation() {
|
|
||||||
let config = FlashAttentionConfig::new(8, 64);
|
|
||||||
|
|
||||||
// This will fail in CI without CUDA, but that's expected
|
|
||||||
match FlashAttention::new(config) {
|
|
||||||
Ok(flash) => {
|
|
||||||
assert_eq!(flash.config().num_heads, 8);
|
|
||||||
assert_eq!(flash.config().head_dim, 64);
|
|
||||||
}
|
|
||||||
Err(FlashError::BackendInit { .. }) => {
|
|
||||||
// Expected in CI without CUDA
|
|
||||||
}
|
|
||||||
Err(e) => panic!("Unexpected error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_validation() {
|
|
||||||
let config = FlashAttentionConfig::new(8, 64);
|
|
||||||
|
|
||||||
// This will fail in CI, but we can test the error path
|
|
||||||
if let Ok(flash) = FlashAttention::new(config) {
|
|
||||||
// Create invalid tensors (different shapes)
|
|
||||||
let q = Tensor::zeros(&[2, 8, 128, 64], DType::F32, Device::cuda(0).unwrap_or(Device::default())).unwrap();
|
|
||||||
let k = Tensor::zeros(&[2, 8, 64, 64], DType::F32, Device::cuda(0).unwrap_or(Device::default())).unwrap(); // Wrong seq_len
|
|
||||||
let v = Tensor::zeros(&[2, 8, 128, 64], DType::F32, Device::cuda(0).unwrap_or(Device::default())).unwrap();
|
|
||||||
|
|
||||||
let result = flash.validate_inputs(&q, &k, &v);
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backend_factory() {
|
|
||||||
let backends = FlashAttentionFactory::available_backends();
|
|
||||||
assert!(backends.contains(&"FlashAttention"));
|
|
||||||
|
|
||||||
#[cfg(feature = "quantum")]
|
|
||||||
assert!(backends.contains(&"QuantumFlashAttention"));
|
|
||||||
|
|
||||||
#[cfg(feature = "neuromorphic")]
|
|
||||||
assert!(backends.contains(&"NeuromorphicFlashAttention"));
|
|
||||||
|
|
||||||
#[cfg(feature = "edge")]
|
|
||||||
assert!(backends.contains(&"EdgeFlashAttention"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
//! # RTX Flash Attention
|
|
||||||
//!
|
|
||||||
//! Revolutionary Flash Attention implementation providing 5-8x speedup over existing solutions
|
|
||||||
//! with O(n) memory complexity and support for quantum, neuromorphic, and edge computing variants.
|
|
||||||
//!
|
|
||||||
//! ## Features
|
|
||||||
//!
|
|
||||||
//! - **Core Flash Attention**: O(n) memory forward/backward passes with SRAM tiling
|
|
||||||
//! - **Quantum-Enhanced**: Quantum pattern optimization for 2-3x additional speedup
|
|
||||||
//! - **Neuromorphic**: Spike-based computation with 1000x energy efficiency
|
|
||||||
//! - **Edge-Optimized**: Deployment to resource-constrained platforms
|
|
||||||
//! - **32K+ Context**: Support for extremely long sequences
|
|
||||||
//!
|
|
||||||
//! ## Usage
|
|
||||||
//!
|
|
||||||
//! ```rust,no_run
|
|
||||||
//! use rtx_flash_attention::{FlashAttention, FlashAttentionConfig};
|
|
||||||
//! use rtx_tensor::Tensor;
|
|
||||||
//!
|
|
||||||
//! let config = FlashAttentionConfig {
|
|
||||||
//! num_heads: 32,
|
|
||||||
//! head_dim: 128,
|
|
||||||
//! block_size_q: 64,
|
|
||||||
//! block_size_kv: 64,
|
|
||||||
//! causal: true,
|
|
||||||
//! };
|
|
||||||
//!
|
|
||||||
//! let flash_attention = FlashAttention::new(config)?;
|
|
||||||
//!
|
|
||||||
//! let (output, lse) = flash_attention.forward(&q, &k, &v)?;
|
|
||||||
//! # Ok::<(), Box<dyn std::error::Error>>(())
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub mod config;
|
|
||||||
pub mod core;
|
|
||||||
pub mod kernels;
|
|
||||||
pub mod memory;
|
|
||||||
|
|
||||||
#[cfg(any(feature = "quantum", feature = "neuromorphic", feature = "edge"))]
|
|
||||||
pub mod variants {
|
|
||||||
#[cfg(feature = "quantum")]
|
|
||||||
pub mod quantum;
|
|
||||||
#[cfg(feature = "neuromorphic")]
|
|
||||||
pub mod neuromorphic;
|
|
||||||
#[cfg(feature = "edge")]
|
|
||||||
pub mod edge;
|
|
||||||
|
|
||||||
#[cfg(feature = "quantum")]
|
|
||||||
pub use quantum::*;
|
|
||||||
#[cfg(feature = "neuromorphic")]
|
|
||||||
pub use neuromorphic::*;
|
|
||||||
#[cfg(feature = "edge")]
|
|
||||||
pub use edge::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-exports
|
|
||||||
pub use error::{FlashError, FlashResult};
|
|
||||||
pub use config::FlashAttentionConfig;
|
|
||||||
pub use core::{FlashAttention, FlashAttentionBackend};
|
|
||||||
pub use memory::{SRAMManager, BlockManager};
|
|
||||||
|
|
||||||
use rtx_tensor::Tensor;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
|
|
||||||
/// Flash Attention execution statistics
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct FlashStats {
|
|
||||||
/// Forward pass execution time (microseconds)
|
|
||||||
pub forward_time_us: u64,
|
|
||||||
/// Backward pass execution time (microseconds)
|
|
||||||
pub backward_time_us: u64,
|
|
||||||
/// Memory usage (bytes)
|
|
||||||
pub memory_usage: usize,
|
|
||||||
/// SRAM efficiency (0.0 - 1.0)
|
|
||||||
pub sram_efficiency: f32,
|
|
||||||
/// Kernel occupancy (0.0 - 1.0)
|
|
||||||
pub kernel_occupancy: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flash Attention output containing result and metadata
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FlashOutput {
|
|
||||||
/// Attention output tensor [batch, heads, seq_len, head_dim]
|
|
||||||
pub output: Tensor,
|
|
||||||
/// Log-sum-exp for numerical stability [batch, heads, seq_len]
|
|
||||||
pub lse: Tensor,
|
|
||||||
/// Execution statistics
|
|
||||||
pub stats: FlashStats,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flash Attention gradient output
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FlashGradOutput {
|
|
||||||
/// Query gradients [batch, heads, seq_len, head_dim]
|
|
||||||
pub dq: Tensor,
|
|
||||||
/// Key gradients [batch, heads, seq_len, head_dim]
|
|
||||||
pub dk: Tensor,
|
|
||||||
/// Value gradients [batch, heads, seq_len, head_dim]
|
|
||||||
pub dv: Tensor,
|
|
||||||
/// Execution statistics
|
|
||||||
pub stats: FlashStats,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Global Flash Attention registry for backend management
|
|
||||||
pub struct FlashRegistry {
|
|
||||||
backends: Arc<RwLock<Vec<Box<dyn FlashAttentionBackend + Send + Sync>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlashRegistry {
|
|
||||||
/// Create a new Flash Attention registry
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
backends: Arc::new(RwLock::new(Vec::new())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register a new Flash Attention backend
|
|
||||||
pub fn register_backend<B>(&self, backend: B)
|
|
||||||
where
|
|
||||||
B: FlashAttentionBackend + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
self.backends.write().push(Box::new(backend));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the best available backend for the given configuration
|
|
||||||
pub fn get_backend(&self, config: &FlashAttentionConfig) -> Option<&dyn FlashAttentionBackend> {
|
|
||||||
// For now, return the first backend. In the future, we could implement
|
|
||||||
// sophisticated backend selection based on hardware capabilities and workload
|
|
||||||
self.backends.read().first().map(|b| b.as_ref())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FlashRegistry {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Global Flash Attention registry instance
|
|
||||||
static FLASH_REGISTRY: std::sync::OnceLock<FlashRegistry> = std::sync::OnceLock::new();
|
|
||||||
|
|
||||||
/// Get the global Flash Attention registry
|
|
||||||
pub fn global_registry() -> &'static FlashRegistry {
|
|
||||||
FLASH_REGISTRY.get_or_init(FlashRegistry::default)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_flash_registry() {
|
|
||||||
let registry = FlashRegistry::new();
|
|
||||||
assert!(registry.backends.read().is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_global_registry() {
|
|
||||||
let registry = global_registry();
|
|
||||||
assert!(registry.backends.read().is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
//! Curriculum-aware data loader.
|
|
||||||
|
|
||||||
use super::sample::Sample;
|
|
||||||
use super::scorers::DifficultyScorer;
|
|
||||||
use super::strategies::CurriculumStrategy;
|
|
||||||
use super::schedules::Schedule;
|
|
||||||
use super::state::CurriculumState;
|
|
||||||
|
|
||||||
/// Data loader with curriculum learning integration
|
|
||||||
pub struct CurriculumDataLoader<S: Sample> {
|
|
||||||
samples: Vec<S>,
|
|
||||||
strategy: Box<dyn CurriculumStrategy>,
|
|
||||||
difficulty_scorer: Box<dyn DifficultyScorer>,
|
|
||||||
schedule: Box<dyn Schedule>,
|
|
||||||
batch_size: usize,
|
|
||||||
curriculum_state: CurriculumState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: Sample> CurriculumDataLoader<S> {
|
|
||||||
pub fn new(
|
|
||||||
samples: Vec<S>,
|
|
||||||
strategy: Box<dyn CurriculumStrategy>,
|
|
||||||
difficulty_scorer: Box<dyn DifficultyScorer>,
|
|
||||||
schedule: Box<dyn Schedule>,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
samples,
|
|
||||||
strategy,
|
|
||||||
difficulty_scorer,
|
|
||||||
schedule,
|
|
||||||
batch_size,
|
|
||||||
curriculum_state: CurriculumState::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_batch(&mut self) -> Option<Vec<S>> {
|
|
||||||
if self.samples.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update difficulty threshold from schedule
|
|
||||||
self.curriculum_state.difficulty_threshold =
|
|
||||||
self.schedule.get_difficulty_at_step(self.curriculum_state.step);
|
|
||||||
|
|
||||||
// Update strategy's threshold
|
|
||||||
self.strategy.update_difficulty_threshold(&mut self.curriculum_state);
|
|
||||||
|
|
||||||
// Select samples using strategy
|
|
||||||
let batch = self.strategy.select_samples(
|
|
||||||
&self.samples,
|
|
||||||
self.difficulty_scorer.as_ref(),
|
|
||||||
&mut self.curriculum_state,
|
|
||||||
self.batch_size,
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(batch)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn step(&mut self) {
|
|
||||||
self.curriculum_state.step += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_curriculum_state(&self) -> &CurriculumState {
|
|
||||||
&self.curriculum_state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
//! Curriculum learning errors.
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
/// Errors that can occur during curriculum learning
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum CurriculumError {
|
|
||||||
#[error("Invalid difficulty score: {0}")]
|
|
||||||
InvalidDifficultyScore(f32),
|
|
||||||
#[error("Invalid schedule parameters: {0}")]
|
|
||||||
InvalidScheduleParameters(String),
|
|
||||||
#[error("Sample not found: {0}")]
|
|
||||||
SampleNotFound(usize),
|
|
||||||
#[error("Strategy configuration error: {0}")]
|
|
||||||
StrategyConfiguration(String),
|
|
||||||
#[error("Performance tracking error: {0}")]
|
|
||||||
PerformanceTracking(String),
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
//! Curriculum learning metrics.
|
|
||||||
|
|
||||||
/// Comprehensive metrics for curriculum learning
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CurriculumMetrics {
|
|
||||||
pub batches_processed: usize,
|
|
||||||
pub total_samples_seen: usize,
|
|
||||||
pub samples_selected: usize,
|
|
||||||
pub difficulty_distributions: Vec<Vec<f32>>,
|
|
||||||
pub performance_history: Vec<f32>,
|
|
||||||
pub selection_history: Vec<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumMetrics {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
batches_processed: 0,
|
|
||||||
total_samples_seen: 0,
|
|
||||||
samples_selected: 0,
|
|
||||||
difficulty_distributions: Vec::new(),
|
|
||||||
performance_history: Vec::new(),
|
|
||||||
selection_history: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_batch_difficulty_distribution(&mut self, difficulties: Vec<f32>) {
|
|
||||||
self.batches_processed += 1;
|
|
||||||
self.total_samples_seen += difficulties.len();
|
|
||||||
self.difficulty_distributions.push(difficulties);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_sample_selection_stats(&mut self, total_samples: usize, selected_samples: usize) {
|
|
||||||
self.total_samples_seen += total_samples;
|
|
||||||
self.samples_selected += selected_samples;
|
|
||||||
self.selection_history.push(selected_samples);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_performance_tracking(&mut self, performance: f32) {
|
|
||||||
self.performance_history.push(performance);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_selection_stats(&self) -> SelectionStats {
|
|
||||||
let selection_ratio = if self.total_samples_seen > 0 {
|
|
||||||
self.samples_selected as f32 / self.total_samples_seen as f32
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let avg_difficulty = if !self.difficulty_distributions.is_empty() {
|
|
||||||
let total_difficulty: f32 = self.difficulty_distributions
|
|
||||||
.iter()
|
|
||||||
.flat_map(|d| d.iter())
|
|
||||||
.sum();
|
|
||||||
let total_count: usize = self.difficulty_distributions
|
|
||||||
.iter()
|
|
||||||
.map(|d| d.len())
|
|
||||||
.sum();
|
|
||||||
total_difficulty / total_count as f32
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
SelectionStats {
|
|
||||||
selection_ratio,
|
|
||||||
avg_difficulty,
|
|
||||||
batches_processed: self.batches_processed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
*self = Self::new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CurriculumMetrics {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SelectionStats {
|
|
||||||
pub selection_ratio: f32,
|
|
||||||
pub avg_difficulty: f32,
|
|
||||||
pub batches_processed: usize,
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
//! Curriculum optimizer for hyperparameter tuning.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Curriculum optimizer for hyperparameter tuning
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CurriculumOptimizer {
|
|
||||||
configuration_performance: HashMap<CurriculumConfig, f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type OrderedFloat = ordered_float::OrderedFloat<f32>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct CurriculumConfig {
|
|
||||||
pub strategy_type: CurriculumStrategyType,
|
|
||||||
pub schedule_type: ScheduleType,
|
|
||||||
pub parameters: Vec<OrderedFloat>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum CurriculumStrategyType {
|
|
||||||
EasyToHard,
|
|
||||||
AntiCurriculum,
|
|
||||||
SelfPaced,
|
|
||||||
CompetencyBased,
|
|
||||||
DataDriven,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum ScheduleType {
|
|
||||||
Linear,
|
|
||||||
Exponential,
|
|
||||||
Adaptive,
|
|
||||||
Cyclic,
|
|
||||||
MultiTask,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumOptimizer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
configuration_performance: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_configuration_performance(&mut self, config: CurriculumConfig, performance: f32) {
|
|
||||||
self.configuration_performance.insert(config, performance);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_best_configuration(&self) -> Option<CurriculumConfig> {
|
|
||||||
self.configuration_performance
|
|
||||||
.iter()
|
|
||||||
.max_by(|a, b| a.1.total_cmp(b.1))
|
|
||||||
.map(|(config, _)| config.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn optimize_hyperparameters(&self, base_config: &CurriculumConfig, _iterations: usize) -> Option<CurriculumConfig> {
|
|
||||||
// Simple optimization: return best known configuration
|
|
||||||
// In a real implementation, this would use more sophisticated optimization
|
|
||||||
self.get_best_configuration().or_else(|| Some(base_config.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CurriculumOptimizer {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//! Sample trait for curriculum learning.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Trait for training samples that can be used with curriculum learning
|
|
||||||
pub trait Sample: Clone {
|
|
||||||
/// Unique identifier for the sample
|
|
||||||
fn id(&self) -> usize;
|
|
||||||
|
|
||||||
/// Extract complexity features for difficulty scoring
|
|
||||||
fn complexity_features(&self) -> HashMap<String, f32>;
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
//! Curriculum-aware batch sampler.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use super::sample::Sample;
|
|
||||||
|
|
||||||
/// Curriculum-aware batch sampler
|
|
||||||
pub struct CurriculumBatchSampler {
|
|
||||||
batch_size: usize,
|
|
||||||
temperature: f32,
|
|
||||||
sampling_strategy: SamplingStrategy,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum SamplingStrategy {
|
|
||||||
WeightedRandom,
|
|
||||||
Uniform,
|
|
||||||
ThresholdBased,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumBatchSampler {
|
|
||||||
pub fn new(batch_size: usize, temperature: f32, sampling_strategy: SamplingStrategy) -> Self {
|
|
||||||
Self {
|
|
||||||
batch_size,
|
|
||||||
temperature,
|
|
||||||
sampling_strategy,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sample_batch<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scores: &HashMap<usize, f32>,
|
|
||||||
difficulty_threshold: f32,
|
|
||||||
) -> Vec<usize> {
|
|
||||||
match self.sampling_strategy {
|
|
||||||
SamplingStrategy::WeightedRandom => {
|
|
||||||
self.weighted_random_sampling(samples, difficulty_scores, difficulty_threshold)
|
|
||||||
}
|
|
||||||
SamplingStrategy::Uniform => {
|
|
||||||
self.uniform_sampling(samples, difficulty_scores, difficulty_threshold)
|
|
||||||
}
|
|
||||||
SamplingStrategy::ThresholdBased => {
|
|
||||||
self.threshold_based_sampling(samples, difficulty_scores, difficulty_threshold)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn weighted_random_sampling<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scores: &HashMap<usize, f32>,
|
|
||||||
_difficulty_threshold: f32,
|
|
||||||
) -> Vec<usize> {
|
|
||||||
// Simple implementation: select based on inverse difficulty weights
|
|
||||||
let mut indices = Vec::new();
|
|
||||||
let total_samples = self.batch_size.min(samples.len());
|
|
||||||
|
|
||||||
for (idx, sample) in samples.iter().enumerate() {
|
|
||||||
if indices.len() >= total_samples {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let score = difficulty_scores.get(&sample.id()).copied().unwrap_or(0.5);
|
|
||||||
let weight = 1.0 / (1.0 + score * self.temperature);
|
|
||||||
|
|
||||||
// Simple selection based on weight (in real implementation would use proper random sampling)
|
|
||||||
if weight > 0.5 {
|
|
||||||
indices.push(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill up to batch_size if needed
|
|
||||||
while indices.len() < total_samples && indices.len() < samples.len() {
|
|
||||||
for (idx, _) in samples.iter().enumerate() {
|
|
||||||
if !indices.contains(&idx) {
|
|
||||||
indices.push(idx);
|
|
||||||
if indices.len() >= total_samples {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
indices
|
|
||||||
}
|
|
||||||
|
|
||||||
fn uniform_sampling<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
_difficulty_scores: &HashMap<usize, f32>,
|
|
||||||
_difficulty_threshold: f32,
|
|
||||||
) -> Vec<usize> {
|
|
||||||
let total_samples = self.batch_size.min(samples.len());
|
|
||||||
(0..total_samples).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn threshold_based_sampling<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scores: &HashMap<usize, f32>,
|
|
||||||
difficulty_threshold: f32,
|
|
||||||
) -> Vec<usize> {
|
|
||||||
let mut indices = Vec::new();
|
|
||||||
|
|
||||||
for (idx, sample) in samples.iter().enumerate() {
|
|
||||||
if indices.len() >= self.batch_size {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let score = difficulty_scores.get(&sample.id()).copied().unwrap_or(0.5);
|
|
||||||
if score <= difficulty_threshold {
|
|
||||||
indices.push(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
indices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
//! Curriculum schedule implementations.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Trait for curriculum scheduling
|
|
||||||
pub trait Schedule: Send + Sync {
|
|
||||||
/// Get difficulty threshold at given training step
|
|
||||||
fn get_difficulty_at_step(&self, step: usize) -> f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Linear curriculum schedule
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LinearSchedule {
|
|
||||||
initial_difficulty: f32,
|
|
||||||
final_difficulty: f32,
|
|
||||||
total_steps: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinearSchedule {
|
|
||||||
pub fn new(initial_difficulty: f32, final_difficulty: f32, total_steps: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_difficulty,
|
|
||||||
final_difficulty,
|
|
||||||
total_steps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Schedule for LinearSchedule {
|
|
||||||
fn get_difficulty_at_step(&self, step: usize) -> f32 {
|
|
||||||
if step >= self.total_steps {
|
|
||||||
return self.final_difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
let progress = step as f32 / self.total_steps as f32;
|
|
||||||
self.initial_difficulty + progress * (self.final_difficulty - self.initial_difficulty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exponential curriculum schedule
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ExponentialSchedule {
|
|
||||||
initial_difficulty: f32,
|
|
||||||
final_difficulty: f32,
|
|
||||||
growth_rate: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExponentialSchedule {
|
|
||||||
pub fn new(initial_difficulty: f32, final_difficulty: f32, growth_rate: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_difficulty,
|
|
||||||
final_difficulty,
|
|
||||||
growth_rate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Schedule for ExponentialSchedule {
|
|
||||||
fn get_difficulty_at_step(&self, step: usize) -> f32 {
|
|
||||||
let exp_factor = 1.0 - (-self.growth_rate * step as f32).exp();
|
|
||||||
let difficulty = self.initial_difficulty + exp_factor * (self.final_difficulty - self.initial_difficulty);
|
|
||||||
difficulty.min(self.final_difficulty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adaptive curriculum schedule (adjusts based on performance)
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AdaptiveSchedule {
|
|
||||||
initial_difficulty: f32,
|
|
||||||
final_difficulty: f32,
|
|
||||||
adaptation_rate: f32,
|
|
||||||
target_performance: f32,
|
|
||||||
performance_window: Vec<f32>,
|
|
||||||
current_difficulty: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AdaptiveSchedule {
|
|
||||||
pub fn new(
|
|
||||||
initial_difficulty: f32,
|
|
||||||
final_difficulty: f32,
|
|
||||||
adaptation_rate: f32,
|
|
||||||
target_performance: f32,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_difficulty,
|
|
||||||
final_difficulty,
|
|
||||||
adaptation_rate,
|
|
||||||
target_performance,
|
|
||||||
performance_window: Vec::new(),
|
|
||||||
current_difficulty: initial_difficulty,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_performance(&mut self, performance: f32) {
|
|
||||||
self.performance_window.push(performance);
|
|
||||||
if self.performance_window.len() > 10 {
|
|
||||||
self.performance_window.remove(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_performance(&self) -> f32 {
|
|
||||||
if self.performance_window.is_empty() {
|
|
||||||
self.target_performance
|
|
||||||
} else {
|
|
||||||
self.performance_window.iter().sum::<f32>() / self.performance_window.len() as f32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Schedule for AdaptiveSchedule {
|
|
||||||
fn get_difficulty_at_step(&self, _step: usize) -> f32 {
|
|
||||||
let current_perf = self.current_performance();
|
|
||||||
|
|
||||||
let adjustment = if current_perf > self.target_performance {
|
|
||||||
self.adaptation_rate // Increase difficulty
|
|
||||||
} else {
|
|
||||||
-self.adaptation_rate // Decrease difficulty
|
|
||||||
};
|
|
||||||
|
|
||||||
(self.current_difficulty + adjustment).clamp(self.initial_difficulty, self.final_difficulty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cyclic curriculum schedule
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CyclicSchedule {
|
|
||||||
min_difficulty: f32,
|
|
||||||
max_difficulty: f32,
|
|
||||||
cycle_length: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CyclicSchedule {
|
|
||||||
pub fn new(min_difficulty: f32, max_difficulty: f32, cycle_length: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
min_difficulty,
|
|
||||||
max_difficulty,
|
|
||||||
cycle_length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Schedule for CyclicSchedule {
|
|
||||||
fn get_difficulty_at_step(&self, step: usize) -> f32 {
|
|
||||||
let cycle_position = (step % self.cycle_length) as f32;
|
|
||||||
let normalized_position = cycle_position / self.cycle_length as f32;
|
|
||||||
|
|
||||||
// Triangle wave: goes from min to max and back
|
|
||||||
let triangle_wave = if normalized_position <= 0.5 {
|
|
||||||
normalized_position * 2.0 // 0 -> 1
|
|
||||||
} else {
|
|
||||||
2.0 - normalized_position * 2.0 // 1 -> 0
|
|
||||||
};
|
|
||||||
|
|
||||||
self.min_difficulty + triangle_wave * (self.max_difficulty - self.min_difficulty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multi-task curriculum schedule coordinator
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MultiTaskSchedule {
|
|
||||||
task_schedules: HashMap<String, Box<dyn Schedule>>,
|
|
||||||
coordination_weight: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MultiTaskSchedule {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
task_schedules: HashMap::new(),
|
|
||||||
coordination_weight: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_task(&mut self, task_name: &str, schedule: Box<dyn Schedule>) {
|
|
||||||
self.task_schedules.insert(task_name.to_string(), schedule);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_coordination_weight(&mut self, weight: f32) {
|
|
||||||
self.coordination_weight = weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_task_difficulty(&self, task_name: &str, step: usize) -> Option<f32> {
|
|
||||||
self.task_schedules.get(task_name).map(|schedule| schedule.get_difficulty_at_step(step))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_coordinated_difficulty(&self, task_name: &str, step: usize) -> Option<f32> {
|
|
||||||
let task_difficulty = self.get_task_difficulty(task_name, step)?;
|
|
||||||
|
|
||||||
if self.coordination_weight <= 0.0 {
|
|
||||||
return Some(task_difficulty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate average difficulty across all tasks
|
|
||||||
let total_difficulty: f32 = self.task_schedules
|
|
||||||
.values()
|
|
||||||
.map(|schedule| schedule.get_difficulty_at_step(step))
|
|
||||||
.sum();
|
|
||||||
let avg_difficulty = total_difficulty / self.task_schedules.len() as f32;
|
|
||||||
|
|
||||||
// Blend task-specific and average difficulty
|
|
||||||
Some(task_difficulty * (1.0 - self.coordination_weight) + avg_difficulty * self.coordination_weight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MultiTaskSchedule {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
//! Difficulty scoring implementations.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use super::sample::Sample;
|
|
||||||
|
|
||||||
/// Trait for difficulty scoring functions (object-safe version)
|
|
||||||
pub trait DifficultyScorer: Send + Sync {
|
|
||||||
/// Score a sample's difficulty (0.0 = easy, 1.0 = hard) using complexity features
|
|
||||||
fn score_features(&self, features: &HashMap<String, f32>) -> f32;
|
|
||||||
|
|
||||||
/// Optional: Update scorer based on performance feedback
|
|
||||||
fn update_from_performance(&mut self, _sample_id: usize, _performance: f32) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience method for scoring samples directly
|
|
||||||
pub trait DifficultyExt<S: Sample> {
|
|
||||||
fn score(&self, sample: &S) -> f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: DifficultyScorer, S: Sample> DifficultyExt<S> for T {
|
|
||||||
fn score(&self, sample: &S) -> f32 {
|
|
||||||
let features = sample.complexity_features();
|
|
||||||
self.score_features(&features)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Length-based difficulty scorer - longer sequences are harder
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LengthBasedDifficultyScorer {
|
|
||||||
max_length: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LengthBasedDifficultyScorer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { max_length: 100.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_max_length(max_length: f32) -> Self {
|
|
||||||
Self { max_length }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LengthBasedDifficultyScorer {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DifficultyScorer for LengthBasedDifficultyScorer {
|
|
||||||
fn score_features(&self, features: &HashMap<String, f32>) -> f32 {
|
|
||||||
let length = features.get("sequence_length").copied().unwrap_or(0.0);
|
|
||||||
(length / self.max_length).min(1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Variance-based difficulty scorer - higher variance indicates more complexity
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct VarianceBasedDifficultyScorer {
|
|
||||||
max_variance: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VarianceBasedDifficultyScorer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { max_variance: 10.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_max_variance(max_variance: f32) -> Self {
|
|
||||||
Self { max_variance }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VarianceBasedDifficultyScorer {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DifficultyScorer for VarianceBasedDifficultyScorer {
|
|
||||||
fn score_features(&self, features: &HashMap<String, f32>) -> f32 {
|
|
||||||
let variance = features.get("variance").copied().unwrap_or(0.0);
|
|
||||||
(variance / self.max_variance).min(1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Composite difficulty scorer that combines multiple scorers
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CompositeDifficultyScorer {
|
|
||||||
scorers: Vec<Box<dyn DifficultyScorer>>,
|
|
||||||
weights: Vec<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CompositeDifficultyScorer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
scorers: Vec::new(),
|
|
||||||
weights: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_scorer(&mut self, scorer: Box<dyn DifficultyScorer>, weight: f32) {
|
|
||||||
self.scorers.push(scorer);
|
|
||||||
self.weights.push(weight);
|
|
||||||
self.normalize_weights();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_weights(&self) -> &[f32] {
|
|
||||||
&self.weights
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_weights(&mut self) {
|
|
||||||
let sum: f32 = self.weights.iter().sum();
|
|
||||||
if sum > 0.0 {
|
|
||||||
for weight in &mut self.weights {
|
|
||||||
*weight /= sum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CompositeDifficultyScorer {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DifficultyScorer for CompositeDifficultyScorer {
|
|
||||||
fn score_features(&self, features: &HashMap<String, f32>) -> f32 {
|
|
||||||
if self.scorers.is_empty() {
|
|
||||||
return 0.5; // Default difficulty
|
|
||||||
}
|
|
||||||
|
|
||||||
self.scorers
|
|
||||||
.iter()
|
|
||||||
.zip(&self.weights)
|
|
||||||
.map(|(scorer, &weight)| scorer.score_features(features) * weight)
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Curriculum-based difficulty scorer using performance history
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CurriculumBasedDifficultyScorer {
|
|
||||||
performance_history: HashMap<usize, f32>,
|
|
||||||
default_difficulty: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumBasedDifficultyScorer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
performance_history: HashMap::new(),
|
|
||||||
default_difficulty: 0.5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_performance_history(&mut self, sample_id: usize, performance: f32) {
|
|
||||||
self.performance_history.insert(sample_id, performance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CurriculumBasedDifficultyScorer {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DifficultyScorer for CurriculumBasedDifficultyScorer {
|
|
||||||
fn score_features(&self, features: &HashMap<String, f32>) -> f32 {
|
|
||||||
// For curriculum-based scoring, we need the sample ID from features
|
|
||||||
if let Some(&sample_id) = features.get("sample_id") {
|
|
||||||
self.performance_history
|
|
||||||
.get(&(sample_id as usize))
|
|
||||||
.map(|&perf| 1.0 - perf) // Inverse relationship: low performance = high difficulty
|
|
||||||
.unwrap_or(self.default_difficulty)
|
|
||||||
} else {
|
|
||||||
self.default_difficulty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_from_performance(&mut self, sample_id: usize, performance: f32) {
|
|
||||||
self.performance_history.insert(sample_id, performance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
//! Simple curriculum learning test without external dependencies
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Test sample implementation
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct TestSample {
|
|
||||||
pub id: usize,
|
|
||||||
pub data: Vec<f32>,
|
|
||||||
pub metadata: HashMap<String, f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestSample {
|
|
||||||
pub fn new(id: usize, data: Vec<f32>) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
metadata: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_metadata(mut self, key: &str, value: f32) -> Self {
|
|
||||||
self.metadata.insert(key.to_string(), value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> usize {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn complexity_features(&self) -> HashMap<String, f32> {
|
|
||||||
let mut features = self.metadata.clone();
|
|
||||||
features.insert("sequence_length".to_string(), self.data.len() as f32);
|
|
||||||
features.insert("variance".to_string(),
|
|
||||||
self.data.iter().map(|&x| x * x).sum::<f32>() / self.data.len() as f32);
|
|
||||||
features
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Length-based difficulty scorer
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LengthBasedDifficultyScorer {
|
|
||||||
max_length: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LengthBasedDifficultyScorer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { max_length: 100.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn score(&self, sample: &TestSample) -> f32 {
|
|
||||||
let features = sample.complexity_features();
|
|
||||||
let length = features.get("sequence_length").copied().unwrap_or(0.0);
|
|
||||||
(length / self.max_length).min(1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Curriculum state
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CurriculumState {
|
|
||||||
pub step: usize,
|
|
||||||
pub difficulty_threshold: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
step: 0,
|
|
||||||
difficulty_threshold: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Easy-to-hard strategy
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct EasyToHardStrategy {
|
|
||||||
initial_threshold: f32,
|
|
||||||
threshold_increment: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EasyToHardStrategy {
|
|
||||||
pub fn new(initial_threshold: f32, threshold_increment: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_threshold,
|
|
||||||
threshold_increment,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_samples(
|
|
||||||
&self,
|
|
||||||
samples: &[TestSample],
|
|
||||||
difficulty_scorer: &LengthBasedDifficultyScorer,
|
|
||||||
curriculum_state: &CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<TestSample> {
|
|
||||||
let mut scored_samples: Vec<(TestSample, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score(s)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Filter by current difficulty threshold
|
|
||||||
scored_samples.retain(|(_, score)| *score <= curriculum_state.difficulty_threshold);
|
|
||||||
|
|
||||||
// Sort by difficulty (easiest first)
|
|
||||||
scored_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
|
|
||||||
// If not enough samples, include some harder ones
|
|
||||||
if scored_samples.len() < batch_size {
|
|
||||||
let mut all_samples: Vec<(TestSample, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score(s)))
|
|
||||||
.collect();
|
|
||||||
all_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
scored_samples = all_samples.into_iter().take(batch_size).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
scored_samples
|
|
||||||
.into_iter()
|
|
||||||
.take(batch_size)
|
|
||||||
.map(|(sample, _)| sample)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_difficulty_threshold(&self, curriculum_state: &mut CurriculumState) {
|
|
||||||
curriculum_state.difficulty_threshold =
|
|
||||||
(self.initial_threshold + curriculum_state.step as f32 * self.threshold_increment).min(1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Linear schedule
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LinearSchedule {
|
|
||||||
initial_difficulty: f32,
|
|
||||||
final_difficulty: f32,
|
|
||||||
total_steps: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinearSchedule {
|
|
||||||
pub fn new(initial_difficulty: f32, final_difficulty: f32, total_steps: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_difficulty,
|
|
||||||
final_difficulty,
|
|
||||||
total_steps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_difficulty_at_step(&self, step: usize) -> f32 {
|
|
||||||
if step >= self.total_steps {
|
|
||||||
return self.final_difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
let progress = step as f32 / self.total_steps as f32;
|
|
||||||
self.initial_difficulty + progress * (self.final_difficulty - self.initial_difficulty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println!("🎯 Testing RTX Curriculum Learning Implementation (Simple)");
|
|
||||||
println!("=========================================================");
|
|
||||||
|
|
||||||
// Test 1: Difficulty Scoring
|
|
||||||
println!("\n📊 Test 1: Difficulty Scoring");
|
|
||||||
let scorer = LengthBasedDifficultyScorer::new();
|
|
||||||
|
|
||||||
let easy_sample = TestSample::new(1, vec![1.0, 2.0]);
|
|
||||||
let hard_sample = TestSample::new(2, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
||||||
|
|
||||||
let easy_score = scorer.score(&easy_sample);
|
|
||||||
let hard_score = scorer.score(&hard_sample);
|
|
||||||
|
|
||||||
println!(" Easy sample (len=2): difficulty = {:.3}", easy_score);
|
|
||||||
println!(" Hard sample (len=5): difficulty = {:.3}", hard_score);
|
|
||||||
|
|
||||||
assert!(easy_score < hard_score, "❌ Easy sample should have lower difficulty");
|
|
||||||
assert!(easy_score >= 0.0 && easy_score <= 1.0, "❌ Easy score should be normalized");
|
|
||||||
assert!(hard_score >= 0.0 && hard_score <= 1.0, "❌ Hard score should be normalized");
|
|
||||||
println!(" ✅ Difficulty scoring works correctly");
|
|
||||||
|
|
||||||
// Test 2: Curriculum Strategy
|
|
||||||
println!("\n📈 Test 2: Easy-to-Hard Curriculum Strategy");
|
|
||||||
let strategy = EasyToHardStrategy::new(0.1, 0.05);
|
|
||||||
|
|
||||||
let samples = vec![
|
|
||||||
TestSample::new(1, vec![1.0; 10]), // Hard
|
|
||||||
TestSample::new(2, vec![1.0; 2]), // Easy
|
|
||||||
TestSample::new(3, vec![1.0; 5]), // Medium
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut state = CurriculumState::new();
|
|
||||||
|
|
||||||
// Initially should select mostly easy samples
|
|
||||||
let selected = strategy.select_samples(&samples, &scorer, &state, 2);
|
|
||||||
println!(" Selected {} samples initially", selected.len());
|
|
||||||
|
|
||||||
let selected_lengths: Vec<usize> = selected.iter()
|
|
||||||
.map(|s| s.data.len())
|
|
||||||
.collect();
|
|
||||||
println!(" Selected sample lengths: {:?}", selected_lengths);
|
|
||||||
|
|
||||||
assert_eq!(selected.len(), 2, "❌ Should select requested batch size");
|
|
||||||
assert!(selected_lengths.contains(&2), "❌ Easy sample should be selected initially");
|
|
||||||
println!(" ✅ Initial selection prefers easy samples");
|
|
||||||
|
|
||||||
// Advance curriculum
|
|
||||||
for i in 0..20 {
|
|
||||||
state.step += 1;
|
|
||||||
strategy.update_difficulty_threshold(&mut state);
|
|
||||||
println!(" Step {}: threshold = {:.3}", i + 1, state.difficulty_threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
let selected_later = strategy.select_samples(&samples, &scorer, &state, 2);
|
|
||||||
println!(" Selected {} samples after progression", selected_later.len());
|
|
||||||
assert_eq!(selected_later.len(), 2, "❌ Should still select requested batch size");
|
|
||||||
println!(" ✅ Curriculum progression works");
|
|
||||||
|
|
||||||
// Test 3: Linear Schedule
|
|
||||||
println!("\n⏱️ Test 3: Linear Schedule");
|
|
||||||
let schedule = LinearSchedule::new(0.1, 0.9, 100);
|
|
||||||
|
|
||||||
let step_0 = schedule.get_difficulty_at_step(0);
|
|
||||||
let step_50 = schedule.get_difficulty_at_step(50);
|
|
||||||
let step_100 = schedule.get_difficulty_at_step(100);
|
|
||||||
let step_150 = schedule.get_difficulty_at_step(150);
|
|
||||||
|
|
||||||
println!(" Step 0: difficulty = {:.3}", step_0);
|
|
||||||
println!(" Step 50: difficulty = {:.3}", step_50);
|
|
||||||
println!(" Step 100: difficulty = {:.3}", step_100);
|
|
||||||
println!(" Step 150: difficulty = {:.3}", step_150);
|
|
||||||
|
|
||||||
let epsilon = 1e-6;
|
|
||||||
assert!((step_0 - 0.1).abs() < epsilon, "❌ Step 0 should be initial difficulty");
|
|
||||||
assert!((step_50 - 0.5).abs() < epsilon, "❌ Step 50 should be halfway");
|
|
||||||
assert!((step_100 - 0.9).abs() < epsilon, "❌ Step 100 should be final difficulty");
|
|
||||||
assert!((step_150 - 0.9).abs() < epsilon, "❌ Step 150 should clamp at final difficulty");
|
|
||||||
println!(" ✅ Linear schedule works correctly");
|
|
||||||
|
|
||||||
// Test 4: Integration Test
|
|
||||||
println!("\n🔗 Test 4: Integration Test");
|
|
||||||
println!(" Testing complete curriculum learning pipeline...");
|
|
||||||
|
|
||||||
let curriculum_samples = vec![
|
|
||||||
TestSample::new(1, vec![1.0; 1]), // Very easy
|
|
||||||
TestSample::new(2, vec![1.0; 3]), // Easy
|
|
||||||
TestSample::new(3, vec![1.0; 5]), // Medium
|
|
||||||
TestSample::new(4, vec![1.0; 8]), // Hard
|
|
||||||
TestSample::new(5, vec![1.0; 12]), // Very hard
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut curriculum_state = CurriculumState::new();
|
|
||||||
let curriculum_strategy = EasyToHardStrategy::new(0.1, 0.1);
|
|
||||||
let curriculum_schedule = LinearSchedule::new(0.1, 0.8, 10);
|
|
||||||
|
|
||||||
for step in 0..10 {
|
|
||||||
curriculum_state.step = step;
|
|
||||||
curriculum_state.difficulty_threshold = curriculum_schedule.get_difficulty_at_step(step);
|
|
||||||
curriculum_strategy.update_difficulty_threshold(&mut curriculum_state);
|
|
||||||
|
|
||||||
let batch = curriculum_strategy.select_samples(
|
|
||||||
&curriculum_samples,
|
|
||||||
&scorer,
|
|
||||||
&curriculum_state,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
let avg_length: f32 = batch.iter()
|
|
||||||
.map(|s| s.data.len() as f32)
|
|
||||||
.sum::<f32>() / batch.len() as f32;
|
|
||||||
|
|
||||||
println!(" Step {}: threshold = {:.2}, avg_batch_length = {:.1}",
|
|
||||||
step, curriculum_state.difficulty_threshold, avg_length);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!(" ✅ Complete pipeline integration successful");
|
|
||||||
|
|
||||||
println!("\n🎉 All Curriculum Learning Tests Passed!");
|
|
||||||
println!(" ✅ Difficulty scoring functions work correctly");
|
|
||||||
println!(" ✅ Easy-to-hard strategy implemented properly");
|
|
||||||
println!(" ✅ Linear scheduling functions correctly");
|
|
||||||
println!(" ✅ Complete pipeline integration successful");
|
|
||||||
|
|
||||||
println!("\n📋 Implementation Status:");
|
|
||||||
println!(" 🟢 Core curriculum learning framework - COMPLETE");
|
|
||||||
println!(" 🟢 Difficulty scoring strategies - COMPLETE");
|
|
||||||
println!(" 🟢 Curriculum selection strategies - COMPLETE");
|
|
||||||
println!(" 🟢 Scheduling algorithms - COMPLETE");
|
|
||||||
println!(" 🟢 State management - COMPLETE");
|
|
||||||
|
|
||||||
println!("\n✨ RTX Curriculum Learning implementation follows strict TDD:");
|
|
||||||
println!(" 1. ✅ Tests written first (RED phase)");
|
|
||||||
println!(" 2. ✅ Minimal implementation created (GREEN phase)");
|
|
||||||
println!(" 3. ✅ Ready for refactoring phase");
|
|
||||||
println!(" 4. ✅ No mocks, stubs, or TODOs - production ready!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_difficulty_scoring() {
|
|
||||||
let scorer = LengthBasedDifficultyScorer::new();
|
|
||||||
let easy_sample = TestSample::new(1, vec![1.0, 2.0]);
|
|
||||||
let hard_sample = TestSample::new(2, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
||||||
|
|
||||||
let easy_score = scorer.score(&easy_sample);
|
|
||||||
let hard_score = scorer.score(&hard_sample);
|
|
||||||
|
|
||||||
assert!(easy_score < hard_score);
|
|
||||||
assert!(easy_score >= 0.0 && easy_score <= 1.0);
|
|
||||||
assert!(hard_score >= 0.0 && hard_score <= 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_curriculum_strategy() {
|
|
||||||
let strategy = EasyToHardStrategy::new(0.1, 0.05);
|
|
||||||
let scorer = LengthBasedDifficultyScorer::new();
|
|
||||||
let samples = vec![
|
|
||||||
TestSample::new(1, vec![1.0; 10]), // Hard
|
|
||||||
TestSample::new(2, vec![1.0; 2]), // Easy
|
|
||||||
TestSample::new(3, vec![1.0; 5]), // Medium
|
|
||||||
];
|
|
||||||
|
|
||||||
let state = CurriculumState::new();
|
|
||||||
let selected = strategy.select_samples(&samples, &scorer, &state, 2);
|
|
||||||
assert_eq!(selected.len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_linear_schedule() {
|
|
||||||
let schedule = LinearSchedule::new(0.1, 0.9, 100);
|
|
||||||
|
|
||||||
assert!((schedule.get_difficulty_at_step(0) - 0.1).abs() < 1e-6);
|
|
||||||
assert!((schedule.get_difficulty_at_step(50) - 0.5).abs() < 1e-6);
|
|
||||||
assert!((schedule.get_difficulty_at_step(100) - 0.9).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,385 +0,0 @@
|
|||||||
//! Standalone test for curriculum learning implementation
|
|
||||||
//! This test runs independently without depending on the full RTX ecosystem
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
// Mock tensor types for testing
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MockDevice;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MockTensor {
|
|
||||||
pub data: Vec<f32>,
|
|
||||||
pub shape: Vec<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MockTensor {
|
|
||||||
pub fn new(data: Vec<f32>, shape: Vec<usize>) -> Self {
|
|
||||||
Self { data, shape }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Minimal error type for testing
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum MockError {
|
|
||||||
#[error("Test error: {0}")]
|
|
||||||
TestError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, MockError>;
|
|
||||||
|
|
||||||
/// Sample trait implementation
|
|
||||||
pub trait Sample: Clone {
|
|
||||||
fn id(&self) -> usize;
|
|
||||||
fn complexity_features(&self) -> HashMap<String, f32>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test sample implementation
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct TestSample {
|
|
||||||
pub id: usize,
|
|
||||||
pub data: Vec<f32>,
|
|
||||||
pub label: Option<usize>,
|
|
||||||
pub metadata: HashMap<String, f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestSample {
|
|
||||||
pub fn new(id: usize, data: Vec<f32>, label: Option<usize>) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
label,
|
|
||||||
metadata: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_metadata(mut self, key: &str, value: f32) -> Self {
|
|
||||||
self.metadata.insert(key.to_string(), value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sample for TestSample {
|
|
||||||
fn id(&self) -> usize {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complexity_features(&self) -> HashMap<String, f32> {
|
|
||||||
let mut features = self.metadata.clone();
|
|
||||||
features.insert("sequence_length".to_string(), self.data.len() as f32);
|
|
||||||
features.insert("variance".to_string(),
|
|
||||||
self.data.iter().map(|&x| x * x).sum::<f32>() / self.data.len() as f32);
|
|
||||||
features
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Difficulty scorer trait
|
|
||||||
pub trait DifficultyScorer: Send + Sync {
|
|
||||||
fn score<S: Sample>(&self, sample: &S) -> f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Length-based difficulty scorer
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LengthBasedDifficultyScorer {
|
|
||||||
max_length: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LengthBasedDifficultyScorer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { max_length: 100.0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DifficultyScorer for LengthBasedDifficultyScorer {
|
|
||||||
fn score<S: Sample>(&self, sample: &S) -> f32 {
|
|
||||||
let features = sample.complexity_features();
|
|
||||||
let length = features.get("sequence_length").copied().unwrap_or(0.0);
|
|
||||||
(length / self.max_length).min(1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Curriculum state
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CurriculumState {
|
|
||||||
pub step: usize,
|
|
||||||
pub difficulty_threshold: f32,
|
|
||||||
pub performance_history: Vec<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
step: 0,
|
|
||||||
difficulty_threshold: 0.0,
|
|
||||||
performance_history: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Curriculum strategy trait
|
|
||||||
pub trait CurriculumStrategy: Send + Sync {
|
|
||||||
fn select_samples<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scorer: &dyn DifficultyScorer,
|
|
||||||
curriculum_state: &mut CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<S>;
|
|
||||||
|
|
||||||
fn update_difficulty_threshold(&self, curriculum_state: &mut CurriculumState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Easy-to-hard strategy
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct EasyToHardStrategy {
|
|
||||||
initial_threshold: f32,
|
|
||||||
threshold_increment: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EasyToHardStrategy {
|
|
||||||
pub fn new(initial_threshold: f32, threshold_increment: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_threshold,
|
|
||||||
threshold_increment,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumStrategy for EasyToHardStrategy {
|
|
||||||
fn select_samples<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scorer: &dyn DifficultyScorer,
|
|
||||||
curriculum_state: &mut CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<S> {
|
|
||||||
let mut scored_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score(s)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Filter by current difficulty threshold
|
|
||||||
scored_samples.retain(|(_, score)| *score <= curriculum_state.difficulty_threshold);
|
|
||||||
|
|
||||||
// Sort by difficulty (easiest first)
|
|
||||||
scored_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
|
|
||||||
// If not enough samples, include some harder ones
|
|
||||||
if scored_samples.len() < batch_size {
|
|
||||||
let mut all_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score(s)))
|
|
||||||
.collect();
|
|
||||||
all_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
scored_samples = all_samples.into_iter().take(batch_size).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
scored_samples
|
|
||||||
.into_iter()
|
|
||||||
.take(batch_size)
|
|
||||||
.map(|(sample, _)| sample)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_difficulty_threshold(&self, curriculum_state: &mut CurriculumState) {
|
|
||||||
curriculum_state.difficulty_threshold =
|
|
||||||
(self.initial_threshold + curriculum_state.step as f32 * self.threshold_increment).min(1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Schedule trait
|
|
||||||
pub trait Schedule: Send + Sync {
|
|
||||||
fn get_difficulty_at_step(&self, step: usize) -> f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Linear schedule
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LinearSchedule {
|
|
||||||
initial_difficulty: f32,
|
|
||||||
final_difficulty: f32,
|
|
||||||
total_steps: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinearSchedule {
|
|
||||||
pub fn new(initial_difficulty: f32, final_difficulty: f32, total_steps: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_difficulty,
|
|
||||||
final_difficulty,
|
|
||||||
total_steps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Schedule for LinearSchedule {
|
|
||||||
fn get_difficulty_at_step(&self, step: usize) -> f32 {
|
|
||||||
if step >= self.total_steps {
|
|
||||||
return self.final_difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
let progress = step as f32 / self.total_steps as f32;
|
|
||||||
self.initial_difficulty + progress * (self.final_difficulty - self.initial_difficulty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
println!("🎯 Testing RTX Curriculum Learning Implementation");
|
|
||||||
println!("================================================");
|
|
||||||
|
|
||||||
// Test 1: Difficulty Scoring
|
|
||||||
println!("\n📊 Test 1: Difficulty Scoring");
|
|
||||||
let scorer = LengthBasedDifficultyScorer::new();
|
|
||||||
|
|
||||||
let easy_sample = TestSample::new(1, vec![1.0, 2.0], None);
|
|
||||||
let hard_sample = TestSample::new(2, vec![1.0, 2.0, 3.0, 4.0, 5.0], None);
|
|
||||||
|
|
||||||
let easy_score = scorer.score(&easy_sample);
|
|
||||||
let hard_score = scorer.score(&hard_sample);
|
|
||||||
|
|
||||||
println!(" Easy sample (len=2): difficulty = {:.3}", easy_score);
|
|
||||||
println!(" Hard sample (len=5): difficulty = {:.3}", hard_score);
|
|
||||||
|
|
||||||
assert!(easy_score < hard_score, "❌ Easy sample should have lower difficulty");
|
|
||||||
assert!(easy_score >= 0.0 && easy_score <= 1.0, "❌ Easy score should be normalized");
|
|
||||||
assert!(hard_score >= 0.0 && hard_score <= 1.0, "❌ Hard score should be normalized");
|
|
||||||
println!(" ✅ Difficulty scoring works correctly");
|
|
||||||
|
|
||||||
// Test 2: Curriculum Strategy
|
|
||||||
println!("\n📈 Test 2: Easy-to-Hard Curriculum Strategy");
|
|
||||||
let strategy = EasyToHardStrategy::new(0.1, 0.05);
|
|
||||||
|
|
||||||
let samples = vec![
|
|
||||||
TestSample::new(1, vec![1.0; 10], None), // Hard
|
|
||||||
TestSample::new(2, vec![1.0; 2], None), // Easy
|
|
||||||
TestSample::new(3, vec![1.0; 5], None), // Medium
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut state = CurriculumState::new();
|
|
||||||
|
|
||||||
// Initially should select mostly easy samples
|
|
||||||
let selected = strategy.select_samples(&samples, &scorer, &mut state, 2);
|
|
||||||
println!(" Selected {} samples initially", selected.len());
|
|
||||||
|
|
||||||
let selected_lengths: Vec<usize> = selected.iter()
|
|
||||||
.map(|s| s.data.len())
|
|
||||||
.collect();
|
|
||||||
println!(" Selected sample lengths: {:?}", selected_lengths);
|
|
||||||
|
|
||||||
assert_eq!(selected.len(), 2, "❌ Should select requested batch size");
|
|
||||||
assert!(selected_lengths.contains(&2), "❌ Easy sample should be selected initially");
|
|
||||||
println!(" ✅ Initial selection prefers easy samples");
|
|
||||||
|
|
||||||
// Advance curriculum
|
|
||||||
for i in 0..20 {
|
|
||||||
state.step += 1;
|
|
||||||
strategy.update_difficulty_threshold(&mut state);
|
|
||||||
println!(" Step {}: threshold = {:.3}", i + 1, state.difficulty_threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
let selected_later = strategy.select_samples(&samples, &scorer, &mut state, 2);
|
|
||||||
println!(" Selected {} samples after progression", selected_later.len());
|
|
||||||
assert_eq!(selected_later.len(), 2, "❌ Should still select requested batch size");
|
|
||||||
println!(" ✅ Curriculum progression works");
|
|
||||||
|
|
||||||
// Test 3: Linear Schedule
|
|
||||||
println!("\n⏱️ Test 3: Linear Schedule");
|
|
||||||
let schedule = LinearSchedule::new(0.1, 0.9, 100);
|
|
||||||
|
|
||||||
let step_0 = schedule.get_difficulty_at_step(0);
|
|
||||||
let step_50 = schedule.get_difficulty_at_step(50);
|
|
||||||
let step_100 = schedule.get_difficulty_at_step(100);
|
|
||||||
let step_150 = schedule.get_difficulty_at_step(150);
|
|
||||||
|
|
||||||
println!(" Step 0: difficulty = {:.3}", step_0);
|
|
||||||
println!(" Step 50: difficulty = {:.3}", step_50);
|
|
||||||
println!(" Step 100: difficulty = {:.3}", step_100);
|
|
||||||
println!(" Step 150: difficulty = {:.3}", step_150);
|
|
||||||
|
|
||||||
assert!((step_0 - 0.1).abs() < 1e-6, "❌ Step 0 should be initial difficulty");
|
|
||||||
assert!((step_50 - 0.5).abs() < 1e-6, "❌ Step 50 should be halfway");
|
|
||||||
assert!((step_100 - 0.9).abs() < 1e-6, "❌ Step 100 should be final difficulty");
|
|
||||||
assert!((step_150 - 0.9).abs() < 1e-6, "❌ Step 150 should clamp at final difficulty");
|
|
||||||
println!(" ✅ Linear schedule works correctly");
|
|
||||||
|
|
||||||
// Test 4: Curriculum State Serialization
|
|
||||||
println!("\n💾 Test 4: Curriculum State Serialization");
|
|
||||||
let mut test_state = CurriculumState::new();
|
|
||||||
test_state.step = 42;
|
|
||||||
test_state.difficulty_threshold = 0.75;
|
|
||||||
test_state.performance_history = vec![0.8, 0.85, 0.9];
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&test_state).unwrap();
|
|
||||||
let deserialized: CurriculumState = serde_json::from_str(&serialized).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(deserialized.step, 42, "❌ Step should match");
|
|
||||||
assert!((deserialized.difficulty_threshold - 0.75).abs() < 1e-6, "❌ Threshold should match");
|
|
||||||
assert_eq!(deserialized.performance_history.len(), 3, "❌ Performance history should match");
|
|
||||||
println!(" ✅ State serialization works correctly");
|
|
||||||
|
|
||||||
// Test 5: Integration Test
|
|
||||||
println!("\n🔗 Test 5: Integration Test");
|
|
||||||
println!(" Testing complete curriculum learning pipeline...");
|
|
||||||
|
|
||||||
let curriculum_samples = vec![
|
|
||||||
TestSample::new(1, vec![1.0; 1], None), // Very easy
|
|
||||||
TestSample::new(2, vec![1.0; 3], None), // Easy
|
|
||||||
TestSample::new(3, vec![1.0; 5], None), // Medium
|
|
||||||
TestSample::new(4, vec![1.0; 8], None), // Hard
|
|
||||||
TestSample::new(5, vec![1.0; 12], None), // Very hard
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut curriculum_state = CurriculumState::new();
|
|
||||||
let curriculum_strategy = EasyToHardStrategy::new(0.1, 0.1);
|
|
||||||
let curriculum_schedule = LinearSchedule::new(0.1, 0.8, 10);
|
|
||||||
|
|
||||||
for step in 0..10 {
|
|
||||||
curriculum_state.step = step;
|
|
||||||
curriculum_state.difficulty_threshold = curriculum_schedule.get_difficulty_at_step(step);
|
|
||||||
curriculum_strategy.update_difficulty_threshold(&mut curriculum_state);
|
|
||||||
|
|
||||||
let batch = curriculum_strategy.select_samples(
|
|
||||||
&curriculum_samples,
|
|
||||||
&scorer,
|
|
||||||
&mut curriculum_state,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
let avg_length: f32 = batch.iter()
|
|
||||||
.map(|s| s.data.len() as f32)
|
|
||||||
.sum::<f32>() / batch.len() as f32;
|
|
||||||
|
|
||||||
println!(" Step {}: threshold = {:.2}, avg_batch_length = {:.1}",
|
|
||||||
step, curriculum_state.difficulty_threshold, avg_length);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!(" ✅ Complete pipeline integration successful");
|
|
||||||
|
|
||||||
println!("\n🎉 All Curriculum Learning Tests Passed!");
|
|
||||||
println!(" ✅ Difficulty scoring functions work correctly");
|
|
||||||
println!(" ✅ Easy-to-hard strategy implemented properly");
|
|
||||||
println!(" ✅ Linear scheduling functions correctly");
|
|
||||||
println!(" ✅ State serialization/deserialization works");
|
|
||||||
println!(" ✅ Complete pipeline integration successful");
|
|
||||||
|
|
||||||
println!("\n📋 Implementation Status:");
|
|
||||||
println!(" 🟢 Core curriculum learning framework - COMPLETE");
|
|
||||||
println!(" 🟢 Difficulty scoring strategies - COMPLETE");
|
|
||||||
println!(" 🟢 Curriculum selection strategies - COMPLETE");
|
|
||||||
println!(" 🟢 Scheduling algorithms - COMPLETE");
|
|
||||||
println!(" 🟢 Performance tracking foundation - COMPLETE");
|
|
||||||
println!(" 🟢 State management and serialization - COMPLETE");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_curriculum_learning_basic_functionality() {
|
|
||||||
// This ensures our curriculum learning implementation works
|
|
||||||
let result = main();
|
|
||||||
assert!(result.is_ok(), "Curriculum learning tests should pass");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//! Curriculum learning state.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// State of curriculum learning process
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CurriculumState {
|
|
||||||
pub step: usize,
|
|
||||||
pub difficulty_threshold: f32,
|
|
||||||
pub performance_history: Vec<f32>,
|
|
||||||
pub metadata: HashMap<String, f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
step: 0,
|
|
||||||
difficulty_threshold: 0.0,
|
|
||||||
performance_history: Vec::new(),
|
|
||||||
metadata: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CurriculumState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
//! Curriculum strategy implementations.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use super::sample::Sample;
|
|
||||||
use super::scorers::DifficultyScorer;
|
|
||||||
use super::state::CurriculumState;
|
|
||||||
|
|
||||||
/// Trait for curriculum strategies
|
|
||||||
pub trait CurriculumStrategy: Send + Sync {
|
|
||||||
/// Select samples based on curriculum strategy
|
|
||||||
fn select_samples<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scorer: &dyn DifficultyScorer,
|
|
||||||
curriculum_state: &mut CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<S>;
|
|
||||||
|
|
||||||
/// Update difficulty threshold based on strategy
|
|
||||||
fn update_difficulty_threshold(&self, curriculum_state: &mut CurriculumState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Easy-to-hard curriculum strategy (Bengio et al.)
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct EasyToHardStrategy {
|
|
||||||
initial_threshold: f32,
|
|
||||||
threshold_increment: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EasyToHardStrategy {
|
|
||||||
pub fn new(initial_threshold: f32, threshold_increment: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_threshold,
|
|
||||||
threshold_increment,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumStrategy for EasyToHardStrategy {
|
|
||||||
fn select_samples<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scorer: &dyn DifficultyScorer,
|
|
||||||
curriculum_state: &mut CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<S> {
|
|
||||||
let mut scored_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score_features(&s.complexity_features())))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Filter by current difficulty threshold
|
|
||||||
scored_samples.retain(|(_, score)| *score <= curriculum_state.difficulty_threshold);
|
|
||||||
|
|
||||||
// If not enough samples, include some harder ones
|
|
||||||
if scored_samples.len() < batch_size {
|
|
||||||
let mut all_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score_features(&s.complexity_features())))
|
|
||||||
.collect();
|
|
||||||
all_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
scored_samples = all_samples.into_iter().take(batch_size).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select batch_size samples, preferring easier ones
|
|
||||||
scored_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
scored_samples
|
|
||||||
.into_iter()
|
|
||||||
.take(batch_size)
|
|
||||||
.map(|(sample, _)| sample)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_difficulty_threshold(&self, curriculum_state: &mut CurriculumState) {
|
|
||||||
curriculum_state.difficulty_threshold =
|
|
||||||
(self.initial_threshold + curriculum_state.step as f32 * self.threshold_increment).min(1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Anti-curriculum strategy (start with hard examples)
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AntiCurriculumStrategy {
|
|
||||||
initial_threshold: f32,
|
|
||||||
threshold_decrement: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AntiCurriculumStrategy {
|
|
||||||
pub fn new(initial_threshold: f32, threshold_decrement: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_threshold,
|
|
||||||
threshold_decrement,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumStrategy for AntiCurriculumStrategy {
|
|
||||||
fn select_samples<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scorer: &dyn DifficultyScorer,
|
|
||||||
curriculum_state: &mut CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<S> {
|
|
||||||
let mut scored_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score_features(&s.complexity_features())))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Filter by current difficulty threshold (but prefer harder samples)
|
|
||||||
scored_samples.retain(|(_, score)| *score >= curriculum_state.difficulty_threshold);
|
|
||||||
|
|
||||||
// Sort by difficulty (hardest first)
|
|
||||||
scored_samples.sort_by(|a, b| b.1.total_cmp(&a.1));
|
|
||||||
|
|
||||||
// If not enough samples, include easier ones
|
|
||||||
if scored_samples.len() < batch_size {
|
|
||||||
let mut all_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score_features(&s.complexity_features())))
|
|
||||||
.collect();
|
|
||||||
all_samples.sort_by(|a, b| b.1.total_cmp(&a.1));
|
|
||||||
scored_samples = all_samples.into_iter().take(batch_size).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
scored_samples
|
|
||||||
.into_iter()
|
|
||||||
.take(batch_size)
|
|
||||||
.map(|(sample, _)| sample)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_difficulty_threshold(&self, curriculum_state: &mut CurriculumState) {
|
|
||||||
curriculum_state.difficulty_threshold =
|
|
||||||
(self.initial_threshold - curriculum_state.step as f32 * self.threshold_decrement).max(0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Self-paced learning strategy (adjusts based on performance)
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SelfPacedStrategy {
|
|
||||||
initial_threshold: f32,
|
|
||||||
target_performance: f32,
|
|
||||||
performance_window_size: usize,
|
|
||||||
performance_window: Vec<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SelfPacedStrategy {
|
|
||||||
pub fn new(initial_threshold: f32, target_performance: f32, performance_window_size: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
initial_threshold,
|
|
||||||
target_performance,
|
|
||||||
performance_window_size,
|
|
||||||
performance_window: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_performance(&mut self, performance: f32) {
|
|
||||||
self.performance_window.push(performance);
|
|
||||||
if self.performance_window.len() > self.performance_window_size {
|
|
||||||
self.performance_window.remove(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_performance_window(&self) -> &[f32] {
|
|
||||||
&self.performance_window
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_performance(&self) -> f32 {
|
|
||||||
if self.performance_window.is_empty() {
|
|
||||||
self.target_performance
|
|
||||||
} else {
|
|
||||||
self.performance_window.iter().sum::<f32>() / self.performance_window.len() as f32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumStrategy for SelfPacedStrategy {
|
|
||||||
fn select_samples<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scorer: &dyn DifficultyScorer,
|
|
||||||
curriculum_state: &mut CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<S> {
|
|
||||||
let mut scored_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score_features(&s.complexity_features())))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Adjust threshold based on current performance
|
|
||||||
let current_perf = self.current_performance();
|
|
||||||
let adaptive_threshold = if current_perf > self.target_performance {
|
|
||||||
curriculum_state.difficulty_threshold * 1.1 // Increase difficulty
|
|
||||||
} else {
|
|
||||||
curriculum_state.difficulty_threshold * 0.9 // Decrease difficulty
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter by adaptive threshold
|
|
||||||
scored_samples.retain(|(_, score)| *score <= adaptive_threshold);
|
|
||||||
|
|
||||||
// Sort by difficulty
|
|
||||||
scored_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
|
|
||||||
// If not enough samples, take from all available
|
|
||||||
if scored_samples.len() < batch_size {
|
|
||||||
let mut all_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.clone(), difficulty_scorer.score_features(&s.complexity_features())))
|
|
||||||
.collect();
|
|
||||||
all_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
scored_samples = all_samples.into_iter().take(batch_size).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
scored_samples
|
|
||||||
.into_iter()
|
|
||||||
.take(batch_size)
|
|
||||||
.map(|(sample, _)| sample)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_difficulty_threshold(&self, curriculum_state: &mut CurriculumState) {
|
|
||||||
let current_perf = self.current_performance();
|
|
||||||
let adjustment = if current_perf > self.target_performance {
|
|
||||||
0.01 // Gradually increase
|
|
||||||
} else {
|
|
||||||
-0.01 // Gradually decrease
|
|
||||||
};
|
|
||||||
|
|
||||||
curriculum_state.difficulty_threshold =
|
|
||||||
(curriculum_state.difficulty_threshold + adjustment).clamp(0.0, 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Competency-based curriculum strategy
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CompetencyBasedStrategy {
|
|
||||||
competencies: HashMap<String, f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CompetencyBasedStrategy {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
competencies: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_competency(&mut self, competency: &str, level: f32) {
|
|
||||||
self.competencies.insert(competency.to_string(), level);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_competency(&mut self, competency: &str, level: f32) {
|
|
||||||
self.competencies.insert(competency.to_string(), level);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_competencies(&self) -> &HashMap<String, f32> {
|
|
||||||
&self.competencies
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CompetencyBasedStrategy {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumStrategy for CompetencyBasedStrategy {
|
|
||||||
fn select_samples<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
_difficulty_scorer: &dyn DifficultyScorer,
|
|
||||||
_curriculum_state: &mut CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<S> {
|
|
||||||
let mut suitable_samples = Vec::new();
|
|
||||||
|
|
||||||
for sample in samples {
|
|
||||||
let features = sample.complexity_features();
|
|
||||||
let mut is_suitable = true;
|
|
||||||
|
|
||||||
// Check if sample matches current competency levels
|
|
||||||
for (competency, &required_level) in &features {
|
|
||||||
if competency.ends_with("_difficulty") {
|
|
||||||
let competency_name = competency.strip_suffix("_difficulty").unwrap_or(competency);
|
|
||||||
if let Some(¤t_level) = self.competencies.get(competency_name) {
|
|
||||||
if required_level > current_level * 1.2 { // Allow 20% buffer
|
|
||||||
is_suitable = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_suitable {
|
|
||||||
suitable_samples.push(sample.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if suitable_samples.len() >= batch_size {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not enough suitable samples, take any available
|
|
||||||
while suitable_samples.len() < batch_size && suitable_samples.len() < samples.len() {
|
|
||||||
for sample in samples {
|
|
||||||
if !suitable_samples.iter().any(|s| s.id() == sample.id()) {
|
|
||||||
suitable_samples.push(sample.clone());
|
|
||||||
if suitable_samples.len() >= batch_size {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
suitable_samples
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_difficulty_threshold(&self, _curriculum_state: &mut CurriculumState) {
|
|
||||||
// Competency-based doesn't use a single difficulty threshold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Data-driven curriculum discovery strategy
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DataDrivenDiscoveryStrategy {
|
|
||||||
cluster_count: usize,
|
|
||||||
learning_rate: f32,
|
|
||||||
performance_data: HashMap<usize, f32>,
|
|
||||||
discovered_patterns: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataDrivenDiscoveryStrategy {
|
|
||||||
pub fn new(cluster_count: usize, learning_rate: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
cluster_count,
|
|
||||||
learning_rate,
|
|
||||||
performance_data: HashMap::new(),
|
|
||||||
discovered_patterns: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_performance_data(&mut self, sample_id: usize, performance: f32) {
|
|
||||||
self.performance_data.insert(sample_id, performance);
|
|
||||||
self.discover_patterns();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_discovered_patterns(&self) -> &[String] {
|
|
||||||
&self.discovered_patterns
|
|
||||||
}
|
|
||||||
|
|
||||||
fn discover_patterns(&mut self) {
|
|
||||||
// Simple pattern discovery: find performance correlations
|
|
||||||
if self.performance_data.len() >= self.cluster_count {
|
|
||||||
let avg_performance: f32 = self.performance_data.values().sum::<f32>()
|
|
||||||
/ self.performance_data.len() as f32;
|
|
||||||
|
|
||||||
let pattern = if avg_performance > 0.8 {
|
|
||||||
"high_performance_cluster"
|
|
||||||
} else if avg_performance > 0.5 {
|
|
||||||
"medium_performance_cluster"
|
|
||||||
} else {
|
|
||||||
"low_performance_cluster"
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self.discovered_patterns.contains(&pattern.to_string()) {
|
|
||||||
self.discovered_patterns.push(pattern.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurriculumStrategy for DataDrivenDiscoveryStrategy {
|
|
||||||
fn select_samples<S: Sample>(
|
|
||||||
&self,
|
|
||||||
samples: &[S],
|
|
||||||
difficulty_scorer: &dyn DifficultyScorer,
|
|
||||||
_curriculum_state: &mut CurriculumState,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<S> {
|
|
||||||
// Use discovered patterns to guide selection
|
|
||||||
let mut scored_samples: Vec<(S, f32)> = samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| {
|
|
||||||
let base_score = difficulty_scorer.score_features(&s.complexity_features());
|
|
||||||
let performance_adjustment = self.performance_data
|
|
||||||
.get(&s.id())
|
|
||||||
.map(|&perf| 1.0 - perf)
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
(s.clone(), base_score + performance_adjustment * 0.1)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Sort by adjusted score
|
|
||||||
scored_samples.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
||||||
|
|
||||||
scored_samples
|
|
||||||
.into_iter()
|
|
||||||
.take(batch_size)
|
|
||||||
.map(|(sample, _)| sample)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_difficulty_threshold(&self, curriculum_state: &mut CurriculumState) {
|
|
||||||
// Use discovered patterns to adjust threshold
|
|
||||||
let avg_performance = if !self.performance_data.is_empty() {
|
|
||||||
self.performance_data.values().sum::<f32>() / self.performance_data.len() as f32
|
|
||||||
} else {
|
|
||||||
0.5
|
|
||||||
};
|
|
||||||
|
|
||||||
curriculum_state.difficulty_threshold = avg_performance * self.learning_rate +
|
|
||||||
curriculum_state.difficulty_threshold * (1.0 - self.learning_rate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
//! Performance tracking for curriculum learning.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Performance tracking for curriculum learning
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PerformanceTracker {
|
|
||||||
sample_performance: HashMap<usize, f32>,
|
|
||||||
overall_performance_window: Vec<f32>,
|
|
||||||
window_size: usize,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
smoothing_factor: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PerformanceTracker {
|
|
||||||
pub fn new(window_size: usize, smoothing_factor: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
sample_performance: HashMap::new(),
|
|
||||||
overall_performance_window: Vec::new(),
|
|
||||||
window_size,
|
|
||||||
smoothing_factor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_sample_performance(&mut self, sample_id: usize, performance: f32) {
|
|
||||||
self.sample_performance.insert(sample_id, performance);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_overall_performance(&mut self, performance: f32) {
|
|
||||||
self.overall_performance_window.push(performance);
|
|
||||||
if self.overall_performance_window.len() > self.window_size {
|
|
||||||
self.overall_performance_window.remove(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_sample_performance(&self, sample_id: usize) -> Option<f32> {
|
|
||||||
self.sample_performance.get(&sample_id).copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_overall_performance(&self) -> f32 {
|
|
||||||
if self.overall_performance_window.is_empty() {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
self.overall_performance_window.iter().sum::<f32>() / self.overall_performance_window.len() as f32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_performance_window(&self) -> &[f32] {
|
|
||||||
&self.overall_performance_window
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_performance_trend(&self) -> f32 {
|
|
||||||
if self.overall_performance_window.len() < 2 {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let recent_half = &self.overall_performance_window[self.overall_performance_window.len()/2..];
|
|
||||||
let early_half = &self.overall_performance_window[..self.overall_performance_window.len()/2];
|
|
||||||
|
|
||||||
let recent_avg = recent_half.iter().sum::<f32>() / recent_half.len() as f32;
|
|
||||||
let early_avg = early_half.iter().sum::<f32>() / early_half.len() as f32;
|
|
||||||
|
|
||||||
recent_avg - early_avg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
//! Error types for the RTX Transformers crate
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
/// Result type alias for transformer operations
|
|
||||||
pub type Result<T> = std::result::Result<T, TransformerError>;
|
|
||||||
|
|
||||||
/// Comprehensive error types for transformer operations
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum TransformerError {
|
|
||||||
/// Tensor operation errors
|
|
||||||
#[error("Tensor operation failed: {0}")]
|
|
||||||
TensorError(#[from] rtx_tensor::TensorError),
|
|
||||||
|
|
||||||
/// Autograd computation errors
|
|
||||||
#[error("Gradient computation failed: {0}")]
|
|
||||||
AutogradError(#[from] rtx_autograd::AutogradError),
|
|
||||||
|
|
||||||
/// Runtime/CUDA errors
|
|
||||||
#[error("Runtime error: {0}")]
|
|
||||||
RuntimeError(#[from] rtx_runtime::RuntimeError),
|
|
||||||
|
|
||||||
/// Optimizer configuration errors
|
|
||||||
#[error("Optimizer configuration error: {message}")]
|
|
||||||
OptimizerError { message: String },
|
|
||||||
|
|
||||||
/// Learning rate scheduler errors
|
|
||||||
#[error("Learning rate scheduler error: {message}")]
|
|
||||||
SchedulerError { message: String },
|
|
||||||
|
|
||||||
/// Layer configuration errors
|
|
||||||
#[error("Layer configuration error: {message}")]
|
|
||||||
LayerError { message: String },
|
|
||||||
|
|
||||||
/// Architecture configuration errors
|
|
||||||
#[error("Architecture configuration error: {message}")]
|
|
||||||
ArchitectureError { message: String },
|
|
||||||
|
|
||||||
/// Training configuration errors
|
|
||||||
#[error("Training configuration error: {message}")]
|
|
||||||
TrainingError { message: String },
|
|
||||||
|
|
||||||
/// Tokenization errors
|
|
||||||
#[error("Tokenization error: {message}")]
|
|
||||||
TokenizationError { message: String },
|
|
||||||
|
|
||||||
/// Revolutionary integration errors
|
|
||||||
#[error("Revolutionary integration error: {message}")]
|
|
||||||
RevolutionaryError { message: String },
|
|
||||||
|
|
||||||
/// Invalid parameter errors
|
|
||||||
#[error("Invalid parameter: {parameter} = {value}, reason: {reason}")]
|
|
||||||
InvalidParameter {
|
|
||||||
parameter: String,
|
|
||||||
value: String,
|
|
||||||
reason: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Shape mismatch errors
|
|
||||||
#[error("Shape mismatch: expected {expected:?}, got {actual:?}")]
|
|
||||||
ShapeMismatch {
|
|
||||||
expected: Vec<usize>,
|
|
||||||
actual: Vec<usize>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Dimension errors
|
|
||||||
#[error("Dimension error: {message}")]
|
|
||||||
DimensionError { message: String },
|
|
||||||
|
|
||||||
/// Memory allocation errors
|
|
||||||
#[error("Memory allocation error: {message}")]
|
|
||||||
MemoryError { message: String },
|
|
||||||
|
|
||||||
/// Convergence errors
|
|
||||||
#[error("Convergence error: {message}")]
|
|
||||||
ConvergenceError { message: String },
|
|
||||||
|
|
||||||
/// I/O errors
|
|
||||||
#[error("I/O error: {0}")]
|
|
||||||
IoError(#[from] std::io::Error),
|
|
||||||
|
|
||||||
/// Serialization errors
|
|
||||||
#[error("Serialization error: {0}")]
|
|
||||||
SerializationError(#[from] serde_json::Error),
|
|
||||||
|
|
||||||
/// Generic errors
|
|
||||||
#[error("Generic error: {0}")]
|
|
||||||
Generic(#[from] anyhow::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransformerError {
|
|
||||||
/// Create a new optimizer error
|
|
||||||
pub fn optimizer<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::OptimizerError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new scheduler error
|
|
||||||
pub fn scheduler<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::SchedulerError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new layer error
|
|
||||||
pub fn layer<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::LayerError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new architecture error
|
|
||||||
pub fn architecture<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::ArchitectureError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new training error
|
|
||||||
pub fn training<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::TrainingError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new tokenization error
|
|
||||||
pub fn tokenization<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::TokenizationError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new revolutionary integration error
|
|
||||||
pub fn revolutionary<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::RevolutionaryError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new invalid parameter error
|
|
||||||
pub fn invalid_parameter<S: Into<String>>(parameter: S, value: S, reason: S) -> Self {
|
|
||||||
Self::InvalidParameter {
|
|
||||||
parameter: parameter.into(),
|
|
||||||
value: value.into(),
|
|
||||||
reason: reason.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new shape mismatch error
|
|
||||||
pub fn shape_mismatch(expected: Vec<usize>, actual: Vec<usize>) -> Self {
|
|
||||||
Self::ShapeMismatch { expected, actual }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new dimension error
|
|
||||||
pub fn dimension<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::DimensionError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new memory error
|
|
||||||
pub fn memory<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::MemoryError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new convergence error
|
|
||||||
pub fn convergence<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::ConvergenceError {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//! Error types for the RTX Transformers crate (Minimal version)
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
/// Result type alias for transformer operations
|
|
||||||
pub type Result<T> = std::result::Result<T, TransformerError>;
|
|
||||||
|
|
||||||
/// Minimal error types for transformer operations
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum TransformerError {
|
|
||||||
/// Generic errors
|
|
||||||
#[error("Transformer error: {0}")]
|
|
||||||
Generic(String),
|
|
||||||
|
|
||||||
/// I/O errors
|
|
||||||
#[error("I/O error: {0}")]
|
|
||||||
IoError(#[from] std::io::Error),
|
|
||||||
|
|
||||||
/// Anyhow errors
|
|
||||||
#[error("Error: {0}")]
|
|
||||||
Anyhow(#[from] anyhow::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransformerError {
|
|
||||||
/// Create a generic error
|
|
||||||
pub fn generic<S: Into<String>>(message: S) -> Self {
|
|
||||||
Self::Generic(message.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
//! Graph Attention implementation for attention mechanisms on graph-structured data
|
|
||||||
//!
|
|
||||||
//! Provides multiple graph attention mechanisms:
|
|
||||||
//! - Standard Graph Attention (GAT)
|
|
||||||
//! - Edge-aware Graph Attention
|
|
||||||
//! - Multi-head Graph Attention
|
|
||||||
//! - Gated Graph Attention
|
|
||||||
|
|
||||||
use crate::{Result, TransformerError};
|
|
||||||
use crate::graph::{GraphBatch, Graph, GraphAttentionType};
|
|
||||||
use rtx_tensor::{Tensor, Device};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Graph attention mechanism configuration
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct GraphAttentionConfig {
|
|
||||||
/// Input feature dimension
|
|
||||||
pub input_dim: usize,
|
|
||||||
/// Number of attention heads
|
|
||||||
pub num_heads: usize,
|
|
||||||
/// Type of attention mechanism
|
|
||||||
pub attention_type: GraphAttentionType,
|
|
||||||
/// Edge feature dimension (for edge-aware attention)
|
|
||||||
pub edge_dim: Option<usize>,
|
|
||||||
/// Whether to concatenate or average multi-head outputs
|
|
||||||
pub concat_heads: bool,
|
|
||||||
/// Dropout probability
|
|
||||||
pub dropout: f32,
|
|
||||||
/// Activation function for attention
|
|
||||||
pub attention_activation: String,
|
|
||||||
/// Gate activation function (for gated attention)
|
|
||||||
pub gate_activation: String,
|
|
||||||
/// Whether to use bias in linear layers
|
|
||||||
pub use_bias: bool,
|
|
||||||
/// Attention temperature scaling
|
|
||||||
pub temperature: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphAttentionConfig {
|
|
||||||
/// Create new graph attention configuration
|
|
||||||
pub fn new(input_dim: usize, num_heads: usize, attention_type: GraphAttentionType) -> Self {
|
|
||||||
Self {
|
|
||||||
input_dim,
|
|
||||||
num_heads,
|
|
||||||
attention_type,
|
|
||||||
edge_dim: None,
|
|
||||||
concat_heads: true,
|
|
||||||
dropout: 0.1,
|
|
||||||
attention_activation: "leaky_relu".to_string(),
|
|
||||||
gate_activation: "sigmoid".to_string(),
|
|
||||||
use_bias: true,
|
|
||||||
temperature: 1.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set edge dimension for edge-aware attention
|
|
||||||
pub fn with_edge_dim(mut self, edge_dim: usize) -> Self {
|
|
||||||
self.edge_dim = Some(edge_dim);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set whether to concatenate heads
|
|
||||||
pub fn with_concat_heads(mut self, concat_heads: bool) -> Self {
|
|
||||||
self.concat_heads = concat_heads;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set gate activation function
|
|
||||||
pub fn with_gate_activation(mut self, activation: &str) -> Self {
|
|
||||||
self.gate_activation = activation.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Output from graph attention layer
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AttentionOutput {
|
|
||||||
/// Updated node features
|
|
||||||
pub node_features: Tensor,
|
|
||||||
/// Updated edge features (if edge-aware)
|
|
||||||
pub edge_features: Option<Tensor>,
|
|
||||||
/// Attention weights [num_heads, num_nodes, num_nodes]
|
|
||||||
pub attention_weights: Option<Tensor>,
|
|
||||||
/// Gate values (if gated attention)
|
|
||||||
pub gate_values: Option<Tensor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Graph attention mechanism types
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum AttentionMechanism {
|
|
||||||
/// Dot-product attention
|
|
||||||
DotProduct,
|
|
||||||
/// Additive attention
|
|
||||||
Additive,
|
|
||||||
/// Scaled dot-product attention
|
|
||||||
ScaledDotProduct,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Graph attention layer implementation
|
|
||||||
pub struct GraphAttention {
|
|
||||||
config: GraphAttentionConfig,
|
|
||||||
device: Device,
|
|
||||||
|
|
||||||
// Attention parameters
|
|
||||||
query_projection: Tensor, // [input_dim, head_dim * num_heads]
|
|
||||||
key_projection: Tensor, // [input_dim, head_dim * num_heads]
|
|
||||||
value_projection: Tensor, // [input_dim, head_dim * num_heads]
|
|
||||||
|
|
||||||
// Edge attention parameters (for edge-aware)
|
|
||||||
edge_query_projection: Option<Tensor>,
|
|
||||||
edge_key_projection: Option<Tensor>,
|
|
||||||
edge_value_projection: Option<Tensor>,
|
|
||||||
|
|
||||||
// Output projection
|
|
||||||
output_projection: Tensor, // [head_dim * num_heads, input_dim]
|
|
||||||
|
|
||||||
// Gate parameters (for gated attention)
|
|
||||||
gate_projection: Option<Tensor>,
|
|
||||||
|
|
||||||
// Attention mechanism
|
|
||||||
attention_mechanism: AttentionMechanism,
|
|
||||||
|
|
||||||
// Head dimension
|
|
||||||
head_dim: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphAttention {
|
|
||||||
/// Create a new graph attention layer
|
|
||||||
pub fn new(config: GraphAttentionConfig, device: &Device) -> Result<Self> {
|
|
||||||
if config.input_dim % config.num_heads != 0 {
|
|
||||||
return Err(TransformerError::InvalidInput(format!(
|
|
||||||
"Input dimension {} must be divisible by number of heads {}",
|
|
||||||
config.input_dim, config.num_heads
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let head_dim = config.input_dim / config.num_heads;
|
|
||||||
let total_dim = head_dim * config.num_heads;
|
|
||||||
|
|
||||||
// Initialize query, key, value projections
|
|
||||||
let query_projection = Tensor::randn(&[config.input_dim, total_dim], device)?;
|
|
||||||
let key_projection = Tensor::randn(&[config.input_dim, total_dim], device)?;
|
|
||||||
let value_projection = Tensor::randn(&[config.input_dim, total_dim], device)?;
|
|
||||||
|
|
||||||
// Initialize edge projections if edge-aware
|
|
||||||
let (edge_query_projection, edge_key_projection, edge_value_projection) =
|
|
||||||
if config.attention_type == GraphAttentionType::EdgeAware {
|
|
||||||
if let Some(edge_dim) = config.edge_dim {
|
|
||||||
(
|
|
||||||
Some(Tensor::randn(&[edge_dim, total_dim], device)?),
|
|
||||||
Some(Tensor::randn(&[edge_dim, total_dim], device)?),
|
|
||||||
Some(Tensor::randn(&[edge_dim, total_dim], device)?),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Err(TransformerError::InvalidInput(
|
|
||||||
"Edge dimension must be specified for edge-aware attention".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Output projection
|
|
||||||
let output_projection = if config.concat_heads {
|
|
||||||
Tensor::randn(&[total_dim, config.input_dim], device)?
|
|
||||||
} else {
|
|
||||||
Tensor::randn(&[head_dim, config.input_dim], device)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gate projection for gated attention
|
|
||||||
let gate_projection = if config.attention_type == GraphAttentionType::Gated {
|
|
||||||
Some(Tensor::randn(&[config.input_dim, config.input_dim], device)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let attention_mechanism = match config.attention_type {
|
|
||||||
GraphAttentionType::Standard | GraphAttentionType::EdgeAware => AttentionMechanism::DotProduct,
|
|
||||||
GraphAttentionType::MultiHead => AttentionMechanism::ScaledDotProduct,
|
|
||||||
GraphAttentionType::Gated => AttentionMechanism::Additive,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
query_projection,
|
|
||||||
key_projection,
|
|
||||||
value_projection,
|
|
||||||
edge_query_projection,
|
|
||||||
edge_key_projection,
|
|
||||||
edge_value_projection,
|
|
||||||
output_projection,
|
|
||||||
gate_projection,
|
|
||||||
attention_mechanism,
|
|
||||||
head_dim,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the configuration
|
|
||||||
pub fn config(&self) -> &GraphAttentionConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute attention scores between nodes
|
|
||||||
fn compute_attention_scores(
|
|
||||||
&self,
|
|
||||||
queries: &Tensor,
|
|
||||||
keys: &Tensor,
|
|
||||||
edge_indices: &Tensor,
|
|
||||||
edge_features: Option<&Tensor>,
|
|
||||||
) -> Result<Tensor> {
|
|
||||||
let num_edges = edge_indices.shape().dims()[1];
|
|
||||||
let num_heads = self.config.num_heads;
|
|
||||||
|
|
||||||
// Get source and target node indices
|
|
||||||
let source_indices = edge_indices.get(&[0])?; // [num_edges]
|
|
||||||
let target_indices = edge_indices.get(&[1])?; // [num_edges]
|
|
||||||
|
|
||||||
// Gather queries and keys for edges
|
|
||||||
let source_queries = queries.index_select(0, &source_indices)?; // [num_edges, head_dim * num_heads]
|
|
||||||
let target_keys = keys.index_select(0, &target_indices)?; // [num_edges, head_dim * num_heads]
|
|
||||||
|
|
||||||
// Reshape for multi-head attention: [num_edges, num_heads, head_dim]
|
|
||||||
let source_queries = source_queries.view([num_edges, num_heads, self.head_dim])?;
|
|
||||||
let target_keys = target_keys.view([num_edges, num_heads, self.head_dim])?;
|
|
||||||
|
|
||||||
// Compute attention scores
|
|
||||||
let mut attention_scores = match self.attention_mechanism {
|
|
||||||
AttentionMechanism::DotProduct => {
|
|
||||||
// Simple dot product: sum over head dimension
|
|
||||||
source_queries.mul(&target_keys)?.sum_dim(&[2], false)?
|
|
||||||
}
|
|
||||||
AttentionMechanism::ScaledDotProduct => {
|
|
||||||
// Scaled dot product
|
|
||||||
let scores = source_queries.mul(&target_keys)?.sum_dim(&[2], false)?;
|
|
||||||
let scale = (self.head_dim as f32).sqrt();
|
|
||||||
scores.div(&Tensor::full(&[], (scale) as f32, &&self.device)?)?
|
|
||||||
}
|
|
||||||
AttentionMechanism::Additive => {
|
|
||||||
// Additive attention (simplified)
|
|
||||||
let combined = source_queries.add(&target_keys)?;
|
|
||||||
combined.sum_dim(&[2], false)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add edge features if edge-aware attention
|
|
||||||
if self.config.attention_type == GraphAttentionType::EdgeAware {
|
|
||||||
if let (Some(edge_feats), Some(edge_q_proj)) = (edge_features, &self.edge_query_projection) {
|
|
||||||
let edge_contributions = edge_feats.matmul(edge_q_proj)?; // [num_edges, head_dim * num_heads]
|
|
||||||
let edge_contributions = edge_contributions.view([num_edges, num_heads, self.head_dim])?;
|
|
||||||
let edge_scores = edge_contributions.sum_dim(&[2], false)?; // [num_edges, num_heads]
|
|
||||||
attention_scores = attention_scores.add(&edge_scores)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply temperature scaling
|
|
||||||
if self.config.temperature != 1.0 {
|
|
||||||
attention_scores = attention_scores.div(&Tensor::full(&[], (self.config.temperature) as f32, &&self.device)?)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply activation (leaky_relu is default)
|
|
||||||
attention_scores = self.apply_activation(&attention_scores, &self.config.attention_activation)?;
|
|
||||||
|
|
||||||
Ok(attention_scores)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply softmax attention over edges for each node
|
|
||||||
fn apply_attention_softmax(&self, attention_scores: &Tensor, edge_indices: &Tensor) -> Result<Tensor> {
|
|
||||||
let num_edges = edge_indices.shape().dims()[1];
|
|
||||||
let num_heads = self.config.num_heads;
|
|
||||||
let source_indices = edge_indices.get(&[0])?;
|
|
||||||
|
|
||||||
// For each source node, apply softmax over its outgoing edges
|
|
||||||
// This is a simplified implementation - full implementation would use scatter_softmax
|
|
||||||
|
|
||||||
// Apply softmax directly (simplified - assumes proper grouping)
|
|
||||||
let attention_weights = attention_scores.softmax(-1)?; // [num_edges, num_heads]
|
|
||||||
|
|
||||||
Ok(attention_weights)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply activation function
|
|
||||||
fn apply_activation(&self, input: &Tensor, activation: &str) -> Result<Tensor> {
|
|
||||||
match activation {
|
|
||||||
"relu" => input.relu(),
|
|
||||||
"leaky_relu" => {
|
|
||||||
// Simplified leaky_relu with alpha=0.2
|
|
||||||
let alpha = Tensor::full(&[], 0.2, &self.device);
|
|
||||||
let positive = input.relu()?;
|
|
||||||
let negative = input.clamp(None, Some(0.0))?.mul(&alpha)?;
|
|
||||||
positive.add(&negative)
|
|
||||||
}
|
|
||||||
"sigmoid" => input.sigmoid(),
|
|
||||||
"tanh" => input.tanh(),
|
|
||||||
_ => input.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphLayer for GraphAttention {
|
|
||||||
fn forward(&self, graph: &GraphBatch) -> Result<AttentionOutput> {
|
|
||||||
let num_nodes = graph.num_nodes();
|
|
||||||
let num_edges = graph.num_edges();
|
|
||||||
|
|
||||||
if num_nodes == 0 {
|
|
||||||
return Err(TransformerError::InvalidInput(
|
|
||||||
"Cannot process empty graph".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Project node features to queries, keys, values
|
|
||||||
let queries = graph.node_features.matmul(&self.query_projection)?; // [num_nodes, head_dim * num_heads]
|
|
||||||
let keys = graph.node_features.matmul(&self.key_projection)?; // [num_nodes, head_dim * num_heads]
|
|
||||||
let values = graph.node_features.matmul(&self.value_projection)?; // [num_nodes, head_dim * num_heads]
|
|
||||||
|
|
||||||
// Compute attention scores
|
|
||||||
let attention_scores = self.compute_attention_scores(
|
|
||||||
&queries,
|
|
||||||
&keys,
|
|
||||||
&graph.edge_indices,
|
|
||||||
Some(&graph.edge_features)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Apply softmax to get attention weights
|
|
||||||
let attention_weights = self.apply_attention_softmax(&attention_scores, &graph.edge_indices)?;
|
|
||||||
|
|
||||||
// Apply attention to values
|
|
||||||
let source_indices = graph.edge_indices.get(&[0])?;
|
|
||||||
let target_indices = graph.edge_indices.get(&[1])?;
|
|
||||||
|
|
||||||
// Gather values for target nodes
|
|
||||||
let target_values = values.index_select(0, &target_indices)?; // [num_edges, head_dim * num_heads]
|
|
||||||
let target_values = target_values.view([num_edges, self.config.num_heads, self.head_dim])?;
|
|
||||||
|
|
||||||
// Weight values by attention
|
|
||||||
let attention_weights_expanded = attention_weights.unsqueeze(2)?; // [num_edges, num_heads, 1]
|
|
||||||
let weighted_values = target_values.mul(&attention_weights_expanded)?; // [num_edges, num_heads, head_dim]
|
|
||||||
|
|
||||||
// Aggregate weighted values for each source node
|
|
||||||
// Simplified aggregation - in full implementation would use scatter_add
|
|
||||||
let aggregated_values = if self.config.concat_heads {
|
|
||||||
weighted_values.view([num_edges, self.config.num_heads * self.head_dim])?
|
|
||||||
} else {
|
|
||||||
weighted_values.mean_dim(&[1], false)? // Average over heads
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create output tensor initialized with input features
|
|
||||||
let mut output_features = graph.node_features.clone();
|
|
||||||
|
|
||||||
// In a full implementation, we would scatter_add the aggregated values
|
|
||||||
// For now, we'll apply a simple linear transformation as a placeholder
|
|
||||||
output_features = output_features.matmul(&self.output_projection)?;
|
|
||||||
|
|
||||||
// Apply gating if enabled
|
|
||||||
let gate_values = if self.config.attention_type == GraphAttentionType::Gated {
|
|
||||||
if let Some(ref gate_proj) = self.gate_projection {
|
|
||||||
let gates = graph.node_features.matmul(gate_proj)?;
|
|
||||||
let gate_vals = self.apply_activation(&gates, &self.config.gate_activation)?;
|
|
||||||
output_features = output_features.mul(&gate_vals)?;
|
|
||||||
Some(gate_vals)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process edge features if edge-aware
|
|
||||||
let updated_edge_features = if self.config.attention_type == GraphAttentionType::EdgeAware {
|
|
||||||
if let Some(ref edge_v_proj) = self.edge_value_projection {
|
|
||||||
Some(graph.edge_features.matmul(edge_v_proj)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(AttentionOutput {
|
|
||||||
node_features: output_features,
|
|
||||||
edge_features: updated_edge_features,
|
|
||||||
attention_weights: Some(attention_weights),
|
|
||||||
gate_values,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layer_type(&self) -> &'static str {
|
|
||||||
match self.config.attention_type {
|
|
||||||
GraphAttentionType::Standard => "StandardGraphAttention",
|
|
||||||
GraphAttentionType::EdgeAware => "EdgeAwareGraphAttention",
|
|
||||||
GraphAttentionType::MultiHead => "MultiHeadGraphAttention",
|
|
||||||
GraphAttentionType::Gated => "GatedGraphAttention",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
let mut params = vec![
|
|
||||||
&self.query_projection,
|
|
||||||
&self.key_projection,
|
|
||||||
&self.value_projection,
|
|
||||||
&self.output_projection,
|
|
||||||
];
|
|
||||||
|
|
||||||
if let Some(ref edge_q) = self.edge_query_projection {
|
|
||||||
params.push(edge_q);
|
|
||||||
}
|
|
||||||
if let Some(ref edge_k) = self.edge_key_projection {
|
|
||||||
params.push(edge_k);
|
|
||||||
}
|
|
||||||
if let Some(ref edge_v) = self.edge_value_projection {
|
|
||||||
params.push(edge_v);
|
|
||||||
}
|
|
||||||
if let Some(ref gate) = self.gate_projection {
|
|
||||||
params.push(gate);
|
|
||||||
}
|
|
||||||
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
let mut params = vec![
|
|
||||||
&mut self.query_projection,
|
|
||||||
&mut self.key_projection,
|
|
||||||
&mut self.value_projection,
|
|
||||||
&mut self.output_projection,
|
|
||||||
];
|
|
||||||
|
|
||||||
if let Some(ref mut edge_q) = self.edge_query_projection {
|
|
||||||
params.push(edge_q);
|
|
||||||
}
|
|
||||||
if let Some(ref mut edge_k) = self.edge_key_projection {
|
|
||||||
params.push(edge_k);
|
|
||||||
}
|
|
||||||
if let Some(ref mut edge_v) = self.edge_value_projection {
|
|
||||||
params.push(edge_v);
|
|
||||||
}
|
|
||||||
if let Some(ref mut gate) = self.gate_projection {
|
|
||||||
params.push(gate);
|
|
||||||
}
|
|
||||||
|
|
||||||
params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,595 +0,0 @@
|
|||||||
//! Graph Pooling implementations for creating graph-level representations
|
|
||||||
//!
|
|
||||||
//! Provides multiple strategies for pooling node-level features into
|
|
||||||
//! graph-level representations:
|
|
||||||
//! - Global pooling (mean, max, sum)
|
|
||||||
//! - Hierarchical graph pooling
|
|
||||||
//! - Set2Set pooling
|
|
||||||
//! - Attention-based pooling
|
|
||||||
|
|
||||||
use crate::{Result, TransformerError};
|
|
||||||
use crate::graph::{GraphBatch, PoolingStrategy};
|
|
||||||
use rtx_tensor::{Tensor, Device};
|
|
||||||
|
|
||||||
/// Graph pooling trait for converting node features to graph representations
|
|
||||||
pub trait GraphPooling: Send + Sync {
|
|
||||||
/// Forward pass to pool node features into graph-level representation
|
|
||||||
fn forward(&self, graph: &GraphBatch) -> Result<Tensor>;
|
|
||||||
|
|
||||||
/// Get the strategy used by this pooling layer
|
|
||||||
fn strategy(&self) -> &PoolingStrategy;
|
|
||||||
|
|
||||||
/// Get input dimension
|
|
||||||
fn input_dim(&self) -> usize;
|
|
||||||
|
|
||||||
/// Get device
|
|
||||||
fn device(&self) -> &Device;
|
|
||||||
|
|
||||||
/// Get parameters
|
|
||||||
fn parameters(&self) -> Vec<&Tensor>;
|
|
||||||
|
|
||||||
/// Get mutable parameters
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Global pooling methods
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum GlobalPoolingType {
|
|
||||||
Mean,
|
|
||||||
Max,
|
|
||||||
Sum,
|
|
||||||
Attention,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Global graph pooling implementation
|
|
||||||
pub struct GlobalPooling {
|
|
||||||
strategy: PoolingStrategy,
|
|
||||||
input_dim: usize,
|
|
||||||
device: Device,
|
|
||||||
pooling_type: GlobalPoolingType,
|
|
||||||
|
|
||||||
// Attention pooling parameters (if using attention)
|
|
||||||
attention_weights: Option<Tensor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlobalPooling {
|
|
||||||
/// Create new global pooling layer
|
|
||||||
pub fn new(input_dim: usize, device: &Device) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
strategy: PoolingStrategy::Global,
|
|
||||||
input_dim,
|
|
||||||
device: device.clone(),
|
|
||||||
pooling_type: GlobalPoolingType::Mean,
|
|
||||||
attention_weights: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create global pooling with specific type
|
|
||||||
pub fn with_type(input_dim: usize, pooling_type: GlobalPoolingType, device: &Device) -> Result<Self> {
|
|
||||||
let attention_weights = if pooling_type == GlobalPoolingType::Attention {
|
|
||||||
Some(Tensor::randn(&[input_dim, 1], device)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
strategy: PoolingStrategy::Global,
|
|
||||||
input_dim,
|
|
||||||
device: device.clone(),
|
|
||||||
pooling_type,
|
|
||||||
attention_weights,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphPooling for GlobalPooling {
|
|
||||||
fn forward(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_graphs = graph.batch_size;
|
|
||||||
let node_features = &graph.node_features;
|
|
||||||
|
|
||||||
// Pool features for each graph in the batch
|
|
||||||
let mut graph_representations = Vec::new();
|
|
||||||
|
|
||||||
for i in 0..num_graphs {
|
|
||||||
let start_idx = graph.graph_boundaries[i];
|
|
||||||
let end_idx = graph.graph_boundaries[i + 1];
|
|
||||||
|
|
||||||
if start_idx >= end_idx {
|
|
||||||
// Handle empty graph case
|
|
||||||
let empty_repr = Tensor::zeros([1, self.input_dim], &self.device)?;
|
|
||||||
graph_representations.push(empty_repr);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract nodes for this graph
|
|
||||||
let graph_nodes = node_features.narrow(0, start_idx, end_idx - start_idx)?;
|
|
||||||
|
|
||||||
// Apply pooling
|
|
||||||
let pooled = match self.pooling_type {
|
|
||||||
GlobalPoolingType::Mean => {
|
|
||||||
graph_nodes.mean_dim(&[0], true)?
|
|
||||||
}
|
|
||||||
GlobalPoolingType::Max => {
|
|
||||||
graph_nodes.max_dim(0, true)?.0
|
|
||||||
}
|
|
||||||
GlobalPoolingType::Sum => {
|
|
||||||
graph_nodes.sum_dim(&[0], true)?
|
|
||||||
}
|
|
||||||
GlobalPoolingType::Attention => {
|
|
||||||
if let Some(ref attention_weights) = self.attention_weights {
|
|
||||||
// Compute attention scores
|
|
||||||
let scores = graph_nodes.matmul(attention_weights)?; // [num_nodes, 1]
|
|
||||||
let attention = scores.softmax(0)?; // [num_nodes, 1]
|
|
||||||
|
|
||||||
// Weighted sum
|
|
||||||
let weighted = graph_nodes.mul(&attention)?;
|
|
||||||
weighted.sum_dim(&[0], true)?
|
|
||||||
} else {
|
|
||||||
return Err(TransformerError::InvalidInput(
|
|
||||||
"Attention weights not initialized".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
graph_representations.push(pooled);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stack all graph representations
|
|
||||||
if graph_representations.is_empty() {
|
|
||||||
Tensor::zeros([0, self.input_dim], &self.device)
|
|
||||||
} else {
|
|
||||||
Tensor::cat(&graph_representations, 0) // [batch_size, input_dim]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strategy(&self) -> &PoolingStrategy {
|
|
||||||
&self.strategy
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_dim(&self) -> usize {
|
|
||||||
self.input_dim
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
if let Some(ref weights) = self.attention_weights {
|
|
||||||
vec![weights]
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
if let Some(ref mut weights) = self.attention_weights {
|
|
||||||
vec![weights]
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hierarchical graph pooling implementation
|
|
||||||
pub struct HierarchicalPooling {
|
|
||||||
strategy: PoolingStrategy,
|
|
||||||
input_dim: usize,
|
|
||||||
device: Device,
|
|
||||||
num_levels: usize,
|
|
||||||
pooling_ratio: f32,
|
|
||||||
|
|
||||||
// Learnable parameters for node selection
|
|
||||||
node_projection: Tensor, // [input_dim, 1] for computing node scores
|
|
||||||
edge_projection: Option<Tensor>, // [edge_dim, 1] for edge scores
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HierarchicalPooling {
|
|
||||||
/// Create new hierarchical pooling layer
|
|
||||||
pub fn new(
|
|
||||||
input_dim: usize,
|
|
||||||
num_levels: usize,
|
|
||||||
pooling_ratio: f32,
|
|
||||||
device: &Device
|
|
||||||
) -> Result<Self> {
|
|
||||||
let node_projection = Tensor::randn(&[input_dim, 1], device)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
strategy: PoolingStrategy::Hierarchical,
|
|
||||||
input_dim,
|
|
||||||
device: device.clone(),
|
|
||||||
num_levels,
|
|
||||||
pooling_ratio,
|
|
||||||
node_projection,
|
|
||||||
edge_projection: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select top-k nodes based on learned scores
|
|
||||||
fn select_nodes(&self, node_features: &Tensor, k: usize) -> Result<(Tensor, Tensor)> {
|
|
||||||
// Compute node scores
|
|
||||||
let scores = node_features.matmul(&self.node_projection)?; // [num_nodes, 1]
|
|
||||||
let scores = scores.squeeze(Some(1))?; // [num_nodes]
|
|
||||||
|
|
||||||
// Get top-k nodes
|
|
||||||
let (top_values, top_indices) = scores.topk(k, 0, true, true)?;
|
|
||||||
|
|
||||||
// Select corresponding node features
|
|
||||||
let selected_features = node_features.index_select(0, &top_indices)?;
|
|
||||||
|
|
||||||
Ok((selected_features, top_indices))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Coarsen graph by clustering nodes
|
|
||||||
fn coarsen_graph(&self, graph: &GraphBatch, selected_indices: &Tensor) -> Result<GraphBatch> {
|
|
||||||
// Simplified coarsening - in practice would properly handle edge connections
|
|
||||||
let selected_nodes = graph.node_features.index_select(0, selected_indices)?;
|
|
||||||
let num_selected = selected_indices.shape().dims()[0];
|
|
||||||
|
|
||||||
// Create simplified edge structure (placeholder)
|
|
||||||
let new_edge_indices = if num_selected > 1 {
|
|
||||||
// Connect all selected nodes in a chain (simplified)
|
|
||||||
let mut edge_list = Vec::new();
|
|
||||||
for i in 0..(num_selected - 1) {
|
|
||||||
edge_list.push(i as i64);
|
|
||||||
edge_list.push((i + 1) as i64);
|
|
||||||
}
|
|
||||||
Tensor::from_vec(edge_list, [2, num_selected - 1], &self.device)?
|
|
||||||
} else {
|
|
||||||
Tensor::zeros([2, 0], &self.device)?
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_edge_features = Tensor::zeros([new_edge_indices.shape().dims()[1], graph.edge_features.shape().dims()[1]], &self.device)?;
|
|
||||||
|
|
||||||
GraphBatch::new(
|
|
||||||
selected_nodes,
|
|
||||||
new_edge_features,
|
|
||||||
new_edge_indices,
|
|
||||||
vec![0, num_selected], // Single graph
|
|
||||||
self.device.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphPooling for HierarchicalPooling {
|
|
||||||
fn forward(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let mut current_graph = graph.clone();
|
|
||||||
let mut representations = Vec::new();
|
|
||||||
|
|
||||||
// Apply hierarchical pooling for each level
|
|
||||||
for level in 0..self.num_levels {
|
|
||||||
let num_graphs = current_graph.batch_size;
|
|
||||||
let mut level_representations = Vec::new();
|
|
||||||
|
|
||||||
for i in 0..num_graphs {
|
|
||||||
let start_idx = current_graph.graph_boundaries[i];
|
|
||||||
let end_idx = current_graph.graph_boundaries[i + 1];
|
|
||||||
|
|
||||||
if start_idx >= end_idx {
|
|
||||||
let empty_repr = Tensor::zeros([1, self.input_dim], &self.device)?;
|
|
||||||
level_representations.push(empty_repr);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let graph_nodes = current_graph.node_features.narrow(0, start_idx, end_idx - start_idx)?;
|
|
||||||
let num_nodes = end_idx - start_idx;
|
|
||||||
|
|
||||||
// Determine number of nodes to select
|
|
||||||
let k = ((num_nodes as f32 * self.pooling_ratio).ceil() as usize).max(1);
|
|
||||||
|
|
||||||
// Select top-k nodes
|
|
||||||
let (selected_features, _indices) = self.select_nodes(&graph_nodes, k)?;
|
|
||||||
|
|
||||||
// Global pooling of selected nodes
|
|
||||||
let pooled = selected_features.mean_dim(&[0], true)?;
|
|
||||||
level_representations.push(pooled);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store representations from this level
|
|
||||||
if !level_representations.is_empty() {
|
|
||||||
let level_repr = Tensor::cat(&level_representations, 0)?;
|
|
||||||
representations.push(level_repr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create coarsened graph for next level (if not last level)
|
|
||||||
if level < self.num_levels - 1 {
|
|
||||||
// Simplified: just use current graph (in practice would coarsen)
|
|
||||||
// current_graph = coarsen_graph(current_graph)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine representations from all levels
|
|
||||||
if representations.is_empty() {
|
|
||||||
Tensor::zeros([graph.batch_size, self.input_dim], &self.device)
|
|
||||||
} else {
|
|
||||||
// For simplicity, return the last level's representation
|
|
||||||
representations.into_iter().last().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strategy(&self) -> &PoolingStrategy {
|
|
||||||
&self.strategy
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_dim(&self) -> usize {
|
|
||||||
self.input_dim
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
let mut params = vec![&self.node_projection];
|
|
||||||
if let Some(ref edge_proj) = self.edge_projection {
|
|
||||||
params.push(edge_proj);
|
|
||||||
}
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
let mut params = vec![&mut self.node_projection];
|
|
||||||
if let Some(ref mut edge_proj) = self.edge_projection {
|
|
||||||
params.push(edge_proj);
|
|
||||||
}
|
|
||||||
params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set2Set pooling implementation
|
|
||||||
pub struct Set2SetPooling {
|
|
||||||
strategy: PoolingStrategy,
|
|
||||||
input_dim: usize,
|
|
||||||
device: Device,
|
|
||||||
num_iterations: usize,
|
|
||||||
|
|
||||||
// LSTM-like parameters for Set2Set
|
|
||||||
lstm_input_size: usize,
|
|
||||||
lstm_hidden_size: usize,
|
|
||||||
lstm_weights: Tensor,
|
|
||||||
lstm_bias: Tensor,
|
|
||||||
attention_projection: Tensor,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Set2SetPooling {
|
|
||||||
/// Create new Set2Set pooling layer
|
|
||||||
pub fn new(input_dim: usize, device: &Device) -> Result<Self> {
|
|
||||||
let num_iterations = 3;
|
|
||||||
let lstm_hidden_size = input_dim;
|
|
||||||
let lstm_input_size = 2 * input_dim; // Memory vector + pooled representation
|
|
||||||
|
|
||||||
let lstm_weights = Tensor::randn(&[lstm_input_size + lstm_hidden_size, 4 * lstm_hidden_size], device)?;
|
|
||||||
let lstm_bias = Tensor::zeros([4 * lstm_hidden_size], device)?;
|
|
||||||
let attention_projection = Tensor::randn(&[lstm_hidden_size, 1], device)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
strategy: PoolingStrategy::Set2Set,
|
|
||||||
input_dim,
|
|
||||||
device: device.clone(),
|
|
||||||
num_iterations,
|
|
||||||
lstm_input_size,
|
|
||||||
lstm_hidden_size,
|
|
||||||
lstm_weights,
|
|
||||||
lstm_bias,
|
|
||||||
attention_projection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply LSTM cell
|
|
||||||
fn lstm_cell(&self, input: &Tensor, hidden: &Tensor, cell: &Tensor) -> Result<(Tensor, Tensor)> {
|
|
||||||
// Concatenate input and hidden state
|
|
||||||
let combined = Tensor::cat(&[input, hidden], -1)?;
|
|
||||||
|
|
||||||
// Apply linear transformation
|
|
||||||
let gates = combined.matmul(&self.lstm_weights)?.add(&self.lstm_bias)?;
|
|
||||||
|
|
||||||
// Split into 4 gates
|
|
||||||
let gate_size = self.lstm_hidden_size;
|
|
||||||
let input_gate = gates.narrow(-1, 0, gate_size)?.sigmoid()?;
|
|
||||||
let forget_gate = gates.narrow(-1, gate_size, gate_size)?.sigmoid()?;
|
|
||||||
let cell_gate = gates.narrow(-1, 2 * gate_size, gate_size)?.tanh()?;
|
|
||||||
let output_gate = gates.narrow(-1, 3 * gate_size, gate_size)?.sigmoid()?;
|
|
||||||
|
|
||||||
// Update cell state
|
|
||||||
let new_cell = forget_gate.mul(cell)?.add(&input_gate.mul(&cell_gate)?)?;
|
|
||||||
|
|
||||||
// Update hidden state
|
|
||||||
let new_hidden = output_gate.mul(&new_cell.tanh()?)?;
|
|
||||||
|
|
||||||
Ok((new_hidden, new_cell))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute attention weights
|
|
||||||
fn compute_attention(&self, query: &Tensor, node_features: &Tensor) -> Result<Tensor> {
|
|
||||||
// Project query to attention space
|
|
||||||
let attention_scores = query.matmul(&self.attention_projection)?; // [1, 1]
|
|
||||||
|
|
||||||
// Compute attention over all nodes (simplified)
|
|
||||||
let num_nodes = node_features.shape().dims()[0];
|
|
||||||
let scores = attention_scores.expand(&[num_nodes, 1])?;
|
|
||||||
let attention_weights = scores.softmax(0)?;
|
|
||||||
|
|
||||||
Ok(attention_weights)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphPooling for Set2SetPooling {
|
|
||||||
fn forward(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_graphs = graph.batch_size;
|
|
||||||
let mut graph_representations = Vec::new();
|
|
||||||
|
|
||||||
for i in 0..num_graphs {
|
|
||||||
let start_idx = graph.graph_boundaries[i];
|
|
||||||
let end_idx = graph.graph_boundaries[i + 1];
|
|
||||||
|
|
||||||
if start_idx >= end_idx {
|
|
||||||
let empty_repr = Tensor::zeros([1, 2 * self.input_dim], &self.device)?;
|
|
||||||
graph_representations.push(empty_repr);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let graph_nodes = graph.node_features.narrow(0, start_idx, end_idx - start_idx)?;
|
|
||||||
let num_nodes = end_idx - start_idx;
|
|
||||||
|
|
||||||
// Initialize LSTM states
|
|
||||||
let mut hidden = Tensor::zeros([1, self.lstm_hidden_size], &self.device)?;
|
|
||||||
let mut cell = Tensor::zeros([1, self.lstm_hidden_size], &self.device)?;
|
|
||||||
let mut memory = Tensor::zeros([1, self.input_dim], &self.device)?;
|
|
||||||
|
|
||||||
// Run Set2Set iterations
|
|
||||||
for _iter in 0..self.num_iterations {
|
|
||||||
// Compute attention
|
|
||||||
let attention_weights = self.compute_attention(&hidden, &graph_nodes)?; // [num_nodes, 1]
|
|
||||||
|
|
||||||
// Compute attended representation
|
|
||||||
let attended = graph_nodes.mul(&attention_weights)?.sum_dim(&[0], true)?; // [1, input_dim]
|
|
||||||
|
|
||||||
// Update memory
|
|
||||||
memory = memory.add(&attended)?;
|
|
||||||
|
|
||||||
// Prepare LSTM input
|
|
||||||
let lstm_input = Tensor::cat(&[memory.clone(), attended], -1)?; // [1, 2 * input_dim]
|
|
||||||
|
|
||||||
// Apply LSTM
|
|
||||||
let (new_hidden, new_cell) = self.lstm_cell(&lstm_input, &hidden, &cell)?;
|
|
||||||
hidden = new_hidden;
|
|
||||||
cell = new_cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final representation is concatenation of memory and final hidden state
|
|
||||||
let final_repr = Tensor::cat(&[memory, hidden], -1)?; // [1, 2 * input_dim]
|
|
||||||
graph_representations.push(final_repr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if graph_representations.is_empty() {
|
|
||||||
Tensor::zeros([0, 2 * self.input_dim], &self.device)
|
|
||||||
} else {
|
|
||||||
Tensor::cat(&graph_representations, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strategy(&self) -> &PoolingStrategy {
|
|
||||||
&self.strategy
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_dim(&self) -> usize {
|
|
||||||
self.input_dim
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
vec![&self.lstm_weights, &self.lstm_bias, &self.attention_projection]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
vec![&mut self.lstm_weights, &mut self.lstm_bias, &mut self.attention_projection]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attention-based pooling implementation
|
|
||||||
pub struct AttentionPooling {
|
|
||||||
strategy: PoolingStrategy,
|
|
||||||
input_dim: usize,
|
|
||||||
device: Device,
|
|
||||||
|
|
||||||
// Attention parameters
|
|
||||||
query_projection: Tensor,
|
|
||||||
key_projection: Tensor,
|
|
||||||
value_projection: Tensor,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttentionPooling {
|
|
||||||
/// Create new attention pooling layer
|
|
||||||
pub fn new(input_dim: usize, device: &Device) -> Result<Self> {
|
|
||||||
let query_projection = Tensor::randn(&[input_dim, input_dim], device)?;
|
|
||||||
let key_projection = Tensor::randn(&[input_dim, input_dim], device)?;
|
|
||||||
let value_projection = Tensor::randn(&[input_dim, input_dim], device)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
strategy: PoolingStrategy::Attention,
|
|
||||||
input_dim,
|
|
||||||
device: device.clone(),
|
|
||||||
query_projection,
|
|
||||||
key_projection,
|
|
||||||
value_projection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphPooling for AttentionPooling {
|
|
||||||
fn forward(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_graphs = graph.batch_size;
|
|
||||||
let mut graph_representations = Vec::new();
|
|
||||||
|
|
||||||
for i in 0..num_graphs {
|
|
||||||
let start_idx = graph.graph_boundaries[i];
|
|
||||||
let end_idx = graph.graph_boundaries[i + 1];
|
|
||||||
|
|
||||||
if start_idx >= end_idx {
|
|
||||||
let empty_repr = Tensor::zeros([1, self.input_dim], &self.device)?;
|
|
||||||
graph_representations.push(empty_repr);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let graph_nodes = graph.node_features.narrow(0, start_idx, end_idx - start_idx)?;
|
|
||||||
|
|
||||||
// Compute query, keys, values
|
|
||||||
let queries = graph_nodes.matmul(&self.query_projection)?;
|
|
||||||
let keys = graph_nodes.matmul(&self.key_projection)?;
|
|
||||||
let values = graph_nodes.matmul(&self.value_projection)?;
|
|
||||||
|
|
||||||
// Use mean of queries as the global query
|
|
||||||
let global_query = queries.mean_dim(&[0], true)?; // [1, input_dim]
|
|
||||||
|
|
||||||
// Compute attention scores
|
|
||||||
let scores = global_query.matmul(&keys.t()?)?; // [1, num_nodes]
|
|
||||||
let attention_weights = scores.softmax(-1)?; // [1, num_nodes]
|
|
||||||
|
|
||||||
// Apply attention to values
|
|
||||||
let attended = attention_weights.matmul(&values)?; // [1, input_dim]
|
|
||||||
graph_representations.push(attended);
|
|
||||||
}
|
|
||||||
|
|
||||||
if graph_representations.is_empty() {
|
|
||||||
Tensor::zeros([0, self.input_dim], &self.device)
|
|
||||||
} else {
|
|
||||||
Tensor::cat(&graph_representations, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strategy(&self) -> &PoolingStrategy {
|
|
||||||
&self.strategy
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_dim(&self) -> usize {
|
|
||||||
self.input_dim
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
vec![&self.query_projection, &self.key_projection, &self.value_projection]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
vec![&mut self.query_projection, &mut self.key_projection, &mut self.value_projection]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to create appropriate pooling layer
|
|
||||||
impl dyn GraphPooling {
|
|
||||||
pub fn new(strategy: PoolingStrategy, input_dim: usize, device: &Device) -> Result<Box<dyn GraphPooling>> {
|
|
||||||
match strategy {
|
|
||||||
PoolingStrategy::Global => Ok(Box::new(GlobalPooling::new(input_dim, device)?)),
|
|
||||||
PoolingStrategy::Hierarchical => Ok(Box::new(HierarchicalPooling::new(input_dim, 2, 0.5, device)?)),
|
|
||||||
PoolingStrategy::Set2Set => Ok(Box::new(Set2SetPooling::new(input_dim, device)?)),
|
|
||||||
PoolingStrategy::Attention => Ok(Box::new(AttentionPooling::new(input_dim, device)?)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
//! Graph Transformer implementation for attention-based learning on graph-structured data
|
|
||||||
//!
|
|
||||||
//! Provides complete Graph Transformer functionality with support for:
|
|
||||||
//! - Node and edge feature processing
|
|
||||||
//! - Multiple attention mechanisms
|
|
||||||
//! - Graph-level representations via pooling
|
|
||||||
//! - Batch processing of multiple graphs
|
|
||||||
|
|
||||||
use crate::{Result, TransformerError};
|
|
||||||
use crate::graph::{GraphBatch, Graph, GraphAttentionType};
|
|
||||||
use rtx_tensor::{Tensor, Device, DType};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Graph pooling strategies
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum PoolingStrategy {
|
|
||||||
/// Global mean/max/sum pooling
|
|
||||||
Global,
|
|
||||||
/// Hierarchical graph pooling
|
|
||||||
Hierarchical,
|
|
||||||
/// Set2Set pooling mechanism
|
|
||||||
Set2Set,
|
|
||||||
/// Attention-based pooling
|
|
||||||
Attention,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Graph Transformer configuration
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct GraphTransformerConfig {
|
|
||||||
/// Node feature dimension
|
|
||||||
pub node_dim: usize,
|
|
||||||
/// Edge feature dimension
|
|
||||||
pub edge_dim: usize,
|
|
||||||
/// Hidden dimension for transformations
|
|
||||||
pub hidden_dim: usize,
|
|
||||||
/// Number of attention heads
|
|
||||||
pub num_heads: usize,
|
|
||||||
/// Number of transformer layers
|
|
||||||
pub num_layers: usize,
|
|
||||||
/// Type of graph attention mechanism
|
|
||||||
pub attention_type: GraphAttentionType,
|
|
||||||
/// Graph pooling strategy
|
|
||||||
pub pooling_strategy: PoolingStrategy,
|
|
||||||
/// Dropout probability
|
|
||||||
pub dropout: f32,
|
|
||||||
/// Whether to use edge features
|
|
||||||
pub use_edge_features: bool,
|
|
||||||
/// Whether to use positional encoding
|
|
||||||
pub use_positional_encoding: bool,
|
|
||||||
/// Whether to return attention weights
|
|
||||||
pub return_attention_weights: bool,
|
|
||||||
/// Memory efficient mode
|
|
||||||
pub memory_efficient: bool,
|
|
||||||
/// Gradient checkpointing
|
|
||||||
pub gradient_checkpointing: bool,
|
|
||||||
/// Layer normalization epsilon
|
|
||||||
pub layer_norm_eps: f64,
|
|
||||||
/// Activation function
|
|
||||||
pub activation: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphTransformerConfig {
|
|
||||||
/// Create new Graph Transformer configuration
|
|
||||||
pub fn new(node_dim: usize, edge_dim: usize, hidden_dim: usize, num_heads: usize) -> Self {
|
|
||||||
if num_heads == 0 {
|
|
||||||
panic!("Number of attention heads must be greater than 0");
|
|
||||||
}
|
|
||||||
if node_dim == 0 {
|
|
||||||
panic!("Node dimension must be greater than 0");
|
|
||||||
}
|
|
||||||
if hidden_dim == 0 {
|
|
||||||
panic!("Hidden dimension must be greater than 0");
|
|
||||||
}
|
|
||||||
if hidden_dim % num_heads != 0 {
|
|
||||||
panic!("Hidden dimension must be divisible by number of heads");
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
node_dim,
|
|
||||||
edge_dim,
|
|
||||||
hidden_dim,
|
|
||||||
num_heads,
|
|
||||||
num_layers: 6,
|
|
||||||
attention_type: GraphAttentionType::Standard,
|
|
||||||
pooling_strategy: PoolingStrategy::Global,
|
|
||||||
dropout: 0.1,
|
|
||||||
use_edge_features: true,
|
|
||||||
use_positional_encoding: true,
|
|
||||||
return_attention_weights: false,
|
|
||||||
memory_efficient: false,
|
|
||||||
gradient_checkpointing: false,
|
|
||||||
layer_norm_eps: 1e-5,
|
|
||||||
activation: "relu".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set number of layers
|
|
||||||
pub fn with_num_layers(mut self, num_layers: usize) -> Self {
|
|
||||||
self.num_layers = num_layers;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set attention type
|
|
||||||
pub fn with_attention_type(mut self, attention_type: GraphAttentionType) -> Self {
|
|
||||||
self.attention_type = attention_type;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set pooling strategy
|
|
||||||
pub fn with_pooling_strategy(mut self, pooling_strategy: PoolingStrategy) -> Self {
|
|
||||||
self.pooling_strategy = pooling_strategy;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set dropout rate
|
|
||||||
pub fn with_dropout(mut self, dropout: f32) -> Self {
|
|
||||||
self.dropout = dropout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Disable edge features
|
|
||||||
pub fn without_edge_features(mut self) -> Self {
|
|
||||||
self.use_edge_features = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Disable positional encoding
|
|
||||||
pub fn without_positional_encoding(mut self) -> Self {
|
|
||||||
self.use_positional_encoding = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable returning attention weights
|
|
||||||
pub fn with_return_attention_weights(mut self, return_weights: bool) -> Self {
|
|
||||||
self.return_attention_weights = return_weights;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable memory efficient mode
|
|
||||||
pub fn with_memory_efficient(mut self, memory_efficient: bool) -> Self {
|
|
||||||
self.memory_efficient = memory_efficient;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable gradient checkpointing
|
|
||||||
pub fn with_gradient_checkpointing(mut self, gradient_checkpointing: bool) -> Self {
|
|
||||||
self.gradient_checkpointing = gradient_checkpointing;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set activation function
|
|
||||||
pub fn with_activation(mut self, activation: &str) -> Self {
|
|
||||||
self.activation = activation.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Output from Graph Transformer
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GraphOutput {
|
|
||||||
/// Updated node representations [total_nodes, node_dim]
|
|
||||||
pub node_representations: Tensor,
|
|
||||||
/// Graph-level representations [batch_size, node_dim]
|
|
||||||
pub graph_representations: Tensor,
|
|
||||||
/// Attention weights if requested [num_layers, batch_size, num_heads, max_nodes, max_nodes]
|
|
||||||
pub attention_weights: Option<Tensor>,
|
|
||||||
/// Updated edge representations if using edge features
|
|
||||||
pub edge_representations: Option<Tensor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Graph Transformer implementation
|
|
||||||
pub struct GraphTransformer {
|
|
||||||
config: GraphTransformerConfig,
|
|
||||||
device: Device,
|
|
||||||
|
|
||||||
// Core components
|
|
||||||
node_embedding: Tensor, // Linear layer for node input projection
|
|
||||||
edge_embedding: Option<Tensor>, // Linear layer for edge input projection
|
|
||||||
|
|
||||||
// Transformer layers
|
|
||||||
attention_layers: Vec<Box<dyn GraphLayer>>,
|
|
||||||
norm_layers: Vec<Tensor>, // Layer normalization parameters
|
|
||||||
|
|
||||||
// Pooling layer
|
|
||||||
pooling_layer: Box<dyn GraphPooling>,
|
|
||||||
|
|
||||||
// Output projection
|
|
||||||
output_projection: Tensor,
|
|
||||||
|
|
||||||
// Positional encoding
|
|
||||||
positional_encoding: Option<Box<dyn GraphPositionalEncoding>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphTransformer {
|
|
||||||
/// Create a new Graph Transformer
|
|
||||||
pub fn new(config: GraphTransformerConfig, device: &Device) -> Result<Self> {
|
|
||||||
// Initialize node embedding (input projection)
|
|
||||||
let node_embedding = Tensor::randn(&[config.node_dim, config.hidden_dim], device)?;
|
|
||||||
|
|
||||||
// Initialize edge embedding if using edge features
|
|
||||||
let edge_embedding = if config.use_edge_features {
|
|
||||||
Some(Tensor::randn(&[config.edge_dim, config.hidden_dim], device)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create attention layers
|
|
||||||
let mut attention_layers: Vec<Box<dyn GraphLayer>> = Vec::new();
|
|
||||||
for _ in 0..config.num_layers {
|
|
||||||
let attention_config = GraphAttentionConfig::new(
|
|
||||||
config.hidden_dim,
|
|
||||||
config.num_heads,
|
|
||||||
config.attention_type,
|
|
||||||
);
|
|
||||||
|
|
||||||
let attention_layer = GraphAttention::new(attention_config, device)?;
|
|
||||||
attention_layers.push(Box::new(attention_layer));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create layer normalization parameters
|
|
||||||
let mut norm_layers = Vec::new();
|
|
||||||
for _ in 0..config.num_layers {
|
|
||||||
let norm_weight = Tensor::ones([config.hidden_dim], device)?;
|
|
||||||
norm_layers.push(norm_weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create pooling layer
|
|
||||||
let pooling_layer: Box<dyn GraphPooling> = match config.pooling_strategy {
|
|
||||||
PoolingStrategy::Global => {
|
|
||||||
Box::new(GlobalPooling::new(config.hidden_dim, device)?)
|
|
||||||
}
|
|
||||||
PoolingStrategy::Hierarchical => {
|
|
||||||
Box::new(HierarchicalPooling::new(config.hidden_dim, 2, 0.5, device)?)
|
|
||||||
}
|
|
||||||
PoolingStrategy::Set2Set => {
|
|
||||||
Box::new(Set2SetPooling::new(config.hidden_dim, device)?)
|
|
||||||
}
|
|
||||||
PoolingStrategy::Attention => {
|
|
||||||
Box::new(AttentionPooling::new(config.hidden_dim, device)?)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Output projection
|
|
||||||
let output_projection = Tensor::randn(&[config.hidden_dim, config.node_dim], device)?;
|
|
||||||
|
|
||||||
// Positional encoding
|
|
||||||
let positional_encoding = if config.use_positional_encoding {
|
|
||||||
let pe_config = GraphPEConfig::new(config.hidden_dim, GraphPEType::Laplacian);
|
|
||||||
Some(Box::new(GraphPositionalEncoding::new(pe_config, device)?))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
node_embedding,
|
|
||||||
edge_embedding,
|
|
||||||
attention_layers,
|
|
||||||
norm_layers,
|
|
||||||
pooling_layer,
|
|
||||||
output_projection,
|
|
||||||
positional_encoding,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the configuration
|
|
||||||
pub fn config(&self) -> &GraphTransformerConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphLayer for GraphTransformer {
|
|
||||||
fn forward(&self, graph: &GraphBatch) -> Result<GraphOutput> {
|
|
||||||
// Input validation
|
|
||||||
if graph.num_nodes() == 0 {
|
|
||||||
return Err(TransformerError::InvalidInput(
|
|
||||||
"Cannot process empty graph batch".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected_node_dim = self.config.node_dim;
|
|
||||||
let actual_node_dim = graph.node_features.shape().dims()[1];
|
|
||||||
if actual_node_dim != expected_node_dim {
|
|
||||||
return Err(TransformerError::shape_mismatch(format!(
|
|
||||||
"Expected node features dimension {}, got {}",
|
|
||||||
expected_node_dim, actual_node_dim
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Project input node features to hidden dimension
|
|
||||||
let mut node_features = graph.node_features.matmul(&self.node_embedding)?;
|
|
||||||
|
|
||||||
// Add positional encoding if enabled
|
|
||||||
if let Some(ref pe) = self.positional_encoding {
|
|
||||||
let pos_encoding = pe.forward(graph)?;
|
|
||||||
node_features = node_features.add(&pos_encoding)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process edge features if enabled
|
|
||||||
let mut edge_features = if self.config.use_edge_features && self.edge_embedding.is_some() {
|
|
||||||
Some(graph.edge_features.matmul(self.edge_embedding.as_ref().unwrap())?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Storage for attention weights if requested
|
|
||||||
let mut all_attention_weights = Vec::new();
|
|
||||||
|
|
||||||
// Apply transformer layers
|
|
||||||
for (layer_idx, attention_layer) in self.attention_layers.iter().enumerate() {
|
|
||||||
// Create temporary graph batch with current features
|
|
||||||
let temp_graph = GraphBatch::new(
|
|
||||||
node_features.clone(),
|
|
||||||
edge_features.clone().unwrap_or_else(|| graph.edge_features.clone()),
|
|
||||||
graph.edge_indices.clone(),
|
|
||||||
graph.graph_boundaries.clone(),
|
|
||||||
self.device.clone(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Apply attention layer
|
|
||||||
let layer_output = attention_layer.forward(&temp_graph)?;
|
|
||||||
|
|
||||||
// Residual connection and layer norm
|
|
||||||
let residual = node_features.clone();
|
|
||||||
node_features = layer_output.node_representations;
|
|
||||||
node_features = node_features.add(&residual)?;
|
|
||||||
|
|
||||||
// Layer normalization
|
|
||||||
node_features = self.apply_layer_norm(&node_features, layer_idx)?;
|
|
||||||
|
|
||||||
// Store attention weights if requested
|
|
||||||
if self.config.return_attention_weights {
|
|
||||||
if let Some(attention_weights) = layer_output.attention_weights {
|
|
||||||
all_attention_weights.push(attention_weights);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update edge features if available
|
|
||||||
if let Some(edge_repr) = layer_output.edge_representations {
|
|
||||||
edge_features = Some(edge_repr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply pooling to get graph-level representations
|
|
||||||
let temp_graph_for_pooling = GraphBatch::new(
|
|
||||||
node_features.clone(),
|
|
||||||
edge_features.clone().unwrap_or_else(|| graph.edge_features.clone()),
|
|
||||||
graph.edge_indices.clone(),
|
|
||||||
graph.graph_boundaries.clone(),
|
|
||||||
self.device.clone(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let graph_representations = self.pooling_layer.forward(&temp_graph_for_pooling)?;
|
|
||||||
|
|
||||||
// Project back to original node dimension
|
|
||||||
let final_node_representations = node_features.matmul(&self.output_projection)?;
|
|
||||||
|
|
||||||
// Combine attention weights if requested
|
|
||||||
let combined_attention_weights = if self.config.return_attention_weights && !all_attention_weights.is_empty() {
|
|
||||||
Some(self.combine_attention_weights(all_attention_weights, graph.batch_size)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(GraphOutput {
|
|
||||||
node_representations: final_node_representations,
|
|
||||||
graph_representations,
|
|
||||||
attention_weights: combined_attention_weights,
|
|
||||||
edge_representations: edge_features,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layer_type(&self) -> &'static str {
|
|
||||||
"GraphTransformer"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
let mut params = vec![&self.node_embedding, &self.output_projection];
|
|
||||||
|
|
||||||
if let Some(ref edge_emb) = self.edge_embedding {
|
|
||||||
params.push(edge_emb);
|
|
||||||
}
|
|
||||||
|
|
||||||
for norm_layer in &self.norm_layers {
|
|
||||||
params.push(norm_layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add attention layer parameters
|
|
||||||
for attention_layer in &self.attention_layers {
|
|
||||||
params.extend(attention_layer.parameters());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add pooling layer parameters
|
|
||||||
params.extend(self.pooling_layer.parameters());
|
|
||||||
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
let mut params = vec![&mut self.node_embedding, &mut self.output_projection];
|
|
||||||
|
|
||||||
if let Some(ref mut edge_emb) = self.edge_embedding {
|
|
||||||
params.push(edge_emb);
|
|
||||||
}
|
|
||||||
|
|
||||||
for norm_layer in &mut self.norm_layers {
|
|
||||||
params.push(norm_layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add attention layer parameters
|
|
||||||
for attention_layer in &mut self.attention_layers {
|
|
||||||
params.extend(attention_layer.parameters_mut());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add pooling layer parameters
|
|
||||||
params.extend(self.pooling_layer.parameters_mut());
|
|
||||||
|
|
||||||
params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphTransformer {
|
|
||||||
/// Apply layer normalization
|
|
||||||
fn apply_layer_norm(&self, input: &Tensor, layer_idx: usize) -> Result<Tensor> {
|
|
||||||
// Simple layer normalization implementation
|
|
||||||
let mean = input.mean_dim(&[-1], true)?;
|
|
||||||
let variance = input.var_dim(&[-1], true, true)?;
|
|
||||||
let normalized = input.sub(&mean)?.div(&(variance.add(&Tensor::full(&[], (self.config.layer_norm_eps) as f32, &&self.device)?))?.sqrt()?)?;
|
|
||||||
|
|
||||||
let weight = &self.norm_layers[layer_idx];
|
|
||||||
normalized.mul(weight)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Combine attention weights from all layers
|
|
||||||
fn combine_attention_weights(&self, weights: Vec<Tensor>, batch_size: usize) -> Result<Tensor> {
|
|
||||||
// Stack attention weights from all layers
|
|
||||||
// Expected shape: [num_layers, batch_size, num_heads, max_nodes, max_nodes]
|
|
||||||
|
|
||||||
if weights.is_empty() {
|
|
||||||
return Err(TransformerError::InvalidInput(
|
|
||||||
"No attention weights to combine".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, return the last layer's attention weights
|
|
||||||
// In a full implementation, we would stack all layers
|
|
||||||
Ok(weights.into_iter().last().unwrap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layer for GraphTransformer {
|
|
||||||
fn forward(&self, input: &Tensor) -> Result<Tensor> {
|
|
||||||
Err(TransformerError::InvalidInput(
|
|
||||||
"GraphTransformer requires GraphBatch input, not Tensor. Use GraphLayer::forward instead.".to_string()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layer_type(&self) -> &'static str {
|
|
||||||
"GraphTransformer"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
GraphLayer::parameters(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
GraphLayer::parameters_mut(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward declarations for components that will be implemented in other files
|
|
||||||
use crate::graph::graph_attention::{GraphAttention, GraphAttentionConfig};
|
|
||||||
use crate::graph::graph_pooling::{GraphPooling, GlobalPooling, HierarchicalPooling, Set2SetPooling, AttentionPooling};
|
|
||||||
use crate::graph::positional_encoding::{GraphPositionalEncoding, GraphPEConfig, GraphPEType};
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
//! Graph Positional Encoding implementations
|
|
||||||
//!
|
|
||||||
//! Provides multiple strategies for encoding positional information in graphs:
|
|
||||||
//! - Laplacian eigenvector-based encoding
|
|
||||||
//! - Random walk-based encoding
|
|
||||||
//! - Learned positional encoding
|
|
||||||
//! - Distance-based encoding
|
|
||||||
|
|
||||||
use crate::{Result, TransformerError};
|
|
||||||
use crate::graph::GraphBatch;
|
|
||||||
use rtx_tensor::{Tensor, Device};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Graph positional encoding types
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum GraphPEType {
|
|
||||||
/// Laplacian eigenvector-based encoding
|
|
||||||
Laplacian,
|
|
||||||
/// Random walk-based encoding
|
|
||||||
RandomWalk,
|
|
||||||
/// Learned positional encoding
|
|
||||||
Learned,
|
|
||||||
/// Shortest path distance-based encoding
|
|
||||||
Distance,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Graph positional encoding configuration
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct GraphPEConfig {
|
|
||||||
/// Embedding dimension
|
|
||||||
pub embedding_dim: usize,
|
|
||||||
/// Type of positional encoding
|
|
||||||
pub pe_type: GraphPEType,
|
|
||||||
/// Number of eigenvectors to use (for Laplacian)
|
|
||||||
pub num_eigenvectors: usize,
|
|
||||||
/// Random walk length (for RandomWalk)
|
|
||||||
pub walk_length: usize,
|
|
||||||
/// Maximum number of nodes (for Learned)
|
|
||||||
pub max_nodes: usize,
|
|
||||||
/// Maximum distance (for Distance-based)
|
|
||||||
pub max_distance: usize,
|
|
||||||
/// Whether to normalize encodings
|
|
||||||
pub normalize: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphPEConfig {
|
|
||||||
/// Create new graph PE configuration
|
|
||||||
pub fn new(embedding_dim: usize, pe_type: GraphPEType) -> Self {
|
|
||||||
Self {
|
|
||||||
embedding_dim,
|
|
||||||
pe_type,
|
|
||||||
num_eigenvectors: 16,
|
|
||||||
walk_length: 10,
|
|
||||||
max_nodes: 1000,
|
|
||||||
max_distance: 10,
|
|
||||||
normalize: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set number of eigenvectors
|
|
||||||
pub fn with_num_eigenvectors(mut self, num_eigenvectors: usize) -> Self {
|
|
||||||
self.num_eigenvectors = num_eigenvectors;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set random walk length
|
|
||||||
pub fn with_walk_length(mut self, walk_length: usize) -> Self {
|
|
||||||
self.walk_length = walk_length;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set maximum nodes
|
|
||||||
pub fn with_max_nodes(mut self, max_nodes: usize) -> Self {
|
|
||||||
self.max_nodes = max_nodes;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Graph positional encoding implementation
|
|
||||||
pub struct GraphPositionalEncoding {
|
|
||||||
config: GraphPEConfig,
|
|
||||||
device: Device,
|
|
||||||
|
|
||||||
// Encoding parameters
|
|
||||||
encoding_weights: Option<Tensor>, // For learned encodings
|
|
||||||
projection_layer: Tensor, // Project PE to embedding_dim
|
|
||||||
|
|
||||||
// Precomputed encodings (for efficiency)
|
|
||||||
cached_encodings: Option<Tensor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphPositionalEncoding {
|
|
||||||
/// Create new graph positional encoding
|
|
||||||
pub fn new(config: GraphPEConfig, device: &Device) -> Result<Self> {
|
|
||||||
// Initialize projection layer based on PE type
|
|
||||||
let projection_input_dim = match config.pe_type {
|
|
||||||
GraphPEType::Laplacian => config.num_eigenvectors,
|
|
||||||
GraphPEType::RandomWalk => config.walk_length,
|
|
||||||
GraphPEType::Learned => config.embedding_dim,
|
|
||||||
GraphPEType::Distance => config.max_distance,
|
|
||||||
};
|
|
||||||
|
|
||||||
let projection_layer = Tensor::randn(&[projection_input_dim, config.embedding_dim], device)?;
|
|
||||||
|
|
||||||
// Initialize learned encoding weights if needed
|
|
||||||
let encoding_weights = if config.pe_type == GraphPEType::Learned {
|
|
||||||
Some(Tensor::randn(&[config.max_nodes, config.embedding_dim], device)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
encoding_weights,
|
|
||||||
projection_layer,
|
|
||||||
cached_encodings: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the configuration
|
|
||||||
pub fn config(&self) -> &GraphPEConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Forward pass - compute positional encodings for graph batch
|
|
||||||
pub fn forward(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
match self.config.pe_type {
|
|
||||||
GraphPEType::Laplacian => self.laplacian_encoding(graph),
|
|
||||||
GraphPEType::RandomWalk => self.random_walk_encoding(graph),
|
|
||||||
GraphPEType::Learned => self.learned_encoding(graph),
|
|
||||||
GraphPEType::Distance => self.distance_encoding(graph),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute Laplacian eigenvector-based positional encoding
|
|
||||||
fn laplacian_encoding(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_nodes = graph.num_nodes();
|
|
||||||
|
|
||||||
// Construct graph Laplacian matrix
|
|
||||||
let laplacian = self.compute_graph_laplacian(graph)?; // [num_nodes, num_nodes]
|
|
||||||
|
|
||||||
// Compute eigenvectors (simplified - in practice would use proper eigendecomposition)
|
|
||||||
// For now, we'll create a placeholder that has the right shape
|
|
||||||
let eigenvalues = Tensor::randn(&[num_nodes, self.config.num_eigenvectors], &self.device)?;
|
|
||||||
|
|
||||||
// Select top-k eigenvectors (excluding the first constant eigenvector)
|
|
||||||
let pe_features = if self.config.num_eigenvectors < num_nodes {
|
|
||||||
eigenvalues.narrow(1, 1, self.config.num_eigenvectors)? // Skip first eigenvector
|
|
||||||
} else {
|
|
||||||
eigenvalues.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Project to embedding dimension
|
|
||||||
let projected = pe_features.matmul(&self.projection_layer)?;
|
|
||||||
|
|
||||||
// Normalize if requested
|
|
||||||
if self.config.normalize {
|
|
||||||
let norm = projected.norm_dim(&[1], true, true)?;
|
|
||||||
projected.div(&(norm.add(&Tensor::full(&[], (1e-8) as f32, &&self.device)?))?)?
|
|
||||||
} else {
|
|
||||||
projected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute random walk-based positional encoding
|
|
||||||
fn random_walk_encoding(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_nodes = graph.num_nodes();
|
|
||||||
|
|
||||||
// Construct adjacency matrix
|
|
||||||
let adjacency = self.compute_adjacency_matrix(graph)?; // [num_nodes, num_nodes]
|
|
||||||
|
|
||||||
// Compute transition matrix (row-normalized adjacency)
|
|
||||||
let degree = adjacency.sum_dim(&[1], true)?; // [num_nodes, 1]
|
|
||||||
let degree_inv = degree.reciprocal()?;
|
|
||||||
let transition_matrix = adjacency.mul(°ree_inv)?;
|
|
||||||
|
|
||||||
// Compute random walk probabilities for different steps
|
|
||||||
let mut walk_features = Vec::new();
|
|
||||||
let mut current_probs = Tensor::eye(num_nodes, &self.device)?; // Identity matrix
|
|
||||||
|
|
||||||
for _ in 0..self.config.walk_length {
|
|
||||||
current_probs = current_probs.matmul(&transition_matrix)?;
|
|
||||||
// Use diagonal values as features (probability of returning to starting node)
|
|
||||||
let diag = current_probs.diag()?; // [num_nodes]
|
|
||||||
walk_features.push(diag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stack walk features
|
|
||||||
let stacked_features = Tensor::stack(&walk_features, 1)?; // [num_nodes, walk_length]
|
|
||||||
|
|
||||||
// Project to embedding dimension
|
|
||||||
let projected = stacked_features.matmul(&self.projection_layer)?;
|
|
||||||
|
|
||||||
// Normalize if requested
|
|
||||||
if self.config.normalize {
|
|
||||||
let norm = projected.norm_dim(&[1], true, true)?;
|
|
||||||
projected.div(&(norm.add(&Tensor::full(&[], (1e-8) as f32, &&self.device)?))?)?
|
|
||||||
} else {
|
|
||||||
projected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute learned positional encoding
|
|
||||||
fn learned_encoding(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_nodes = graph.num_nodes();
|
|
||||||
|
|
||||||
if let Some(ref weights) = self.encoding_weights {
|
|
||||||
// Simple learned encoding based on node indices
|
|
||||||
let node_indices = Tensor::arange(0, num_nodes as i64, &self.device)?;
|
|
||||||
let encodings = weights.index_select(0, &node_indices)?;
|
|
||||||
|
|
||||||
if self.config.normalize {
|
|
||||||
let norm = encodings.norm_dim(&[1], true, true)?;
|
|
||||||
encodings.div(&(norm.add(&Tensor::full(&[], (1e-8) as f32, &&self.device)?))?)?
|
|
||||||
} else {
|
|
||||||
encodings
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(TransformerError::InvalidInput(
|
|
||||||
"Encoding weights not initialized for learned PE".to_string()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute distance-based positional encoding
|
|
||||||
fn distance_encoding(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_nodes = graph.num_nodes();
|
|
||||||
|
|
||||||
// Compute shortest path distances (simplified implementation)
|
|
||||||
let distance_matrix = self.compute_shortest_paths(graph)?; // [num_nodes, num_nodes]
|
|
||||||
|
|
||||||
// Create distance-based features
|
|
||||||
// For each node, create a histogram of distances to other nodes
|
|
||||||
let mut distance_features = Vec::new();
|
|
||||||
|
|
||||||
for dist in 1..=self.config.max_distance {
|
|
||||||
let dist_tensor = Tensor::full(&[], (dist as f32) as f32, &&self.device)?;
|
|
||||||
let mask = distance_matrix.eq(&dist_tensor)?;
|
|
||||||
let count = mask.sum_dim(&[1], false)?.to_dtype(rtx_tensor::DType::F32)?;
|
|
||||||
distance_features.push(count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stack distance features
|
|
||||||
let stacked_features = Tensor::stack(&distance_features, 1)?; // [num_nodes, max_distance]
|
|
||||||
|
|
||||||
// Project to embedding dimension
|
|
||||||
let projected = stacked_features.matmul(&self.projection_layer)?;
|
|
||||||
|
|
||||||
// Normalize if requested
|
|
||||||
if self.config.normalize {
|
|
||||||
let norm = projected.norm_dim(&[1], true, true)?;
|
|
||||||
projected.div(&(norm.add(&Tensor::full(&[], (1e-8) as f32, &&self.device)?))?)?
|
|
||||||
} else {
|
|
||||||
projected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute graph Laplacian matrix
|
|
||||||
fn compute_graph_laplacian(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_nodes = graph.num_nodes();
|
|
||||||
|
|
||||||
// Construct adjacency matrix
|
|
||||||
let adjacency = self.compute_adjacency_matrix(graph)?;
|
|
||||||
|
|
||||||
// Compute degree matrix
|
|
||||||
let degree_values = adjacency.sum_dim(&[1], false)?; // [num_nodes]
|
|
||||||
let degree_matrix = Tensor::diag(°ree_values)?; // [num_nodes, num_nodes]
|
|
||||||
|
|
||||||
// Laplacian = D - A
|
|
||||||
degree_matrix.sub(&adjacency)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute adjacency matrix from edge indices
|
|
||||||
fn compute_adjacency_matrix(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_nodes = graph.num_nodes();
|
|
||||||
let num_edges = graph.num_edges();
|
|
||||||
|
|
||||||
// Initialize adjacency matrix
|
|
||||||
let mut adjacency = Tensor::zeros([num_nodes, num_nodes], &self.device)?;
|
|
||||||
|
|
||||||
// Fill adjacency matrix based on edges
|
|
||||||
// This is a simplified implementation - in practice would use scatter operations
|
|
||||||
let source_indices = graph.edge_indices.get(&[0])?; // [num_edges]
|
|
||||||
let target_indices = graph.edge_indices.get(&[1])?; // [num_edges]
|
|
||||||
|
|
||||||
// For now, create a simple symmetric adjacency matrix
|
|
||||||
for i in 0..num_edges {
|
|
||||||
// In a real implementation, we would set adjacency[source[i], target[i]] = 1
|
|
||||||
// This is a placeholder implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return identity + small random values as placeholder
|
|
||||||
let identity = Tensor::eye(num_nodes, &self.device)?;
|
|
||||||
let noise = Tensor::randn(&[num_nodes, num_nodes], &self.device)?.mul(&Tensor::full(&[], 0.1, &self.device))?;
|
|
||||||
identity.add(&noise)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute shortest path distances (simplified Floyd-Warshall-like)
|
|
||||||
fn compute_shortest_paths(&self, graph: &GraphBatch) -> Result<Tensor> {
|
|
||||||
let num_nodes = graph.num_nodes();
|
|
||||||
|
|
||||||
// Initialize distance matrix with large values
|
|
||||||
let mut distances = Tensor::full([num_nodes, num_nodes], f32::INFINITY, &self.device)?;
|
|
||||||
|
|
||||||
// Set diagonal to 0
|
|
||||||
let diagonal_indices = Tensor::arange(0, num_nodes as i64, &self.device)?;
|
|
||||||
// distances[diagonal_indices, diagonal_indices] = 0 (simplified)
|
|
||||||
|
|
||||||
// Set direct edges to distance 1
|
|
||||||
// This would require scatter operations in a full implementation
|
|
||||||
|
|
||||||
// For now, return a simple distance matrix as placeholder
|
|
||||||
let identity = Tensor::eye(num_nodes, &self.device)?;
|
|
||||||
let ones = Tensor::ones([num_nodes, num_nodes], &self.device)?;
|
|
||||||
identity.add(&ones) // Distance matrix with 1s for neighbors, 2s otherwise
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get parameters for optimization
|
|
||||||
pub fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
let mut params = vec![&self.projection_layer];
|
|
||||||
|
|
||||||
if let Some(ref encoding_weights) = self.encoding_weights {
|
|
||||||
params.push(encoding_weights);
|
|
||||||
}
|
|
||||||
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get mutable parameters for optimization
|
|
||||||
pub fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
let mut params = vec![&mut self.projection_layer];
|
|
||||||
|
|
||||||
if let Some(ref mut encoding_weights) = self.encoding_weights {
|
|
||||||
params.push(encoding_weights);
|
|
||||||
}
|
|
||||||
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get device
|
|
||||||
pub fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -48,8 +48,6 @@ pub mod sparse_attention;
|
|||||||
// MoE (Mixture of Experts)
|
// MoE (Mixture of Experts)
|
||||||
pub mod metal_moe;
|
pub mod metal_moe;
|
||||||
pub mod mixture_of_experts;
|
pub mod mixture_of_experts;
|
||||||
// pub mod moe_layer;
|
|
||||||
// pub mod moe_integration;
|
|
||||||
// pub mod moe_routing;
|
// pub mod moe_routing;
|
||||||
// pub mod capacity_tuning;
|
// pub mod capacity_tuning;
|
||||||
// pub mod expert_dropout;
|
// pub mod expert_dropout;
|
||||||
|
|||||||
@@ -1,564 +0,0 @@
|
|||||||
//! Integration layer for MoE with existing transformer architectures
|
|
||||||
|
|
||||||
use crate::{Result, TransformerError};
|
|
||||||
use crate::layers::{Layer, MoEConfig, MoELayer, MoEOutput};
|
|
||||||
use crate::architectures::TransformerConfig;
|
|
||||||
use rtx_tensor::{Tensor, Device, DType};
|
|
||||||
use rtx_autograd::TensorAutograd;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Configuration for MoE-enabled transformer layers
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct MoETransformerConfig {
|
|
||||||
/// Base transformer configuration
|
|
||||||
pub transformer_config: TransformerConfig,
|
|
||||||
/// MoE configuration (replaces traditional FFN)
|
|
||||||
pub moe_config: MoEConfig,
|
|
||||||
/// Whether to use MoE in all layers or only specific ones
|
|
||||||
pub moe_layers: Vec<usize>, // Layer indices that should use MoE
|
|
||||||
/// Whether to keep traditional FFN alongside MoE (hybrid mode)
|
|
||||||
pub hybrid_mode: bool,
|
|
||||||
/// Weight for combining traditional FFN with MoE in hybrid mode
|
|
||||||
pub hybrid_weight: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MoETransformerConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
transformer_config: TransformerConfig::default(),
|
|
||||||
moe_config: MoEConfig::default(),
|
|
||||||
moe_layers: vec![],
|
|
||||||
hybrid_mode: false,
|
|
||||||
hybrid_weight: 0.5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MoETransformerConfig {
|
|
||||||
/// Create a new MoE transformer configuration
|
|
||||||
pub fn new(transformer_config: TransformerConfig, moe_config: MoEConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
transformer_config,
|
|
||||||
moe_config,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable MoE in all transformer layers
|
|
||||||
pub fn enable_moe_all_layers(&mut self) {
|
|
||||||
self.moe_layers = (0..self.transformer_config.num_layers).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable MoE in specific layers
|
|
||||||
pub fn enable_moe_layers(&mut self, layer_indices: Vec<usize>) -> Result<()> {
|
|
||||||
for &idx in &layer_indices {
|
|
||||||
if idx >= self.transformer_config.num_layers {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
format!("MoE layer index {} exceeds total layers {}",
|
|
||||||
idx, self.transformer_config.num_layers)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.moe_layers = layer_indices;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable hybrid mode with traditional FFN + MoE
|
|
||||||
pub fn enable_hybrid_mode(&mut self, hybrid_weight: f32) -> Result<()> {
|
|
||||||
if !(0.0..=1.0).contains(&hybrid_weight) {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
"hybrid_weight must be between 0.0 and 1.0".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self.hybrid_mode = true;
|
|
||||||
self.hybrid_weight = hybrid_weight;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a given layer should use MoE
|
|
||||||
pub fn is_moe_layer(&self, layer_index: usize) -> bool {
|
|
||||||
self.moe_layers.contains(&layer_index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate the configuration
|
|
||||||
pub fn validate(&self) -> Result<()> {
|
|
||||||
self.transformer_config.validate()?;
|
|
||||||
self.moe_config.validate()?;
|
|
||||||
|
|
||||||
// Ensure MoE hidden dimensions match transformer
|
|
||||||
if self.moe_config.hidden_dim != self.transformer_config.d_model {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
format!("MoE hidden_dim ({}) must match transformer d_model ({})",
|
|
||||||
self.moe_config.hidden_dim, self.transformer_config.d_model)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check layer indices
|
|
||||||
for &idx in &self.moe_layers {
|
|
||||||
if idx >= self.transformer_config.num_layers {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
format!("MoE layer index {} exceeds total layers {}",
|
|
||||||
idx, self.transformer_config.num_layers)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate hybrid mode settings
|
|
||||||
if self.hybrid_mode && !(0.0..=1.0).contains(&self.hybrid_weight) {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
"hybrid_weight must be between 0.0 and 1.0".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Estimate total memory usage
|
|
||||||
pub fn estimate_memory_usage(&self, batch_size: usize, seq_len: usize) -> usize {
|
|
||||||
let base_memory = self.transformer_config.estimate_kv_cache_memory(seq_len);
|
|
||||||
let moe_memory = self.moe_config.estimate_memory_usage(batch_size, seq_len);
|
|
||||||
|
|
||||||
// Memory for MoE layers
|
|
||||||
let moe_layer_memory = moe_memory * self.moe_layers.len();
|
|
||||||
|
|
||||||
base_memory + moe_layer_memory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MoE-enabled feedforward layer that can replace traditional FFN
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MoEFeedForward {
|
|
||||||
/// Configuration
|
|
||||||
config: MoETransformerConfig,
|
|
||||||
/// Device
|
|
||||||
device: Device,
|
|
||||||
/// MoE layer
|
|
||||||
moe_layer: MoELayer,
|
|
||||||
/// Traditional FFN for hybrid mode (optional)
|
|
||||||
traditional_ffn: Option<TraditionalFFN>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MoEFeedForward {
|
|
||||||
/// Create a new MoE feedforward layer
|
|
||||||
pub fn new(config: MoETransformerConfig, device: &Device) -> Result<Self> {
|
|
||||||
config.validate()?;
|
|
||||||
|
|
||||||
let moe_layer = MoELayer::new(config.moe_config.clone(), device)?;
|
|
||||||
|
|
||||||
let traditional_ffn = if config.hybrid_mode {
|
|
||||||
Some(TraditionalFFN::new(
|
|
||||||
config.transformer_config.d_model,
|
|
||||||
config.transformer_config.d_ff,
|
|
||||||
&config.transformer_config.activation,
|
|
||||||
config.transformer_config.bias,
|
|
||||||
device
|
|
||||||
)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
moe_layer,
|
|
||||||
traditional_ffn,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Forward pass with optional auxiliary loss output
|
|
||||||
pub fn forward_with_aux(&self, input: &Tensor) -> Result<(Tensor, Option<Tensor>)> {
|
|
||||||
let moe_output = self.moe_layer.forward(input)?;
|
|
||||||
|
|
||||||
let output = if let Some(ref traditional_ffn) = self.traditional_ffn {
|
|
||||||
// Hybrid mode: combine MoE and traditional FFN
|
|
||||||
let ffn_output = traditional_ffn.forward(input)?;
|
|
||||||
let moe_weighted = moe_output.output.mul_scalar(self.config.hybrid_weight)?;
|
|
||||||
let ffn_weighted = ffn_output.mul_scalar(1.0 - self.config.hybrid_weight)?;
|
|
||||||
moe_weighted.add(&ffn_weighted)?
|
|
||||||
} else {
|
|
||||||
// Pure MoE mode
|
|
||||||
moe_output.output
|
|
||||||
};
|
|
||||||
|
|
||||||
let aux_loss = moe_output.aux_loss().cloned();
|
|
||||||
Ok((output, aux_loss))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get MoE output with full statistics
|
|
||||||
pub fn forward_detailed(&self, input: &Tensor) -> Result<MoEDetailedOutput> {
|
|
||||||
let moe_output = self.moe_layer.forward(input)?;
|
|
||||||
|
|
||||||
let final_output = if let Some(ref traditional_ffn) = self.traditional_ffn {
|
|
||||||
// Hybrid mode
|
|
||||||
let ffn_output = traditional_ffn.forward(input)?;
|
|
||||||
let moe_weighted = moe_output.output.mul_scalar(self.config.hybrid_weight)?;
|
|
||||||
let ffn_weighted = ffn_output.mul_scalar(1.0 - self.config.hybrid_weight)?;
|
|
||||||
let combined = moe_weighted.add(&ffn_weighted)?;
|
|
||||||
|
|
||||||
Some(MoEDetailedOutput {
|
|
||||||
output: combined,
|
|
||||||
moe_output: moe_output.output,
|
|
||||||
ffn_output: Some(ffn_output),
|
|
||||||
aux_loss: moe_output.aux_loss().cloned(),
|
|
||||||
routing_info: moe_output.routing_info,
|
|
||||||
load_stats: moe_output.load_stats,
|
|
||||||
is_load_balanced: moe_output.is_load_balanced(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Pure MoE mode
|
|
||||||
Some(MoEDetailedOutput {
|
|
||||||
output: moe_output.output.clone(),
|
|
||||||
moe_output: moe_output.output,
|
|
||||||
ffn_output: None,
|
|
||||||
aux_loss: moe_output.aux_loss().cloned(),
|
|
||||||
routing_info: moe_output.routing_info,
|
|
||||||
load_stats: moe_output.load_stats,
|
|
||||||
is_load_balanced: moe_output.is_load_balanced(),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
final_output.ok_or_else(|| TransformerError::runtime("Failed to create detailed output".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all parameters including MoE and optional traditional FFN
|
|
||||||
pub fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
let mut params = self.moe_layer.parameters();
|
|
||||||
if let Some(ref ffn) = self.traditional_ffn {
|
|
||||||
params.extend(ffn.parameters());
|
|
||||||
}
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all mutable parameters
|
|
||||||
pub fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
let mut params = self.moe_layer.parameters_mut();
|
|
||||||
if let Some(ref mut ffn) = self.traditional_ffn {
|
|
||||||
params.extend(ffn.parameters_mut());
|
|
||||||
}
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get configuration
|
|
||||||
pub fn config(&self) -> &MoETransformerConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layer for MoEFeedForward {
|
|
||||||
fn forward(&self, input: &Tensor) -> Result<Tensor> {
|
|
||||||
let (output, _aux_loss) = self.forward_with_aux(input)?;
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layer_type(&self) -> &'static str {
|
|
||||||
if self.traditional_ffn.is_some() {
|
|
||||||
"HybridMoEFeedForward"
|
|
||||||
} else {
|
|
||||||
"MoEFeedForward"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
self.parameters()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
self.parameters_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Traditional feedforward network for hybrid mode
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct TraditionalFFN {
|
|
||||||
up_weight: Tensor,
|
|
||||||
up_bias: Option<Tensor>,
|
|
||||||
down_weight: Tensor,
|
|
||||||
down_bias: Option<Tensor>,
|
|
||||||
activation: String,
|
|
||||||
device: Device,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TraditionalFFN {
|
|
||||||
fn new(
|
|
||||||
hidden_dim: usize,
|
|
||||||
intermediate_dim: usize,
|
|
||||||
activation: &str,
|
|
||||||
use_bias: bool,
|
|
||||||
device: &Device,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let up_weight = Tensor::randn(&[intermediate_dim, hidden_dim], DType::F32, device)?
|
|
||||||
.require_grad()?;
|
|
||||||
let up_bias = if use_bias {
|
|
||||||
Some(Tensor::zeros(&[intermediate_dim], device)?.require_grad())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let down_weight = Tensor::randn(&[hidden_dim, intermediate_dim], DType::F32, device)?
|
|
||||||
.require_grad()?;
|
|
||||||
let down_bias = if use_bias {
|
|
||||||
Some(Tensor::zeros(&[hidden_dim], device)?.require_grad())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
up_weight,
|
|
||||||
up_bias,
|
|
||||||
down_weight,
|
|
||||||
down_bias,
|
|
||||||
activation: activation.to_string(),
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forward(&self, input: &Tensor) -> Result<Tensor> {
|
|
||||||
// Up projection
|
|
||||||
let up_output = input.matmul(&self.up_weight.t()?)?;
|
|
||||||
let up_output = if let Some(ref bias) = self.up_bias {
|
|
||||||
up_output.add(bias)?
|
|
||||||
} else {
|
|
||||||
up_output
|
|
||||||
};
|
|
||||||
|
|
||||||
// Activation
|
|
||||||
let activated = self.apply_activation(&up_output)?;
|
|
||||||
|
|
||||||
// Down projection
|
|
||||||
let down_output = activated.matmul(&self.down_weight.t()?)?;
|
|
||||||
let output = if let Some(ref bias) = self.down_bias {
|
|
||||||
down_output.add(bias)?
|
|
||||||
} else {
|
|
||||||
down_output
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_activation(&self, input: &Tensor) -> Result<Tensor> {
|
|
||||||
match self.activation.as_str() {
|
|
||||||
"relu" => input.relu(),
|
|
||||||
"gelu" => input.gelu(),
|
|
||||||
"swish" | "silu" => input.swish(),
|
|
||||||
"tanh" => input.tanh(),
|
|
||||||
_ => Err(TransformerError::config(
|
|
||||||
format!("Unsupported activation function: {}", self.activation)
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
let mut params = vec![&self.up_weight, &self.down_weight];
|
|
||||||
if let Some(ref bias) = self.up_bias {
|
|
||||||
params.push(bias);
|
|
||||||
}
|
|
||||||
if let Some(ref bias) = self.down_bias {
|
|
||||||
params.push(bias);
|
|
||||||
}
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
let mut params = vec![&mut self.up_weight, &mut self.down_weight];
|
|
||||||
if let Some(ref mut bias) = self.up_bias {
|
|
||||||
params.push(bias);
|
|
||||||
}
|
|
||||||
if let Some(ref mut bias) = self.down_bias {
|
|
||||||
params.push(bias);
|
|
||||||
}
|
|
||||||
params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detailed output from MoE feedforward layer
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MoEDetailedOutput {
|
|
||||||
/// Final combined output
|
|
||||||
pub output: Tensor,
|
|
||||||
/// MoE component output
|
|
||||||
pub moe_output: Tensor,
|
|
||||||
/// Traditional FFN output (in hybrid mode)
|
|
||||||
pub ffn_output: Option<Tensor>,
|
|
||||||
/// Auxiliary loss for training
|
|
||||||
pub aux_loss: Option<Tensor>,
|
|
||||||
/// Routing information
|
|
||||||
pub routing_info: crate::layers::RoutingInfo,
|
|
||||||
/// Load balancing statistics
|
|
||||||
pub load_stats: crate::layers::LoadBalancingStats,
|
|
||||||
/// Whether the load is balanced
|
|
||||||
pub is_load_balanced: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for TransformerConfig validation
|
|
||||||
impl TransformerConfig {
|
|
||||||
/// Validate transformer configuration
|
|
||||||
pub fn validate(&self) -> Result<()> {
|
|
||||||
if self.vocab_size == 0 {
|
|
||||||
return Err(TransformerError::config("vocab_size must be greater than 0".to_string()));
|
|
||||||
}
|
|
||||||
if self.d_model == 0 {
|
|
||||||
return Err(TransformerError::config("d_model must be greater than 0".to_string()));
|
|
||||||
}
|
|
||||||
if self.num_layers == 0 {
|
|
||||||
return Err(TransformerError::config("num_layers must be greater than 0".to_string()));
|
|
||||||
}
|
|
||||||
if self.num_heads == 0 {
|
|
||||||
return Err(TransformerError::config("num_heads must be greater than 0".to_string()));
|
|
||||||
}
|
|
||||||
if self.d_model % self.num_heads != 0 {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
format!("d_model ({}) must be divisible by num_heads ({})",
|
|
||||||
self.d_model, self.num_heads)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_transformer_config() {
|
|
||||||
let transformer_config = TransformerConfig::new(10000, 768, 12, 12, 3072, 1024);
|
|
||||||
let moe_config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
let mut config = MoETransformerConfig::new(transformer_config, moe_config);
|
|
||||||
|
|
||||||
assert!(config.validate().is_ok());
|
|
||||||
|
|
||||||
// Test enabling MoE in all layers
|
|
||||||
config.enable_moe_all_layers();
|
|
||||||
assert_eq!(config.moe_layers.len(), 12);
|
|
||||||
|
|
||||||
// Test enabling MoE in specific layers
|
|
||||||
config.enable_moe_layers(vec![0, 2, 4, 6]).unwrap();
|
|
||||||
assert_eq!(config.moe_layers, vec![0, 2, 4, 6]);
|
|
||||||
|
|
||||||
// Test hybrid mode
|
|
||||||
config.enable_hybrid_mode(0.7).unwrap();
|
|
||||||
assert!(config.hybrid_mode);
|
|
||||||
assert_eq!(config.hybrid_weight, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_transformer_config_validation() {
|
|
||||||
let transformer_config = TransformerConfig::new(10000, 768, 12, 12, 3072, 1024);
|
|
||||||
let mut moe_config = MoEConfig::new(8, 2, 512, 3072); // Wrong hidden_dim
|
|
||||||
let config = MoETransformerConfig::new(transformer_config, moe_config);
|
|
||||||
|
|
||||||
// Should fail due to mismatched dimensions
|
|
||||||
assert!(config.validate().is_err());
|
|
||||||
|
|
||||||
// Fix dimensions
|
|
||||||
moe_config.hidden_dim = 768;
|
|
||||||
let mut config = MoETransformerConfig::new(transformer_config, moe_config);
|
|
||||||
|
|
||||||
// Should pass now
|
|
||||||
assert!(config.validate().is_ok());
|
|
||||||
|
|
||||||
// Test invalid layer indices
|
|
||||||
assert!(config.enable_moe_layers(vec![15]).is_err()); // Layer 15 doesn't exist
|
|
||||||
|
|
||||||
// Test invalid hybrid weight
|
|
||||||
assert!(config.enable_hybrid_mode(1.5).is_err());
|
|
||||||
assert!(config.enable_hybrid_mode(-0.1).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_feedforward_creation() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let transformer_config = TransformerConfig::new(10000, 512, 6, 8, 2048, 512);
|
|
||||||
let moe_config = MoEConfig::new(4, 2, 512, 2048);
|
|
||||||
let config = MoETransformerConfig::new(transformer_config, moe_config);
|
|
||||||
|
|
||||||
let moe_ff = MoEFeedForward::new(config, &device);
|
|
||||||
assert!(moe_ff.is_ok());
|
|
||||||
|
|
||||||
let moe_ff = moe_ff.unwrap();
|
|
||||||
assert_eq!(moe_ff.layer_type(), "MoEFeedForward");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_feedforward_hybrid_mode() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let transformer_config = TransformerConfig::new(10000, 512, 6, 8, 2048, 512);
|
|
||||||
let moe_config = MoEConfig::new(4, 2, 512, 2048);
|
|
||||||
let mut config = MoETransformerConfig::new(transformer_config, moe_config);
|
|
||||||
config.enable_hybrid_mode(0.6).unwrap();
|
|
||||||
|
|
||||||
let moe_ff = MoEFeedForward::new(config, &device).unwrap();
|
|
||||||
assert_eq!(moe_ff.layer_type(), "HybridMoEFeedForward");
|
|
||||||
|
|
||||||
// Test forward pass
|
|
||||||
let input = Tensor::randn(&[2, 8, 512], DType::F32, &device).unwrap();
|
|
||||||
let (output, aux_loss) = moe_ff.forward_with_aux(&input).unwrap();
|
|
||||||
|
|
||||||
// Output shape should match input
|
|
||||||
assert_eq!(output.shape(), input.shape());
|
|
||||||
|
|
||||||
// Should have auxiliary loss
|
|
||||||
assert!(aux_loss.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_feedforward_detailed_output() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let transformer_config = TransformerConfig::new(10000, 256, 4, 4, 1024, 256);
|
|
||||||
let moe_config = MoEConfig::new(4, 2, 256, 1024);
|
|
||||||
let mut config = MoETransformerConfig::new(transformer_config, moe_config);
|
|
||||||
config.enable_hybrid_mode(0.5).unwrap();
|
|
||||||
|
|
||||||
let moe_ff = MoEFeedForward::new(config, &device).unwrap();
|
|
||||||
|
|
||||||
let input = Tensor::randn(&[1, 4, 256], DType::F32, &device).unwrap();
|
|
||||||
let detailed_output = moe_ff.forward_detailed(&input).unwrap();
|
|
||||||
|
|
||||||
// Should have both MoE and FFN outputs in hybrid mode
|
|
||||||
assert!(detailed_output.ffn_output.is_some());
|
|
||||||
|
|
||||||
// Outputs should have correct shapes
|
|
||||||
assert_eq!(detailed_output.output.shape(), input.shape());
|
|
||||||
assert_eq!(detailed_output.moe_output.shape(), input.shape());
|
|
||||||
assert_eq!(detailed_output.ffn_output.as_ref().unwrap().shape(), input.shape());
|
|
||||||
|
|
||||||
// Should have routing information
|
|
||||||
assert_eq!(detailed_output.routing_info.expert_token_counts.len(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_traditional_ffn() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let ffn = TraditionalFFN::new(256, 1024, "gelu", true, &device).unwrap();
|
|
||||||
|
|
||||||
let input = Tensor::randn(&[2, 8, 256], DType::F32, &device).unwrap();
|
|
||||||
let output = ffn.forward(&input).unwrap();
|
|
||||||
|
|
||||||
// Output shape should match input
|
|
||||||
assert_eq!(output.shape(), input.shape());
|
|
||||||
|
|
||||||
// Should have correct number of parameters (with bias)
|
|
||||||
let params = ffn.parameters();
|
|
||||||
assert_eq!(params.len(), 4); // up_weight, down_weight, up_bias, down_bias
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_memory_estimation() {
|
|
||||||
let transformer_config = TransformerConfig::new(10000, 768, 12, 12, 3072, 1024);
|
|
||||||
let moe_config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
let mut config = MoETransformerConfig::new(transformer_config, moe_config);
|
|
||||||
config.enable_moe_layers(vec![0, 2, 4, 6]).unwrap();
|
|
||||||
|
|
||||||
let memory_usage = config.estimate_memory_usage(32, 128);
|
|
||||||
assert!(memory_usage > 0);
|
|
||||||
|
|
||||||
// More MoE layers should use more memory
|
|
||||||
config.enable_moe_all_layers();
|
|
||||||
let full_memory_usage = config.estimate_memory_usage(32, 128);
|
|
||||||
assert!(full_memory_usage > memory_usage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
//! MoE Layer implementation with load balancing and auxiliary losses
|
|
||||||
|
|
||||||
use crate::layers::Layer;
|
|
||||||
use crate::{Result, TransformerError};
|
|
||||||
use crate::layers::mixture_of_experts::{MoEConfig, Router, Expert, RoutingInfo};
|
|
||||||
use rtx_tensor::{Tensor, Device, DType};
|
|
||||||
use rtx_autograd::TensorAutograd;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Load balancer for ensuring even distribution of tokens across experts
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct LoadBalancer {
|
|
||||||
/// Configuration
|
|
||||||
config: MoEConfig,
|
|
||||||
/// Device
|
|
||||||
device: Device,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoadBalancer {
|
|
||||||
/// Create a new load balancer
|
|
||||||
pub fn new(config: MoEConfig, device: &Device) -> Result<Self> {
|
|
||||||
config.validate()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply capacity-based load balancing to routing decisions
|
|
||||||
pub fn apply_capacity_constraints(&self, routing_info: &RoutingInfo, capacity_per_expert: usize) -> Result<RoutingInfo> {
|
|
||||||
// This is a simplified implementation
|
|
||||||
// In practice, we would need to:
|
|
||||||
// 1. Sort tokens by routing weights for each expert
|
|
||||||
// 2. Keep only top tokens up to capacity limit
|
|
||||||
// 3. Reassign overflow tokens to other experts or drop them
|
|
||||||
|
|
||||||
// For now, return the input routing info unchanged
|
|
||||||
Ok(routing_info.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate load balancing statistics
|
|
||||||
pub fn calculate_load_stats(&self, expert_counts: &[usize], total_tokens: usize) -> LoadBalancingStats {
|
|
||||||
if expert_counts.is_empty() || total_tokens == 0 {
|
|
||||||
return LoadBalancingStats::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
let average_load = total_tokens as f32 / self.config.num_experts as f32;
|
|
||||||
let mut imbalance = 0.0;
|
|
||||||
let mut min_load = expert_counts[0] as f32;
|
|
||||||
let mut max_load = expert_counts[0] as f32;
|
|
||||||
|
|
||||||
for &count in expert_counts {
|
|
||||||
let count_f32 = count as f32;
|
|
||||||
min_load = min_load.min(count_f32);
|
|
||||||
max_load = max_load.max(count_f32);
|
|
||||||
imbalance += (count_f32 - average_load).abs();
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadBalancingStats {
|
|
||||||
average_load,
|
|
||||||
min_load,
|
|
||||||
max_load,
|
|
||||||
imbalance_score: imbalance / (self.config.num_experts as f32 * average_load).max(1e-6),
|
|
||||||
expert_utilization: expert_counts.iter().map(|&c| c > 0).count() as f32 / self.config.num_experts as f32,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if load balancing constraints are violated
|
|
||||||
pub fn check_constraints(&self, stats: &LoadBalancingStats) -> bool {
|
|
||||||
// Consider load balanced if imbalance score is below threshold
|
|
||||||
stats.imbalance_score < 0.5 && stats.expert_utilization > 0.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load balancing statistics
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct LoadBalancingStats {
|
|
||||||
/// Average number of tokens per expert
|
|
||||||
pub average_load: f32,
|
|
||||||
/// Minimum tokens assigned to any expert
|
|
||||||
pub min_load: f32,
|
|
||||||
/// Maximum tokens assigned to any expert
|
|
||||||
pub max_load: f32,
|
|
||||||
/// Load imbalance score (0 = perfectly balanced, higher = more imbalanced)
|
|
||||||
pub imbalance_score: f32,
|
|
||||||
/// Fraction of experts that received at least one token
|
|
||||||
pub expert_utilization: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Complete Mixture of Experts layer
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MoELayer {
|
|
||||||
/// Configuration
|
|
||||||
config: MoEConfig,
|
|
||||||
/// Device
|
|
||||||
device: Device,
|
|
||||||
/// Router for top-k gating
|
|
||||||
router: Router,
|
|
||||||
/// Individual expert networks
|
|
||||||
experts: Vec<Expert>,
|
|
||||||
/// Load balancer
|
|
||||||
load_balancer: LoadBalancer,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MoELayer {
|
|
||||||
/// Create a new MoE layer
|
|
||||||
pub fn new(config: MoEConfig, device: &Device) -> Result<Self> {
|
|
||||||
config.validate()?;
|
|
||||||
|
|
||||||
// Create router
|
|
||||||
let router = Router::new(config.clone(), device)?;
|
|
||||||
|
|
||||||
// Create experts
|
|
||||||
let mut experts = Vec::with_capacity(config.num_experts);
|
|
||||||
for _ in 0..config.num_experts {
|
|
||||||
experts.push(Expert::new(config.clone(), device)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create load balancer
|
|
||||||
let load_balancer = LoadBalancer::new(config.clone(), device)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
router,
|
|
||||||
experts,
|
|
||||||
load_balancer,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Forward pass through the MoE layer
|
|
||||||
pub fn forward(&self, input: &Tensor) -> Result<MoEOutput> {
|
|
||||||
let input_shape = input.shape();
|
|
||||||
let batch_size = input_shape[0];
|
|
||||||
let seq_len = input_shape[1];
|
|
||||||
|
|
||||||
// Get routing decisions
|
|
||||||
let routing_info = self.router.route(input)?;
|
|
||||||
|
|
||||||
// Apply capacity constraints
|
|
||||||
let expert_capacity = self.config.calculate_expert_capacity(batch_size, seq_len);
|
|
||||||
let routing_info = self.load_balancer.apply_capacity_constraints(&routing_info, expert_capacity)?;
|
|
||||||
|
|
||||||
// Route tokens to experts and compute outputs
|
|
||||||
let expert_outputs = self.compute_expert_outputs(input, &routing_info)?;
|
|
||||||
|
|
||||||
// Combine expert outputs using routing weights
|
|
||||||
let final_output = self.combine_expert_outputs(&expert_outputs, &routing_info)?;
|
|
||||||
|
|
||||||
// Calculate load balancing statistics
|
|
||||||
let load_stats = self.load_balancer.calculate_load_stats(&routing_info.expert_token_counts, batch_size * seq_len);
|
|
||||||
|
|
||||||
Ok(MoEOutput {
|
|
||||||
output: final_output,
|
|
||||||
routing_info,
|
|
||||||
load_stats,
|
|
||||||
expert_outputs: Some(expert_outputs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute outputs from all experts
|
|
||||||
fn compute_expert_outputs(&self, input: &Tensor, routing_info: &RoutingInfo) -> Result<Vec<Tensor>> {
|
|
||||||
let mut expert_outputs = Vec::with_capacity(self.config.num_experts);
|
|
||||||
|
|
||||||
// For simplicity, we compute all experts on the full input
|
|
||||||
// In practice, we would only compute experts that have assigned tokens
|
|
||||||
for expert in &self.experts {
|
|
||||||
let output = expert.forward(input)?;
|
|
||||||
expert_outputs.push(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(expert_outputs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Combine expert outputs using routing weights
|
|
||||||
fn combine_expert_outputs(&self, expert_outputs: &[Tensor], routing_info: &RoutingInfo) -> Result<Tensor> {
|
|
||||||
// This is a simplified implementation
|
|
||||||
// In practice, we would:
|
|
||||||
// 1. Scatter tokens to their assigned experts
|
|
||||||
// 2. Compute expert outputs only on assigned tokens
|
|
||||||
// 3. Gather and combine outputs using routing weights
|
|
||||||
|
|
||||||
let input_shape = expert_outputs[0].shape();
|
|
||||||
let mut combined = Tensor::zeros(input_shape, &self.device)?;
|
|
||||||
|
|
||||||
// Weighted combination of all expert outputs
|
|
||||||
// This is not the actual MoE implementation but serves as a placeholder
|
|
||||||
for (i, expert_output) in expert_outputs.iter().enumerate() {
|
|
||||||
let weight = 1.0 / self.config.num_experts as f32;
|
|
||||||
let weighted_output = expert_output.mul_scalar(weight)?;
|
|
||||||
combined = combined.add(&weighted_output)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(combined)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all MoE parameters for optimization
|
|
||||||
pub fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
let mut params = Vec::new();
|
|
||||||
|
|
||||||
// Router parameters
|
|
||||||
params.extend(self.router.parameters());
|
|
||||||
|
|
||||||
// Expert parameters
|
|
||||||
for expert in &self.experts {
|
|
||||||
params.extend(expert.parameters());
|
|
||||||
}
|
|
||||||
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all mutable MoE parameters for optimization
|
|
||||||
pub fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
let mut params = Vec::new();
|
|
||||||
|
|
||||||
// Router parameters
|
|
||||||
params.extend(self.router.parameters_mut());
|
|
||||||
|
|
||||||
// Expert parameters
|
|
||||||
for expert in &mut self.experts {
|
|
||||||
params.extend(expert.parameters_mut());
|
|
||||||
}
|
|
||||||
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get MoE configuration
|
|
||||||
pub fn config(&self) -> &MoEConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get number of experts
|
|
||||||
pub fn num_experts(&self) -> usize {
|
|
||||||
self.config.num_experts
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count total parameters in the MoE layer
|
|
||||||
pub fn parameter_count(&self) -> usize {
|
|
||||||
// Router parameters
|
|
||||||
let router_params = self.config.hidden_dim * self.config.num_experts +
|
|
||||||
if self.config.bias { self.config.num_experts } else { 0 };
|
|
||||||
|
|
||||||
// Expert parameters
|
|
||||||
let expert_params = self.experts[0].parameter_count() * self.config.num_experts;
|
|
||||||
|
|
||||||
router_params + expert_params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layer for MoELayer {
|
|
||||||
fn forward(&self, input: &Tensor) -> Result<Tensor> {
|
|
||||||
let output = self.forward(input)?;
|
|
||||||
Ok(output.output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layer_type(&self) -> &'static str {
|
|
||||||
"MixtureOfExperts"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
self.parameters()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
self.parameters_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Output from MoE layer forward pass
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MoEOutput {
|
|
||||||
/// Final combined output tensor
|
|
||||||
pub output: Tensor,
|
|
||||||
/// Routing information
|
|
||||||
pub routing_info: RoutingInfo,
|
|
||||||
/// Load balancing statistics
|
|
||||||
pub load_stats: LoadBalancingStats,
|
|
||||||
/// Individual expert outputs (optional, for debugging)
|
|
||||||
pub expert_outputs: Option<Vec<Tensor>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MoEOutput {
|
|
||||||
/// Get auxiliary loss for training
|
|
||||||
pub fn aux_loss(&self) -> Option<&Tensor> {
|
|
||||||
self.routing_info.load_balance_loss.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if load balancing constraints are satisfied
|
|
||||||
pub fn is_load_balanced(&self) -> bool {
|
|
||||||
self.load_stats.imbalance_score < 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_load_balancer_creation() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
|
|
||||||
let load_balancer = LoadBalancer::new(config, &device);
|
|
||||||
assert!(load_balancer.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_load_balancer_stats() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
let load_balancer = LoadBalancer::new(config, &device).unwrap();
|
|
||||||
|
|
||||||
// Test perfectly balanced load
|
|
||||||
let balanced_counts = vec![16; 8]; // 8 experts, 16 tokens each
|
|
||||||
let stats = load_balancer.calculate_load_stats(&balanced_counts, 128);
|
|
||||||
|
|
||||||
assert_eq!(stats.average_load, 16.0);
|
|
||||||
assert_eq!(stats.min_load, 16.0);
|
|
||||||
assert_eq!(stats.max_load, 16.0);
|
|
||||||
assert!(stats.imbalance_score < 1e-6);
|
|
||||||
assert_eq!(stats.expert_utilization, 1.0);
|
|
||||||
|
|
||||||
// Test imbalanced load
|
|
||||||
let imbalanced_counts = vec![32, 0, 16, 8, 24, 20, 12, 16]; // Total 128
|
|
||||||
let stats = load_balancer.calculate_load_stats(&imbalanced_counts, 128);
|
|
||||||
|
|
||||||
assert_eq!(stats.average_load, 16.0);
|
|
||||||
assert_eq!(stats.min_load, 0.0);
|
|
||||||
assert_eq!(stats.max_load, 32.0);
|
|
||||||
assert!(stats.imbalance_score > 0.0);
|
|
||||||
assert_eq!(stats.expert_utilization, 7.0 / 8.0); // 7 experts used
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_load_balancer_constraints() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
let load_balancer = LoadBalancer::new(config, &device).unwrap();
|
|
||||||
|
|
||||||
// Well-balanced case
|
|
||||||
let balanced_stats = LoadBalancingStats {
|
|
||||||
average_load: 16.0,
|
|
||||||
min_load: 14.0,
|
|
||||||
max_load: 18.0,
|
|
||||||
imbalance_score: 0.1,
|
|
||||||
expert_utilization: 1.0,
|
|
||||||
};
|
|
||||||
assert!(load_balancer.check_constraints(&balanced_stats));
|
|
||||||
|
|
||||||
// Poorly balanced case
|
|
||||||
let imbalanced_stats = LoadBalancingStats {
|
|
||||||
average_load: 16.0,
|
|
||||||
min_load: 0.0,
|
|
||||||
max_load: 64.0,
|
|
||||||
imbalance_score: 1.5,
|
|
||||||
expert_utilization: 0.5,
|
|
||||||
};
|
|
||||||
assert!(!load_balancer.check_constraints(&imbalanced_stats));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_layer_creation() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
|
|
||||||
let moe_layer = MoELayer::new(config.clone(), &device);
|
|
||||||
assert!(moe_layer.is_ok());
|
|
||||||
|
|
||||||
let moe_layer = moe_layer.unwrap();
|
|
||||||
assert_eq!(moe_layer.num_experts(), 8);
|
|
||||||
assert_eq!(moe_layer.config().top_k, 2);
|
|
||||||
assert_eq!(moe_layer.config().hidden_dim, 768);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_layer_forward() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
let moe_layer = MoELayer::new(config.clone(), &device).unwrap();
|
|
||||||
|
|
||||||
// Test input: (batch_size, seq_len, hidden_dim)
|
|
||||||
let batch_size = 4;
|
|
||||||
let seq_len = 16;
|
|
||||||
let input = Tensor::randn(&[batch_size, seq_len, config.hidden_dim], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let output = moe_layer.forward(&input);
|
|
||||||
assert!(output.is_ok());
|
|
||||||
|
|
||||||
let output = output.unwrap();
|
|
||||||
let output_shape = output.output.shape();
|
|
||||||
|
|
||||||
// Output should have same shape as input
|
|
||||||
assert_eq!(output_shape[0], batch_size);
|
|
||||||
assert_eq!(output_shape[1], seq_len);
|
|
||||||
assert_eq!(output_shape[2], config.hidden_dim);
|
|
||||||
|
|
||||||
// Should have routing info
|
|
||||||
assert_eq!(output.routing_info.expert_token_counts.len(), config.num_experts);
|
|
||||||
|
|
||||||
// Should have load stats
|
|
||||||
assert!(output.load_stats.average_load >= 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_layer_parameters() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = MoEConfig::new(4, 2, 512, 2048); // Smaller config for testing
|
|
||||||
let mut moe_layer = MoELayer::new(config.clone(), &device).unwrap();
|
|
||||||
|
|
||||||
let params = moe_layer.parameters();
|
|
||||||
let params_mut = moe_layer.parameters_mut();
|
|
||||||
|
|
||||||
// Should have parameters from router + all experts
|
|
||||||
assert!(!params.is_empty());
|
|
||||||
assert_eq!(params.len(), params_mut.len());
|
|
||||||
|
|
||||||
// Count should match expected
|
|
||||||
let expected_router_params = if config.bias { 2 } else { 1 }; // weight [+ bias]
|
|
||||||
let expected_expert_params = if config.bias { 4 } else { 2 }; // up_weight, down_weight [+ biases]
|
|
||||||
let expected_total = expected_router_params + expected_expert_params * config.num_experts;
|
|
||||||
|
|
||||||
assert_eq!(params.len(), expected_total);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_layer_as_layer_trait() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
let mut moe_layer = MoELayer::new(config.clone(), &device).unwrap();
|
|
||||||
|
|
||||||
// Test Layer trait methods
|
|
||||||
assert_eq!(moe_layer.layer_type(), "MixtureOfExperts");
|
|
||||||
assert_eq!(moe_layer.device(), &device);
|
|
||||||
|
|
||||||
// Test forward through Layer trait
|
|
||||||
let input = Tensor::randn(&[2, 8, config.hidden_dim], DType::F32, &device).unwrap();
|
|
||||||
let output = moe_layer.forward(&input);
|
|
||||||
assert!(output.is_ok());
|
|
||||||
|
|
||||||
let output = output.unwrap();
|
|
||||||
assert_eq!(output.shape()[2], config.hidden_dim);
|
|
||||||
|
|
||||||
// Test parameter access
|
|
||||||
let params = moe_layer.parameters();
|
|
||||||
let params_mut = moe_layer.parameters_mut();
|
|
||||||
assert!(!params.is_empty());
|
|
||||||
assert_eq!(params.len(), params_mut.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_moe_output_aux_loss() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let mut config = MoEConfig::new(8, 2, 768, 3072);
|
|
||||||
config.aux_loss_weight = 0.01; // Enable aux loss
|
|
||||||
let moe_layer = MoELayer::new(config.clone(), &device).unwrap();
|
|
||||||
|
|
||||||
let input = Tensor::randn(&[2, 8, config.hidden_dim], DType::F32, &device).unwrap();
|
|
||||||
let output = moe_layer.forward(&input).unwrap();
|
|
||||||
|
|
||||||
// Should have auxiliary loss when enabled
|
|
||||||
assert!(output.aux_loss().is_some());
|
|
||||||
|
|
||||||
// Test with disabled aux loss
|
|
||||||
let mut config_no_aux = config.clone();
|
|
||||||
config_no_aux.aux_loss_weight = 0.0;
|
|
||||||
let moe_layer_no_aux = MoELayer::new(config_no_aux, &device).unwrap();
|
|
||||||
let output_no_aux = moe_layer_no_aux.forward(&input).unwrap();
|
|
||||||
|
|
||||||
// Should not have auxiliary loss when disabled
|
|
||||||
assert!(output_no_aux.aux_loss().is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parameter_count() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = MoEConfig::new(4, 2, 256, 1024);
|
|
||||||
let moe_layer = MoELayer::new(config.clone(), &device).unwrap();
|
|
||||||
|
|
||||||
let param_count = moe_layer.parameter_count();
|
|
||||||
|
|
||||||
// Router: 256 * 4 (weight) + [4] (bias) = 1024 [+ 4]
|
|
||||||
let router_params = 256 * 4 + if config.bias { 4 } else { 0 };
|
|
||||||
|
|
||||||
// Each expert: 256 * 1024 * 2 (weights) + [256 + 1024] (biases)
|
|
||||||
let expert_params_each = 256 * 1024 * 2 + if config.bias { 256 + 1024 } else { 0 };
|
|
||||||
let total_expert_params = expert_params_each * 4;
|
|
||||||
|
|
||||||
let expected_total = router_params + total_expert_params;
|
|
||||||
assert_eq!(param_count, expected_total);
|
|
||||||
assert!(param_count > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,516 +0,0 @@
|
|||||||
//! Positional encoding implementations for transformer models
|
|
||||||
//!
|
|
||||||
//! Provides various positional encoding schemes including sinusoidal,
|
|
||||||
//! learned, rotary, and relative positional encodings.
|
|
||||||
|
|
||||||
use crate::{Result, TransformerError};
|
|
||||||
use rtx_tensor::{Tensor, Shape, Device};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing::{debug, trace};
|
|
||||||
use std::f64::consts::PI;
|
|
||||||
|
|
||||||
/// Positional encoding variants for transformer models
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum PositionalEncoding {
|
|
||||||
/// Sinusoidal positional encoding (Attention is All You Need)
|
|
||||||
Sinusoidal {
|
|
||||||
max_len: usize,
|
|
||||||
d_model: usize,
|
|
||||||
encoding: Tensor,
|
|
||||||
device: Device,
|
|
||||||
},
|
|
||||||
/// Learned positional embeddings
|
|
||||||
Learned {
|
|
||||||
max_len: usize,
|
|
||||||
d_model: usize,
|
|
||||||
embeddings: Tensor,
|
|
||||||
device: Device,
|
|
||||||
},
|
|
||||||
/// Rotary positional embedding (RoPE)
|
|
||||||
Rotary {
|
|
||||||
d_model: usize,
|
|
||||||
max_len: usize,
|
|
||||||
theta: f64,
|
|
||||||
sin_cache: Tensor,
|
|
||||||
cos_cache: Tensor,
|
|
||||||
device: Device,
|
|
||||||
},
|
|
||||||
/// Relative positional encoding
|
|
||||||
Relative {
|
|
||||||
max_len: usize,
|
|
||||||
d_model: usize,
|
|
||||||
clamp_len: Option<usize>,
|
|
||||||
embeddings: Tensor,
|
|
||||||
device: Device,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PositionalEncoding {
|
|
||||||
/// Create sinusoidal positional encoding
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `max_len` - Maximum sequence length
|
|
||||||
/// * `d_model` - Model dimension
|
|
||||||
/// * `device` - Device to place tensors on
|
|
||||||
pub fn sinusoidal(max_len: usize, d_model: usize, device: &Device) -> Result<Self> {
|
|
||||||
if max_len == 0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"max_len",
|
|
||||||
format!("{}", max_len),
|
|
||||||
"must be positive",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if d_model == 0 || d_model % 2 != 0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"d_model",
|
|
||||||
format!("{}", d_model),
|
|
||||||
"must be positive and even",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Creating sinusoidal positional encoding: max_len={}, d_model={}", max_len, d_model);
|
|
||||||
|
|
||||||
// Create position indices [0, 1, 2, ..., max_len-1]
|
|
||||||
let positions: Vec<f64> = (0..max_len).map(|i| i as f64).collect();
|
|
||||||
let position_tensor = Tensor::from_slice(&positions, &Shape::new(vec![max_len, 1]), device)?;
|
|
||||||
|
|
||||||
// Create dimension indices [0, 2, 4, ..., d_model-2]
|
|
||||||
let dim_indices: Vec<f64> = (0..d_model/2).map(|i| i as f64).collect();
|
|
||||||
let dim_tensor = Tensor::from_slice(&dim_indices, &Shape::new(vec![1, d_model/2]), device)?;
|
|
||||||
|
|
||||||
// Compute div_term = 1 / (10000 ^ (2i / d_model))
|
|
||||||
let div_term = (&dim_tensor * (2.0 / d_model as f64))?.exp()? * (-2.0 * PI.ln());
|
|
||||||
let div_term = div_term.exp()?;
|
|
||||||
|
|
||||||
// Compute angles = position / div_term
|
|
||||||
let angles = &position_tensor / &div_term;
|
|
||||||
|
|
||||||
// Create encoding tensor
|
|
||||||
let mut encoding_data = Vec::with_capacity(max_len * d_model);
|
|
||||||
|
|
||||||
// Fill with sin and cos alternating
|
|
||||||
for pos in 0..max_len {
|
|
||||||
for i in 0..d_model/2 {
|
|
||||||
let angle = angles.get(&[pos, i])?.to_scalar::<f64>()?;
|
|
||||||
encoding_data.push(angle.sin() as f32);
|
|
||||||
encoding_data.push(angle.cos() as f32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let encoding = Tensor::from_slice(&encoding_data, &Shape::new(vec![max_len, d_model]), device)?;
|
|
||||||
|
|
||||||
Ok(Self::Sinusoidal {
|
|
||||||
max_len,
|
|
||||||
d_model,
|
|
||||||
encoding,
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create learned positional embeddings
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `max_len` - Maximum sequence length
|
|
||||||
/// * `d_model` - Model dimension
|
|
||||||
/// * `device` - Device to place tensors on
|
|
||||||
pub fn learned(max_len: usize, d_model: usize, device: &Device) -> Result<Self> {
|
|
||||||
if max_len == 0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"max_len",
|
|
||||||
format!("{}", max_len),
|
|
||||||
"must be positive",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if d_model == 0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"d_model",
|
|
||||||
format!("{}", d_model),
|
|
||||||
"must be positive",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Creating learned positional encoding: max_len={}, d_model={}", max_len, d_model);
|
|
||||||
|
|
||||||
// Initialize with small random values
|
|
||||||
let embeddings = Tensor::randn(&Shape::new(vec![max_len, d_model]), device)? * 0.1;
|
|
||||||
|
|
||||||
Ok(Self::Learned {
|
|
||||||
max_len,
|
|
||||||
d_model,
|
|
||||||
embeddings,
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create rotary positional embedding (RoPE)
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `d_model` - Model dimension (must be even)
|
|
||||||
/// * `max_len` - Maximum sequence length
|
|
||||||
/// * `theta` - Base for frequency computation (typically 10000.0)
|
|
||||||
/// * `device` - Device to place tensors on
|
|
||||||
pub fn rotary(d_model: usize, max_len: usize, theta: f64, device: &Device) -> Result<Self> {
|
|
||||||
if d_model == 0 || d_model % 2 != 0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"d_model",
|
|
||||||
format!("{}", d_model),
|
|
||||||
"must be positive and even",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if max_len == 0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"max_len",
|
|
||||||
format!("{}", max_len),
|
|
||||||
"must be positive",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if theta <= 0.0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"theta",
|
|
||||||
format!("{}", theta),
|
|
||||||
"must be positive",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Creating rotary positional encoding: d_model={}, max_len={}, theta={}", d_model, max_len, theta);
|
|
||||||
|
|
||||||
// Compute frequencies
|
|
||||||
let half_dim = d_model / 2;
|
|
||||||
let freqs: Vec<f64> = (0..half_dim)
|
|
||||||
.map(|i| 1.0 / theta.powf(2.0 * i as f64 / d_model as f64))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Create position indices
|
|
||||||
let positions: Vec<f64> = (0..max_len).map(|i| i as f64).collect();
|
|
||||||
|
|
||||||
// Compute angles for each position and frequency
|
|
||||||
let mut sin_data = Vec::with_capacity(max_len * half_dim);
|
|
||||||
let mut cos_data = Vec::with_capacity(max_len * half_dim);
|
|
||||||
|
|
||||||
for &pos in &positions {
|
|
||||||
for &freq in &freqs {
|
|
||||||
let angle = pos * freq;
|
|
||||||
sin_data.push(angle.sin() as f32);
|
|
||||||
cos_data.push(angle.cos() as f32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sin_cache = Tensor::from_slice(&sin_data, &Shape::new(vec![max_len, half_dim]), device)?;
|
|
||||||
let cos_cache = Tensor::from_slice(&cos_data, &Shape::new(vec![max_len, half_dim]), device)?;
|
|
||||||
|
|
||||||
Ok(Self::Rotary {
|
|
||||||
d_model,
|
|
||||||
max_len,
|
|
||||||
theta,
|
|
||||||
sin_cache,
|
|
||||||
cos_cache,
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create relative positional encoding
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `max_len` - Maximum sequence length
|
|
||||||
/// * `d_model` - Model dimension
|
|
||||||
/// * `clamp_len` - Optional maximum relative distance
|
|
||||||
/// * `device` - Device to place tensors on
|
|
||||||
pub fn relative(max_len: usize, d_model: usize, clamp_len: Option<usize>, device: &Device) -> Result<Self> {
|
|
||||||
if max_len == 0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"max_len",
|
|
||||||
format!("{}", max_len),
|
|
||||||
"must be positive",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if d_model == 0 {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"d_model",
|
|
||||||
format!("{}", d_model),
|
|
||||||
"must be positive",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Creating relative positional encoding: max_len={}, d_model={}, clamp_len={:?}", max_len, d_model, clamp_len);
|
|
||||||
|
|
||||||
// Relative positions range from -(max_len-1) to +(max_len-1)
|
|
||||||
let relative_range = 2 * max_len - 1;
|
|
||||||
let embeddings = Tensor::randn(&Shape::new(vec![relative_range, d_model]), device)? * 0.1;
|
|
||||||
|
|
||||||
Ok(Self::Relative {
|
|
||||||
max_len,
|
|
||||||
d_model,
|
|
||||||
clamp_len,
|
|
||||||
embeddings,
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the maximum sequence length
|
|
||||||
pub fn max_len(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::Sinusoidal { max_len, .. } => *max_len,
|
|
||||||
Self::Learned { max_len, .. } => *max_len,
|
|
||||||
Self::Rotary { max_len, .. } => *max_len,
|
|
||||||
Self::Relative { max_len, .. } => *max_len,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the model dimension
|
|
||||||
pub fn d_model(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::Sinusoidal { d_model, .. } => *d_model,
|
|
||||||
Self::Learned { d_model, .. } => *d_model,
|
|
||||||
Self::Rotary { d_model, .. } => *d_model,
|
|
||||||
Self::Relative { d_model, .. } => *d_model,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply positional encoding to input
|
|
||||||
pub fn apply(&self, input: &Tensor, start_pos: usize) -> Result<Tensor> {
|
|
||||||
match self {
|
|
||||||
Self::Sinusoidal { encoding, .. } => {
|
|
||||||
self.apply_additive(input, encoding, start_pos)
|
|
||||||
}
|
|
||||||
Self::Learned { embeddings, .. } => {
|
|
||||||
self.apply_additive(input, embeddings, start_pos)
|
|
||||||
}
|
|
||||||
Self::Rotary { sin_cache, cos_cache, .. } => {
|
|
||||||
self.apply_rotary(input, sin_cache, cos_cache, start_pos)
|
|
||||||
}
|
|
||||||
Self::Relative { .. } => {
|
|
||||||
// Relative encoding requires special handling in attention computation
|
|
||||||
// For now, return input unchanged
|
|
||||||
Ok(input.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply additive positional encoding
|
|
||||||
fn apply_additive(&self, input: &Tensor, encoding: &Tensor, start_pos: usize) -> Result<Tensor> {
|
|
||||||
let input_shape = input.shape();
|
|
||||||
let input_dims = input_shape.dims();
|
|
||||||
|
|
||||||
if input_dims.len() < 2 {
|
|
||||||
return Err(TransformerError::dimension(
|
|
||||||
"Input must have at least 2 dimensions for positional encoding".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let seq_len = input_dims[input_dims.len() - 2];
|
|
||||||
let model_dim = input_dims[input_dims.len() - 1];
|
|
||||||
|
|
||||||
if model_dim != self.d_model() {
|
|
||||||
return Err(TransformerError::shape_mismatch(
|
|
||||||
vec![model_dim],
|
|
||||||
vec![self.d_model()],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if start_pos + seq_len > self.max_len() {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"sequence_position",
|
|
||||||
format!("start_pos={}, seq_len={}", start_pos, seq_len),
|
|
||||||
format!("exceeds max_len={}", self.max_len()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract relevant portion of encoding
|
|
||||||
let pos_encoding = encoding.slice(&[start_pos..start_pos + seq_len, 0..model_dim])?;
|
|
||||||
|
|
||||||
// Add positional encoding to input
|
|
||||||
let output = input + &pos_encoding;
|
|
||||||
|
|
||||||
trace!("Applied additive positional encoding: start_pos={}, seq_len={}", start_pos, seq_len);
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply rotary positional encoding
|
|
||||||
fn apply_rotary(&self, input: &Tensor, sin_cache: &Tensor, cos_cache: &Tensor, start_pos: usize) -> Result<Tensor> {
|
|
||||||
let input_shape = input.shape();
|
|
||||||
let input_dims = input_shape.dims();
|
|
||||||
|
|
||||||
if input_dims.len() < 2 {
|
|
||||||
return Err(TransformerError::dimension(
|
|
||||||
"Input must have at least 2 dimensions for rotary encoding".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let seq_len = input_dims[input_dims.len() - 2];
|
|
||||||
let model_dim = input_dims[input_dims.len() - 1];
|
|
||||||
|
|
||||||
if model_dim != self.d_model() {
|
|
||||||
return Err(TransformerError::shape_mismatch(
|
|
||||||
vec![model_dim],
|
|
||||||
vec![self.d_model()],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if start_pos + seq_len > self.max_len() {
|
|
||||||
return Err(TransformerError::generic(
|
|
||||||
"sequence_position",
|
|
||||||
format!("start_pos={}, seq_len={}", start_pos, seq_len),
|
|
||||||
format!("exceeds max_len={}", self.max_len()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split input into x1 and x2 (first and second half)
|
|
||||||
let half_dim = model_dim / 2;
|
|
||||||
let x1 = input.slice(&[.., 0..half_dim])?;
|
|
||||||
let x2 = input.slice(&[.., half_dim..model_dim])?;
|
|
||||||
|
|
||||||
// Get sin and cos for current positions
|
|
||||||
let sin_pos = sin_cache.slice(&[start_pos..start_pos + seq_len, ..])?;
|
|
||||||
let cos_pos = cos_cache.slice(&[start_pos..start_pos + seq_len, ..])?;
|
|
||||||
|
|
||||||
// Apply rotary transformation: [x1*cos - x2*sin, x1*sin + x2*cos]
|
|
||||||
let rotated_x1 = &x1 * &cos_pos - &x2 * &sin_pos;
|
|
||||||
let rotated_x2 = &x1 * &sin_pos + &x2 * &cos_pos;
|
|
||||||
|
|
||||||
// Concatenate rotated halves
|
|
||||||
let output = Tensor::concat(&[rotated_x1, rotated_x2], input_dims.len() - 1)?;
|
|
||||||
|
|
||||||
trace!("Applied rotary positional encoding: start_pos={}, seq_len={}", start_pos, seq_len);
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get relative position bias for attention computation
|
|
||||||
pub fn get_relative_bias(&self, query_len: usize, key_len: usize) -> Result<Option<Tensor>> {
|
|
||||||
match self {
|
|
||||||
Self::Relative { embeddings, max_len, clamp_len, .. } => {
|
|
||||||
let mut bias_data = Vec::with_capacity(query_len * key_len);
|
|
||||||
|
|
||||||
for i in 0..query_len {
|
|
||||||
for j in 0..key_len {
|
|
||||||
let relative_pos = (i as i32 - j as i32) + (*max_len as i32 - 1);
|
|
||||||
let clamped_pos = if let Some(clamp) = clamp_len {
|
|
||||||
relative_pos.max(0).min(*clamp as i32 - 1)
|
|
||||||
} else {
|
|
||||||
relative_pos.max(0).min((2 * max_len - 2) as i32)
|
|
||||||
};
|
|
||||||
bias_data.push(clamped_pos as f32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let indices = Tensor::from_slice(&bias_data, &Shape::new(vec![query_len, key_len]), self.device())?;
|
|
||||||
let bias = embeddings.index_select(0, &indices)?;
|
|
||||||
|
|
||||||
Ok(Some(bias))
|
|
||||||
}
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layer for PositionalEncoding {
|
|
||||||
fn forward(&self, input: &Tensor) -> Result<Tensor> {
|
|
||||||
self.apply(input, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layer_type(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Sinusoidal { .. } => "SinusoidalPositionalEncoding",
|
|
||||||
Self::Learned { .. } => "LearnedPositionalEncoding",
|
|
||||||
Self::Rotary { .. } => "RotaryPositionalEncoding",
|
|
||||||
Self::Relative { .. } => "RelativePositionalEncoding",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &Device {
|
|
||||||
match self {
|
|
||||||
Self::Sinusoidal { device, .. } => device,
|
|
||||||
Self::Learned { device, .. } => device,
|
|
||||||
Self::Rotary { device, .. } => device,
|
|
||||||
Self::Relative { device, .. } => device,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters(&self) -> Vec<&Tensor> {
|
|
||||||
match self {
|
|
||||||
Self::Sinusoidal { .. } => vec![], // Fixed encoding
|
|
||||||
Self::Learned { embeddings, .. } => vec![embeddings],
|
|
||||||
Self::Rotary { .. } => vec![], // Fixed encoding
|
|
||||||
Self::Relative { embeddings, .. } => vec![embeddings],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
|
|
||||||
match self {
|
|
||||||
Self::Sinusoidal { .. } => vec![], // Fixed encoding
|
|
||||||
Self::Learned { embeddings, .. } => vec![embeddings],
|
|
||||||
Self::Rotary { .. } => vec![], // Fixed encoding
|
|
||||||
Self::Relative { embeddings, .. } => vec![embeddings],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rtx_tensor::Device;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_sinusoidal_encoding() -> Result<()> {
|
|
||||||
let device = Device::cuda(0)?;
|
|
||||||
let pos_enc = PositionalEncoding::sinusoidal(512, 128, &device)?;
|
|
||||||
|
|
||||||
assert_eq!(pos_enc.max_len(), 512);
|
|
||||||
assert_eq!(pos_enc.d_model(), 128);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_learned_encoding() -> Result<()> {
|
|
||||||
let device = Device::cuda(0)?;
|
|
||||||
let pos_enc = PositionalEncoding::learned(512, 128, &device)?;
|
|
||||||
|
|
||||||
assert_eq!(pos_enc.max_len(), 512);
|
|
||||||
assert_eq!(pos_enc.d_model(), 128);
|
|
||||||
assert_eq!(pos_enc.parameters().len(), 1);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_rotary_encoding() -> Result<()> {
|
|
||||||
let device = Device::cuda(0)?;
|
|
||||||
let pos_enc = PositionalEncoding::rotary(128, 512, 10000.0, &device)?;
|
|
||||||
|
|
||||||
assert_eq!(pos_enc.max_len(), 512);
|
|
||||||
assert_eq!(pos_enc.d_model(), 128);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_relative_encoding() -> Result<()> {
|
|
||||||
let device = Device::cuda(0)?;
|
|
||||||
let pos_enc = PositionalEncoding::relative(512, 128, Some(64), &device)?;
|
|
||||||
|
|
||||||
assert_eq!(pos_enc.max_len(), 512);
|
|
||||||
assert_eq!(pos_enc.d_model(), 128);
|
|
||||||
assert_eq!(pos_enc.parameters().len(), 1);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_parameters() {
|
|
||||||
let device = Device::cuda(0).unwrap();
|
|
||||||
|
|
||||||
// Invalid d_model for sinusoidal (must be even)
|
|
||||||
assert!(PositionalEncoding::sinusoidal(512, 127, &device).is_err());
|
|
||||||
|
|
||||||
// Invalid max_len
|
|
||||||
assert!(PositionalEncoding::learned(0, 128, &device).is_err());
|
|
||||||
|
|
||||||
// Invalid theta for rotary
|
|
||||||
assert!(PositionalEncoding::rotary(128, 512, 0.0, &device).is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,871 +0,0 @@
|
|||||||
//! Sliding Window Attention Implementation
|
|
||||||
//!
|
|
||||||
//! This module implements Sliding Window Attention, which limits attention to a
|
|
||||||
//! local window of tokens rather than the full sequence. This approach enables
|
|
||||||
//! efficient processing of long sequences by reducing computational complexity
|
|
||||||
//! from O(n²) to O(n*w) where w is the window size.
|
|
||||||
//!
|
|
||||||
//! ## Features
|
|
||||||
//!
|
|
||||||
//! - **Configurable window size**: Support for different window sizes (e.g., 256 tokens)
|
|
||||||
//! - **Efficient computation**: O(n*w) complexity for long sequences instead of O(n²)
|
|
||||||
//! - **Boundary handling**: Proper handling at sequence start/end
|
|
||||||
//! - **Causal and bidirectional modes**: Support for both autoregressive and encoder models
|
|
||||||
//! - **MQA/GQA integration**: Full compatibility with Multi-Query and Grouped-Query Attention
|
|
||||||
//! - **Memory efficiency**: Significant memory savings for long sequences
|
|
||||||
//!
|
|
||||||
//! ## Usage
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! use rtx_transformers::layers::{SlidingWindowAttention, SlidingWindowConfig};
|
|
||||||
//! use rtx_tensor::Device;
|
|
||||||
//!
|
|
||||||
//! // Create configuration
|
|
||||||
//! let config = SlidingWindowConfig::new(12, 768, 256)?; // 12 heads, 768 dim, 256 window
|
|
||||||
//! let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
//!
|
|
||||||
//! // Create attention layer
|
|
||||||
//! let mut attention = SlidingWindowAttention::new(config, &device)?;
|
|
||||||
//! attention.initialize_parameters()?;
|
|
||||||
//!
|
|
||||||
//! // Use in forward pass
|
|
||||||
//! let output = attention.forward(&input_tensor, None, None)?;
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## Integration with MQA/GQA
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! // Multi-Query Attention (1 KV head)
|
|
||||||
//! let mqa_config = SlidingWindowConfig::new(12, 768, 256)?
|
|
||||||
//! .with_kv_heads(1)?;
|
|
||||||
//!
|
|
||||||
//! // Grouped-Query Attention (4 KV heads)
|
|
||||||
//! let gqa_config = SlidingWindowConfig::new(12, 768, 256)?
|
|
||||||
//! .with_kv_heads(4)?;
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use crate::{Result, TransformerError};
|
|
||||||
use crate::architectures::TransformerConfig;
|
|
||||||
use rtx_tensor::{Tensor, Device, DType};
|
|
||||||
use rtx_autograd::{TensorAutograd, backward, NodeId};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Sliding Window Attention configuration
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct SlidingWindowConfig {
|
|
||||||
/// Number of attention heads
|
|
||||||
pub num_heads: usize,
|
|
||||||
/// Model dimension
|
|
||||||
pub d_model: usize,
|
|
||||||
/// Head dimension (d_model / num_heads)
|
|
||||||
pub head_dim: usize,
|
|
||||||
/// Window size (number of tokens to attend to on each side)
|
|
||||||
pub window_size: usize,
|
|
||||||
/// Whether to use causal masking (only attend to previous tokens)
|
|
||||||
pub causal: bool,
|
|
||||||
/// Whether to apply scaling factor (1/sqrt(head_dim))
|
|
||||||
pub scale: bool,
|
|
||||||
/// Dropout probability for attention weights
|
|
||||||
pub dropout: f64,
|
|
||||||
/// Whether to use bias in linear projections
|
|
||||||
pub bias: bool,
|
|
||||||
/// Number of key-value heads (for integration with MQA/GQA)
|
|
||||||
pub num_kv_heads: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlidingWindowConfig {
|
|
||||||
/// Create sliding window config from TransformerConfig
|
|
||||||
pub fn from_transformer_config(config: &TransformerConfig, window_size: usize) -> Result<Self> {
|
|
||||||
if window_size == 0 {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
"Window size must be greater than 0".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let head_dim = config.d_model / config.num_heads;
|
|
||||||
if config.d_model % config.num_heads != 0 {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
format!("d_model ({}) must be divisible by num_heads ({})",
|
|
||||||
config.d_model, config.num_heads)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let num_kv_heads = if config.get_num_key_value_heads() != config.num_heads {
|
|
||||||
Some(config.get_num_key_value_heads())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
num_heads: config.num_heads,
|
|
||||||
d_model: config.d_model,
|
|
||||||
head_dim,
|
|
||||||
window_size,
|
|
||||||
causal: true, // Default to causal for autoregressive models
|
|
||||||
scale: true,
|
|
||||||
dropout: config.dropout,
|
|
||||||
bias: config.bias,
|
|
||||||
num_kv_heads,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create config with specific parameters
|
|
||||||
pub fn new(
|
|
||||||
num_heads: usize,
|
|
||||||
d_model: usize,
|
|
||||||
window_size: usize,
|
|
||||||
) -> Result<Self> {
|
|
||||||
if window_size == 0 {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
"Window size must be greater than 0".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if d_model % num_heads != 0 {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
format!("d_model ({}) must be divisible by num_heads ({})",
|
|
||||||
d_model, num_heads)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
num_heads,
|
|
||||||
d_model,
|
|
||||||
head_dim: d_model / num_heads,
|
|
||||||
window_size,
|
|
||||||
causal: true,
|
|
||||||
scale: true,
|
|
||||||
dropout: 0.0,
|
|
||||||
bias: true,
|
|
||||||
num_kv_heads: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable/disable causal masking
|
|
||||||
pub fn with_causal(mut self, causal: bool) -> Self {
|
|
||||||
self.causal = causal;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set number of key-value heads for MQA/GQA integration
|
|
||||||
pub fn with_kv_heads(mut self, num_kv_heads: usize) -> Result<Self> {
|
|
||||||
if num_kv_heads > self.num_heads {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
format!("num_kv_heads ({}) cannot exceed num_heads ({})",
|
|
||||||
num_kv_heads, self.num_heads)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.num_heads % num_kv_heads != 0 {
|
|
||||||
return Err(TransformerError::config(
|
|
||||||
format!("num_heads ({}) must be divisible by num_kv_heads ({})",
|
|
||||||
self.num_heads, num_kv_heads)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.num_kv_heads = Some(num_kv_heads);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get effective number of key-value heads
|
|
||||||
pub fn get_num_kv_heads(&self) -> usize {
|
|
||||||
self.num_kv_heads.unwrap_or(self.num_heads)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this is Multi-Query Attention (single KV head)
|
|
||||||
pub fn is_mqa(&self) -> bool {
|
|
||||||
self.num_kv_heads == Some(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this is Grouped-Query Attention (multiple but fewer KV heads)
|
|
||||||
pub fn is_gqa(&self) -> bool {
|
|
||||||
match self.num_kv_heads {
|
|
||||||
Some(kv_heads) => kv_heads > 1 && kv_heads < self.num_heads,
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sliding Window Attention layer
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SlidingWindowAttention {
|
|
||||||
config: SlidingWindowConfig,
|
|
||||||
device: Device,
|
|
||||||
|
|
||||||
// Placeholder for actual tensor parameters
|
|
||||||
q_proj_initialized: bool,
|
|
||||||
k_proj_initialized: bool,
|
|
||||||
v_proj_initialized: bool,
|
|
||||||
o_proj_initialized: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlidingWindowAttention {
|
|
||||||
/// Create new Sliding Window Attention layer
|
|
||||||
pub fn new(config: SlidingWindowConfig, device: &Device) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
device: device.clone(),
|
|
||||||
q_proj_initialized: false,
|
|
||||||
k_proj_initialized: false,
|
|
||||||
v_proj_initialized: false,
|
|
||||||
o_proj_initialized: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize layer parameters
|
|
||||||
pub fn initialize_parameters(&mut self) -> Result<()> {
|
|
||||||
// In a real implementation, this would initialize:
|
|
||||||
// - q_proj: Linear(d_model, num_heads * head_dim)
|
|
||||||
// - k_proj: Linear(d_model, num_kv_heads * head_dim)
|
|
||||||
// - v_proj: Linear(d_model, num_kv_heads * head_dim)
|
|
||||||
// - o_proj: Linear(num_heads * head_dim, d_model)
|
|
||||||
|
|
||||||
self.q_proj_initialized = true;
|
|
||||||
self.k_proj_initialized = true;
|
|
||||||
self.v_proj_initialized = true;
|
|
||||||
self.o_proj_initialized = true;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Forward pass with sliding window attention
|
|
||||||
pub fn forward(
|
|
||||||
&self,
|
|
||||||
hidden_states: &Tensor,
|
|
||||||
attention_mask: Option<&Tensor>,
|
|
||||||
position_ids: Option<&Tensor>,
|
|
||||||
) -> Result<Tensor> {
|
|
||||||
if !self.q_proj_initialized {
|
|
||||||
return Err(TransformerError::architecture(
|
|
||||||
"Sliding window attention layer parameters not initialized. Call initialize_parameters() first.".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let batch_size = hidden_states.shape().dims()[0];
|
|
||||||
let seq_len = hidden_states.shape().dims()[1];
|
|
||||||
let d_model = hidden_states.shape().dims()[2];
|
|
||||||
|
|
||||||
if d_model != self.config.d_model {
|
|
||||||
return Err(TransformerError::shape_mismatch(
|
|
||||||
format!("Expected d_model {}, got {}", self.config.d_model, d_model)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement sliding window attention computation
|
|
||||||
// This is a simplified but functional implementation that demonstrates
|
|
||||||
// the key concepts of sliding window attention
|
|
||||||
|
|
||||||
let scale_factor = if self.config.scale {
|
|
||||||
1.0 / (self.config.head_dim as f32).sqrt()
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Step 1: Simulate query, key, value projections
|
|
||||||
let num_kv_heads = self.config.get_num_kv_heads();
|
|
||||||
|
|
||||||
// Query projection: [batch, seq_len, num_heads * head_dim]
|
|
||||||
let query_scale = (self.config.num_heads as f32 / d_model as f32).sqrt() * scale_factor;
|
|
||||||
let queries = hidden_states.mul_scalar(query_scale)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
|
|
||||||
|
|
||||||
// Key/Value projection: [batch, seq_len, num_kv_heads * head_dim]
|
|
||||||
let kv_scale = (num_kv_heads as f32 / d_model as f32).sqrt() * scale_factor;
|
|
||||||
let keys = hidden_states.mul_scalar(kv_scale * 0.9) // Slightly different from queries
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
|
|
||||||
let values = hidden_states.mul_scalar(kv_scale * 1.1) // Different scaling for values
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
|
|
||||||
|
|
||||||
// Step 2: Apply sliding window attention pattern
|
|
||||||
let effective_window = self.effective_attention_span(seq_len);
|
|
||||||
let window_factor = (effective_window as f32) / (seq_len as f32);
|
|
||||||
|
|
||||||
// Step 3: Simulate attention computation with window constraints
|
|
||||||
// In a real implementation, this would involve:
|
|
||||||
// - Reshaping Q, K, V to separate heads
|
|
||||||
// - Computing attention scores only within the window
|
|
||||||
// - Applying causal/bidirectional masking as needed
|
|
||||||
// - Applying attention mask if provided
|
|
||||||
|
|
||||||
let attention_output = if self.config.causal {
|
|
||||||
// Causal sliding window: each token can only attend to previous tokens within window
|
|
||||||
let causal_factor = 0.8 + window_factor * 0.2;
|
|
||||||
queries.mul_scalar(causal_factor)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?
|
|
||||||
.add(&values.mul_scalar(0.3)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?
|
|
||||||
} else {
|
|
||||||
// Bidirectional sliding window: can attend to tokens on both sides
|
|
||||||
let bidirectional_factor = 0.7 + window_factor * 0.3;
|
|
||||||
queries.mul_scalar(bidirectional_factor)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?
|
|
||||||
.add(&values.mul_scalar(0.4)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Step 4: Apply output projection
|
|
||||||
// In a real implementation: output = attention_output @ W_o
|
|
||||||
let output_factor = if num_kv_heads < self.config.num_heads {
|
|
||||||
// Account for reduced KV heads in MQA/GQA
|
|
||||||
1.1 - (num_kv_heads as f32 / self.config.num_heads as f32) * 0.1
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let final_output = attention_output.mul_scalar(output_factor)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
|
|
||||||
|
|
||||||
// Step 5: Apply attention mask if provided
|
|
||||||
let masked_output = if let Some(mask) = attention_mask {
|
|
||||||
// Simple mask application - in real implementation would be more sophisticated
|
|
||||||
let mask_expanded = mask.unsqueeze(2)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?
|
|
||||||
.broadcast_to(&final_output.shape().dims())
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
|
|
||||||
|
|
||||||
final_output.mul(&mask_expanded)
|
|
||||||
.map_err(|e| TransformerError::tensor_op(e.to_string()))?
|
|
||||||
} else {
|
|
||||||
final_output
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(masked_output)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get configuration
|
|
||||||
pub fn config(&self) -> &SlidingWindowConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute effective attention span for given sequence length
|
|
||||||
pub fn effective_attention_span(&self, seq_len: usize) -> usize {
|
|
||||||
if self.config.causal {
|
|
||||||
// Causal attention: can only attend to previous tokens within window
|
|
||||||
(self.config.window_size + 1).min(seq_len)
|
|
||||||
} else {
|
|
||||||
// Bidirectional: can attend to tokens on both sides within window
|
|
||||||
(2 * self.config.window_size + 1).min(seq_len)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Estimate memory savings compared to full attention
|
|
||||||
pub fn memory_savings_ratio(&self, seq_len: usize) -> f64 {
|
|
||||||
let effective_span = self.effective_attention_span(seq_len) as f64;
|
|
||||||
let full_span = seq_len as f64;
|
|
||||||
|
|
||||||
// Memory scales with attention matrix size
|
|
||||||
let sliding_memory = effective_span;
|
|
||||||
let full_memory = full_span;
|
|
||||||
|
|
||||||
1.0 - (sliding_memory / full_memory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::architectures::TransformerConfig;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_config_creation_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing basic config creation
|
|
||||||
let result = SlidingWindowConfig::new(12, 768, 256);
|
|
||||||
// This will fail because SlidingWindowConfig::new doesn't exist yet
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let config = result.unwrap();
|
|
||||||
assert_eq!(config.num_heads, 12);
|
|
||||||
assert_eq!(config.d_model, 768);
|
|
||||||
assert_eq!(config.window_size, 256);
|
|
||||||
assert_eq!(config.head_dim, 64); // 768 / 12
|
|
||||||
assert!(config.causal); // Default should be causal
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_config_validation_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing validation
|
|
||||||
let result = SlidingWindowConfig::new(12, 768, 0);
|
|
||||||
// Should fail with zero window size
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().to_string().contains("Window size must be greater than 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_config_from_transformer_config_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing creation from transformer config
|
|
||||||
let transformer_config = TransformerConfig::new(50257, 768, 12, 12, 3072, 1024);
|
|
||||||
let result = SlidingWindowConfig::from_transformer_config(&transformer_config, 256);
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let config = result.unwrap();
|
|
||||||
assert_eq!(config.window_size, 256);
|
|
||||||
assert_eq!(config.num_heads, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_config_causal_settings_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing causal configuration
|
|
||||||
let config = SlidingWindowConfig::new(8, 512, 128).unwrap()
|
|
||||||
.with_causal(false);
|
|
||||||
|
|
||||||
assert!(!config.causal); // Should be bidirectional
|
|
||||||
assert_eq!(config.window_size, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_config_mqa_integration_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing MQA integration
|
|
||||||
let config = SlidingWindowConfig::new(12, 768, 256).unwrap()
|
|
||||||
.with_kv_heads(1).unwrap(); // Multi-Query Attention
|
|
||||||
|
|
||||||
assert!(config.is_mqa());
|
|
||||||
assert!(!config.is_gqa());
|
|
||||||
assert_eq!(config.get_num_kv_heads(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_config_gqa_integration_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing GQA integration
|
|
||||||
let config = SlidingWindowConfig::new(12, 768, 256).unwrap()
|
|
||||||
.with_kv_heads(4).unwrap(); // Grouped-Query Attention
|
|
||||||
|
|
||||||
assert!(!config.is_mqa());
|
|
||||||
assert!(config.is_gqa());
|
|
||||||
assert_eq!(config.get_num_kv_heads(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_attention_creation_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing attention layer creation
|
|
||||||
let config = SlidingWindowConfig::new(12, 768, 256).unwrap();
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let attention = SlidingWindowAttention::new(config, &device);
|
|
||||||
assert!(attention.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_attention_forward_pass_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing forward pass
|
|
||||||
let config = SlidingWindowConfig::new(12, 768, 256).unwrap();
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let mut attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
attention.initialize_parameters().unwrap();
|
|
||||||
|
|
||||||
// Create test input
|
|
||||||
let batch_size = 2;
|
|
||||||
let seq_len = 512; // Longer than window size
|
|
||||||
let d_model = 768;
|
|
||||||
|
|
||||||
let hidden_states = Tensor::zeros_typed(
|
|
||||||
&[batch_size, seq_len, d_model],
|
|
||||||
DType::F32,
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let result = attention.forward(&hidden_states, None, None);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let output = result.unwrap();
|
|
||||||
assert_eq!(output.shape().dims(), &[batch_size, seq_len, d_model]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_effective_attention_span_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing attention span calculation
|
|
||||||
let config = SlidingWindowConfig::new(12, 768, 256).unwrap();
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
|
|
||||||
// Test causal attention span
|
|
||||||
let causal_span = attention.effective_attention_span(512);
|
|
||||||
assert_eq!(causal_span, 257); // window_size + 1 for causal
|
|
||||||
|
|
||||||
// Test with sequence shorter than window
|
|
||||||
let short_span = attention.effective_attention_span(128);
|
|
||||||
assert_eq!(short_span, 128); // Limited by sequence length
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_bidirectional_attention_span_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing bidirectional attention span
|
|
||||||
let config = SlidingWindowConfig::new(12, 768, 256).unwrap()
|
|
||||||
.with_causal(false);
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
|
|
||||||
// Test bidirectional attention span
|
|
||||||
let bidirectional_span = attention.effective_attention_span(512);
|
|
||||||
assert_eq!(bidirectional_span, 513); // 2 * window_size + 1
|
|
||||||
|
|
||||||
// Test with sequence shorter than window
|
|
||||||
let short_span = attention.effective_attention_span(128);
|
|
||||||
assert_eq!(short_span, 128); // Limited by sequence length
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_memory_savings_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing memory savings calculation
|
|
||||||
let config = SlidingWindowConfig::new(12, 768, 256).unwrap();
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
|
|
||||||
// Test memory savings for long sequence
|
|
||||||
let savings = attention.memory_savings_ratio(2048);
|
|
||||||
assert!(savings > 0.8); // Should save significant memory
|
|
||||||
assert!(savings < 1.0); // But not 100%
|
|
||||||
|
|
||||||
// Test with sequence equal to window size
|
|
||||||
let no_savings = attention.memory_savings_ratio(257); // window_size + 1
|
|
||||||
assert!(no_savings < 0.1); // Minimal savings when sequence fits in window
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_long_sequence_forward_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing forward pass with long sequences
|
|
||||||
let config = SlidingWindowConfig::new(8, 512, 64).unwrap(); // Small window for testing
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let mut attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
attention.initialize_parameters().unwrap();
|
|
||||||
|
|
||||||
// Create a long sequence that exceeds the window size
|
|
||||||
let batch_size = 1;
|
|
||||||
let seq_len = 256; // Much longer than window_size=64
|
|
||||||
let d_model = 512;
|
|
||||||
|
|
||||||
let hidden_states = Tensor::ones_typed(
|
|
||||||
&[batch_size, seq_len, d_model],
|
|
||||||
DType::F32,
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let result = attention.forward(&hidden_states, None, None);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let output = result.unwrap();
|
|
||||||
assert_eq!(output.shape().dims(), &[batch_size, seq_len, d_model]);
|
|
||||||
|
|
||||||
// Output should be different from input (showing attention computation)
|
|
||||||
let input_sum = hidden_states.sum(None).unwrap().to_scalar::<f32>().unwrap();
|
|
||||||
let output_sum = output.sum(None).unwrap().to_scalar::<f32>().unwrap();
|
|
||||||
assert_ne!(input_sum, output_sum, "Output should differ from input due to attention");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_gradient_computation_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing gradient computation through sliding window
|
|
||||||
let config = SlidingWindowConfig::new(4, 256, 32).unwrap();
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let mut attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
attention.initialize_parameters().unwrap();
|
|
||||||
|
|
||||||
let batch_size = 1;
|
|
||||||
let seq_len = 64;
|
|
||||||
let d_model = 256;
|
|
||||||
|
|
||||||
let input_data = vec![0.5f32; batch_size * seq_len * d_model];
|
|
||||||
let hidden_states = Tensor::from_vec(
|
|
||||||
input_data,
|
|
||||||
&[batch_size, seq_len, d_model],
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let hidden_states_grad = hidden_states.set_requires_grad(true);
|
|
||||||
|
|
||||||
// Forward pass
|
|
||||||
let output = attention.forward(&hidden_states_grad, None, None).unwrap();
|
|
||||||
|
|
||||||
// Create loss
|
|
||||||
let loss = output.sum(None).unwrap();
|
|
||||||
|
|
||||||
// Backward pass
|
|
||||||
let grad_result = backward(&loss, &[&hidden_states_grad]);
|
|
||||||
assert!(grad_result.is_ok());
|
|
||||||
|
|
||||||
let gradients = grad_result.unwrap();
|
|
||||||
assert_eq!(gradients.len(), 1);
|
|
||||||
|
|
||||||
let input_grad = &gradients[0];
|
|
||||||
assert_eq!(input_grad.shape().dims(), hidden_states_grad.shape().dims());
|
|
||||||
|
|
||||||
// Gradients should be meaningful (not all zeros)
|
|
||||||
let grad_norm = input_grad.sum(None).unwrap().to_scalar::<f32>().unwrap();
|
|
||||||
assert!(grad_norm.abs() > 1e-6, "Gradients should be non-zero for proper backprop");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_causal_vs_bidirectional_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing causal vs bidirectional modes
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let window_size = 32;
|
|
||||||
|
|
||||||
// Create causal config
|
|
||||||
let causal_config = SlidingWindowConfig::new(8, 512, window_size).unwrap()
|
|
||||||
.with_causal(true);
|
|
||||||
let causal_attention = SlidingWindowAttention::new(causal_config, &device).unwrap();
|
|
||||||
|
|
||||||
// Create bidirectional config
|
|
||||||
let bidirectional_config = SlidingWindowConfig::new(8, 512, window_size).unwrap()
|
|
||||||
.with_causal(false);
|
|
||||||
let bidirectional_attention = SlidingWindowAttention::new(bidirectional_config, &device).unwrap();
|
|
||||||
|
|
||||||
let seq_len = 128;
|
|
||||||
|
|
||||||
// Test attention spans
|
|
||||||
let causal_span = causal_attention.effective_attention_span(seq_len);
|
|
||||||
let bidirectional_span = bidirectional_attention.effective_attention_span(seq_len);
|
|
||||||
|
|
||||||
// Bidirectional should have larger effective span
|
|
||||||
assert!(bidirectional_span > causal_span);
|
|
||||||
assert_eq!(causal_span, window_size + 1); // window_size + current token
|
|
||||||
assert_eq!(bidirectional_span, 2 * window_size + 1); // both directions + current token
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_with_attention_mask_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing interaction with attention masks
|
|
||||||
let config = SlidingWindowConfig::new(4, 256, 16).unwrap();
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let mut attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
attention.initialize_parameters().unwrap();
|
|
||||||
|
|
||||||
let batch_size = 2;
|
|
||||||
let seq_len = 32;
|
|
||||||
let d_model = 256;
|
|
||||||
|
|
||||||
let hidden_states = Tensor::ones_typed(
|
|
||||||
&[batch_size, seq_len, d_model],
|
|
||||||
DType::F32,
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
// Create attention mask (e.g., padding mask)
|
|
||||||
let mask_data = vec![1.0f32; batch_size * seq_len];
|
|
||||||
let attention_mask = Tensor::from_vec(
|
|
||||||
mask_data,
|
|
||||||
&[batch_size, seq_len],
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let result = attention.forward(&hidden_states, Some(&attention_mask), None);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let output = result.unwrap();
|
|
||||||
assert_eq!(output.shape().dims(), &[batch_size, seq_len, d_model]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_boundary_tokens_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing behavior at sequence boundaries
|
|
||||||
let config = SlidingWindowConfig::new(4, 128, 8).unwrap();
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let mut attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
attention.initialize_parameters().unwrap();
|
|
||||||
|
|
||||||
// Test with sequence exactly equal to window size
|
|
||||||
let seq_len = 8; // Same as window_size
|
|
||||||
let batch_size = 1;
|
|
||||||
let d_model = 128;
|
|
||||||
|
|
||||||
let hidden_states = Tensor::ones_typed(
|
|
||||||
&[batch_size, seq_len, d_model],
|
|
||||||
DType::F32,
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let result = attention.forward(&hidden_states, None, None);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
// Test with sequence smaller than window
|
|
||||||
let small_seq_len = 4; // Smaller than window_size=8
|
|
||||||
let small_hidden_states = Tensor::ones_typed(
|
|
||||||
&[batch_size, small_seq_len, d_model],
|
|
||||||
DType::F32,
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let small_result = attention.forward(&small_hidden_states, None, None);
|
|
||||||
assert!(small_result.is_ok());
|
|
||||||
|
|
||||||
let small_output = small_result.unwrap();
|
|
||||||
assert_eq!(small_output.shape().dims(), &[batch_size, small_seq_len, d_model]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_with_mqa_integration_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing MQA integration
|
|
||||||
let base_config = SlidingWindowConfig::new(12, 768, 128).unwrap();
|
|
||||||
let mqa_config = base_config.with_kv_heads(1).unwrap(); // Multi-Query Attention
|
|
||||||
|
|
||||||
assert!(mqa_config.is_mqa());
|
|
||||||
assert!(!mqa_config.is_gqa());
|
|
||||||
assert_eq!(mqa_config.get_num_kv_heads(), 1);
|
|
||||||
assert_eq!(mqa_config.num_heads, 12);
|
|
||||||
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let mut attention = SlidingWindowAttention::new(mqa_config, &device).unwrap();
|
|
||||||
attention.initialize_parameters().unwrap();
|
|
||||||
|
|
||||||
let batch_size = 1;
|
|
||||||
let seq_len = 64;
|
|
||||||
let d_model = 768;
|
|
||||||
|
|
||||||
let hidden_states = Tensor::ones_typed(
|
|
||||||
&[batch_size, seq_len, d_model],
|
|
||||||
DType::F32,
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let result = attention.forward(&hidden_states, None, None);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let output = result.unwrap();
|
|
||||||
assert_eq!(output.shape().dims(), &[batch_size, seq_len, d_model]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_with_gqa_integration_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing GQA integration
|
|
||||||
let base_config = SlidingWindowConfig::new(12, 768, 128).unwrap();
|
|
||||||
let gqa_config = base_config.with_kv_heads(4).unwrap(); // Grouped-Query Attention
|
|
||||||
|
|
||||||
assert!(!gqa_config.is_mqa());
|
|
||||||
assert!(gqa_config.is_gqa());
|
|
||||||
assert_eq!(gqa_config.get_num_kv_heads(), 4);
|
|
||||||
assert_eq!(gqa_config.num_heads, 12);
|
|
||||||
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let mut attention = SlidingWindowAttention::new(gqa_config, &device).unwrap();
|
|
||||||
attention.initialize_parameters().unwrap();
|
|
||||||
|
|
||||||
let batch_size = 2;
|
|
||||||
let seq_len = 96;
|
|
||||||
let d_model = 768;
|
|
||||||
|
|
||||||
let hidden_states = Tensor::ones_typed(
|
|
||||||
&[batch_size, seq_len, d_model],
|
|
||||||
DType::F32,
|
|
||||||
&device
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let result = attention.forward(&hidden_states, None, None);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let output = result.unwrap();
|
|
||||||
assert_eq!(output.shape().dims(), &[batch_size, seq_len, d_model]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_kv_heads_validation_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing KV heads validation
|
|
||||||
let base_config = SlidingWindowConfig::new(12, 768, 64).unwrap();
|
|
||||||
|
|
||||||
// Should fail with too many KV heads
|
|
||||||
let invalid_result = base_config.clone().with_kv_heads(15); // More than num_heads
|
|
||||||
assert!(invalid_result.is_err());
|
|
||||||
assert!(invalid_result.unwrap_err().to_string().contains("cannot exceed"));
|
|
||||||
|
|
||||||
// Should fail with non-divisible KV heads
|
|
||||||
let invalid_result2 = base_config.clone().with_kv_heads(5); // 12 not divisible by 5
|
|
||||||
assert!(invalid_result2.is_err());
|
|
||||||
assert!(invalid_result2.unwrap_err().to_string().contains("divisible"));
|
|
||||||
|
|
||||||
// Should succeed with valid KV heads
|
|
||||||
let valid_result = base_config.with_kv_heads(6); // 12 divisible by 6
|
|
||||||
assert!(valid_result.is_ok());
|
|
||||||
|
|
||||||
let valid_config = valid_result.unwrap();
|
|
||||||
assert_eq!(valid_config.get_num_kv_heads(), 6);
|
|
||||||
assert!(valid_config.is_gqa());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_window_size_effects_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing how window size affects computations
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let seq_len = 256;
|
|
||||||
|
|
||||||
// Test different window sizes
|
|
||||||
let small_window_config = SlidingWindowConfig::new(8, 512, 32).unwrap(); // Small window
|
|
||||||
let large_window_config = SlidingWindowConfig::new(8, 512, 128).unwrap(); // Large window
|
|
||||||
|
|
||||||
let small_attention = SlidingWindowAttention::new(small_window_config, &device).unwrap();
|
|
||||||
let large_attention = SlidingWindowAttention::new(large_window_config, &device).unwrap();
|
|
||||||
|
|
||||||
// Memory savings should be different
|
|
||||||
let small_savings = small_attention.memory_savings_ratio(seq_len);
|
|
||||||
let large_savings = large_attention.memory_savings_ratio(seq_len);
|
|
||||||
|
|
||||||
// Smaller window should save more memory
|
|
||||||
assert!(small_savings > large_savings);
|
|
||||||
|
|
||||||
// Effective attention spans should be different
|
|
||||||
let small_span = small_attention.effective_attention_span(seq_len);
|
|
||||||
let large_span = large_attention.effective_attention_span(seq_len);
|
|
||||||
|
|
||||||
assert!(large_span > small_span);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_from_transformer_config_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing creation from TransformerConfig
|
|
||||||
let mut transformer_config = TransformerConfig::new(50257, 768, 12, 12, 3072, 1024);
|
|
||||||
|
|
||||||
// Test with standard MHA
|
|
||||||
let result = SlidingWindowConfig::from_transformer_config(&transformer_config, 256);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let config = result.unwrap();
|
|
||||||
assert_eq!(config.window_size, 256);
|
|
||||||
assert_eq!(config.num_heads, 12);
|
|
||||||
assert_eq!(config.d_model, 768);
|
|
||||||
assert!(config.num_kv_heads.is_none()); // Standard MHA
|
|
||||||
|
|
||||||
// Test with MQA enabled
|
|
||||||
transformer_config.set_mqa(true, 1).unwrap();
|
|
||||||
let mqa_result = SlidingWindowConfig::from_transformer_config(&transformer_config, 256);
|
|
||||||
assert!(mqa_result.is_ok());
|
|
||||||
|
|
||||||
let mqa_config = mqa_result.unwrap();
|
|
||||||
assert_eq!(mqa_config.get_num_kv_heads(), 1);
|
|
||||||
assert!(mqa_config.is_mqa());
|
|
||||||
|
|
||||||
// Test with invalid window size
|
|
||||||
let invalid_result = SlidingWindowConfig::from_transformer_config(&transformer_config, 0);
|
|
||||||
assert!(invalid_result.is_err());
|
|
||||||
assert!(invalid_result.unwrap_err().to_string().contains("Window size must be greater than 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sliding_window_performance_characteristics_should_fail_initially() {
|
|
||||||
// This test should FAIL initially - testing performance characteristics
|
|
||||||
let config = SlidingWindowConfig::new(16, 1024, 128).unwrap();
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let attention = SlidingWindowAttention::new(config, &device).unwrap();
|
|
||||||
|
|
||||||
// Test memory savings for various sequence lengths
|
|
||||||
let test_sequences = vec![256, 512, 1024, 2048, 4096];
|
|
||||||
|
|
||||||
for seq_len in test_sequences {
|
|
||||||
let savings = attention.memory_savings_ratio(seq_len);
|
|
||||||
let effective_span = attention.effective_attention_span(seq_len);
|
|
||||||
|
|
||||||
// Memory savings should increase with sequence length
|
|
||||||
if seq_len > 256 { // Window size + some buffer
|
|
||||||
assert!(savings > 0.5, "Should save significant memory for seq_len {}", seq_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Effective span should be bounded by window size for causal attention
|
|
||||||
if seq_len > 128 { // If sequence is longer than window
|
|
||||||
assert_eq!(effective_span, 129); // window_size + 1 for causal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
|||||||
//! # RTX Transformers: Complete Transformer Training Infrastructure
|
|
||||||
//!
|
|
||||||
//! This crate provides a complete transformer training infrastructure that is 5-10x faster
|
|
||||||
//! than PyTorch 2.5 while integrating revolutionary quantum, neuromorphic, and edge capabilities.
|
|
||||||
//!
|
|
||||||
//! ## Features
|
|
||||||
//!
|
|
||||||
//! - **Adam/AdamW Optimizers**: Industry-standard optimizers with superior performance
|
|
||||||
//! - **Learning Rate Schedulers**: Warmup, cosine annealing, and advanced scheduling
|
|
||||||
//! - **Layer Components**: LayerNorm, RMSNorm, positional encodings
|
|
||||||
//! - **Transformer Architectures**: GPT, BERT, T5 with modern variants
|
|
||||||
//! - **Revolutionary Integration**: Quantum-enhanced attention, neuromorphic preprocessing, edge deployment
|
|
||||||
//!
|
|
||||||
//! ## Examples
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! use rtx_transformers::{AdamOptimizer, TransformerConfig};
|
|
||||||
//!
|
|
||||||
//! // Create Adam optimizer
|
|
||||||
//! let optimizer = AdamOptimizer::new(0.001, 0.9, 0.999, 1e-8, 0.01)?;
|
|
||||||
//!
|
|
||||||
//! // Configure transformer
|
|
||||||
//! let config = TransformerConfig::gpt2_small();
|
|
||||||
//! # Ok::<(), Box<dyn std::error::Error>>(())
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![deny(unsafe_code)]
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub mod optimizers;
|
|
||||||
pub mod schedulers;
|
|
||||||
pub mod layers;
|
|
||||||
pub mod architectures;
|
|
||||||
pub mod training;
|
|
||||||
pub mod tokenization;
|
|
||||||
pub mod revolutionary;
|
|
||||||
pub mod benchmarks;
|
|
||||||
|
|
||||||
// Re-export key types for convenience
|
|
||||||
pub use error::{TransformerError, Result};
|
|
||||||
pub use optimizers::{AdamOptimizer, AdamWOptimizer, Optimizer};
|
|
||||||
pub use schedulers::{LearningRateScheduler, WarmupScheduler, CosineAnnealingScheduler};
|
|
||||||
pub use layers::{LayerNorm, RMSNorm, PositionalEncoding};
|
|
||||||
pub use architectures::{TransformerConfig, TransformerBlock, GPTConfig};
|
|
||||||
pub use training::{TrainingConfig, TrainingLoop};
|
|
||||||
|
|
||||||
/// Current version of rtx-transformers
|
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
/// Verify transformer infrastructure is properly initialized
|
|
||||||
pub fn verify_infrastructure() -> Result<()> {
|
|
||||||
tracing::info!("RTX Transformers v{} - Verifying infrastructure", VERSION);
|
|
||||||
|
|
||||||
// Verify core dependencies are available
|
|
||||||
rtx_tensor::verify_backend()?;
|
|
||||||
rtx_autograd::verify_gradient_system()?;
|
|
||||||
rtx_runtime::verify_cuda_availability()?;
|
|
||||||
|
|
||||||
tracing::info!("RTX Transformers infrastructure verification complete");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
//! # RTX Transformers: Complete Transformer Training Infrastructure (Minimal Version)
|
|
||||||
//!
|
|
||||||
//! This is a minimal version to test compilation with rustg tools.
|
|
||||||
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
|
|
||||||
// Re-export key types for convenience
|
|
||||||
pub use error::{TransformerError, Result};
|
|
||||||
|
|
||||||
/// Current version of rtx-transformers
|
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
/// Simple test function to verify the crate loads
|
|
||||||
pub fn test_minimal() -> Result<()> {
|
|
||||||
tracing::info!("RTX Transformers v{} - Minimal test successful", VERSION);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
//! Batch 33B — Complete I-JEPA end-to-end forward pass integration.
|
|
||||||
//!
|
|
||||||
//! Wires together all JEPA components into a single `JepaFullPipeline`:
|
|
||||||
//! 1. Block masking — sample context and target patch indices.
|
|
||||||
//! 2. Context encoding — encode context patches with the online encoder.
|
|
||||||
//! 3. Prediction — narrow predictor transformer maps context → target space.
|
|
||||||
//! 4. Target encoding — encode target patches with the EMA encoder (no gradient).
|
|
||||||
//! 5. Loss — L2 between predicted and target representations.
|
|
||||||
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use super::jepa::{BlockMaskStrategy, JepaPredictor, JepaLossResult, jepa_loss};
|
|
||||||
use super::jepa_vit::{JepaEncoder, JepaViTConfig, CpuViTEncoder, EmaTargetEncoderDyn};
|
|
||||||
use super::jepa_data::{ImageRecord, JepaAugmentationPipeline, JepaDataConfig};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// JepaStepOutput
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Output of one `JepaFullPipeline` step.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct JepaStepOutput {
|
|
||||||
/// Per-block I-JEPA loss result.
|
|
||||||
pub loss_result: JepaLossResult,
|
|
||||||
/// Scalar loss (mean across target blocks).
|
|
||||||
pub loss: f32,
|
|
||||||
/// Context patch count.
|
|
||||||
pub n_context: usize,
|
|
||||||
/// Total target patch count.
|
|
||||||
pub n_target: usize,
|
|
||||||
/// Current EMA tau at the time of this step.
|
|
||||||
pub ema_tau: f64,
|
|
||||||
/// Wall-clock time for this step (µs).
|
|
||||||
pub step_us: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// JepaFullPipeline
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Complete I-JEPA training pipeline: context encoder + predictor + EMA target encoder.
|
|
||||||
///
|
|
||||||
/// This is the reference implementation of the full I-JEPA forward pass:
|
|
||||||
/// 1. Block masking: sample context and target patch indices.
|
|
||||||
/// 2. Context encoding: encode context patches.
|
|
||||||
/// 3. Prediction: predict target representations from context.
|
|
||||||
/// 4. Target encoding: encode target patches with EMA encoder (no gradient).
|
|
||||||
/// 5. Loss: L2 between predicted and target representations.
|
|
||||||
pub struct JepaFullPipeline {
|
|
||||||
/// Context (online) encoder.
|
|
||||||
pub context_encoder: Box<dyn JepaEncoder>,
|
|
||||||
/// Narrow predictor transformer (encoder_dim → predictor_dim → encoder_dim).
|
|
||||||
pub predictor: JepaPredictor,
|
|
||||||
/// EMA target encoder.
|
|
||||||
pub target_encoder: EmaTargetEncoderDyn,
|
|
||||||
/// Block masking strategy.
|
|
||||||
pub mask_strategy: BlockMaskStrategy,
|
|
||||||
/// Step counter.
|
|
||||||
pub step: usize,
|
|
||||||
/// EMA tau schedule: (tau_start, tau_end, total_steps).
|
|
||||||
tau_schedule: (f64, f64, usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JepaFullPipeline {
|
|
||||||
/// Create a new pipeline from a `JepaViTConfig` using a `CpuViTEncoder`.
|
|
||||||
///
|
|
||||||
/// A second independent `CpuViTEncoder` (tiny preset) is used for the EMA
|
|
||||||
/// target encoder. Its weights diverge immediately via the tau schedule, so
|
|
||||||
/// the asymmetry is expected at this stage.
|
|
||||||
pub fn from_vit_config(
|
|
||||||
vit_cfg: JepaViTConfig,
|
|
||||||
predictor_dim: usize,
|
|
||||||
predictor_depth: usize,
|
|
||||||
tau_start: f64,
|
|
||||||
tau_end: f64,
|
|
||||||
total_steps: usize,
|
|
||||||
) -> Self {
|
|
||||||
let embed_dim = vit_cfg.embed_dim;
|
|
||||||
let num_patches = vit_cfg.num_patches();
|
|
||||||
let num_heads = (predictor_dim / 64).max(1).min(8);
|
|
||||||
|
|
||||||
let context_enc: Box<dyn JepaEncoder> = Box::new(CpuViTEncoder::new(vit_cfg));
|
|
||||||
|
|
||||||
// Target encoder: independent CpuViTEncoder.
|
|
||||||
// EmaTargetEncoderDyn wraps it and updates tau on each step.
|
|
||||||
let target_enc_inner: Box<dyn JepaEncoder> =
|
|
||||||
Box::new(CpuViTEncoder::new(JepaViTConfig::tiny()));
|
|
||||||
let target_enc = EmaTargetEncoderDyn::from_encoder(target_enc_inner, tau_start);
|
|
||||||
|
|
||||||
let predictor = JepaPredictor::new(
|
|
||||||
embed_dim,
|
|
||||||
predictor_dim,
|
|
||||||
predictor_depth,
|
|
||||||
num_heads,
|
|
||||||
num_patches,
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
context_encoder: context_enc,
|
|
||||||
predictor,
|
|
||||||
target_encoder: target_enc,
|
|
||||||
mask_strategy: BlockMaskStrategy::default_ijepa(),
|
|
||||||
step: 0,
|
|
||||||
tau_schedule: (tau_start, tau_end, total_steps),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create with a custom pair of encoders (e.g. `GpuViTEncoder`).
|
|
||||||
///
|
|
||||||
/// `context_enc` is used as the online encoder; `target_enc_clone` is wrapped
|
|
||||||
/// in `EmaTargetEncoderDyn` as the EMA target encoder.
|
|
||||||
pub fn with_encoder(
|
|
||||||
context_enc: Box<dyn JepaEncoder>,
|
|
||||||
target_enc_clone: Box<dyn JepaEncoder>,
|
|
||||||
predictor_dim: usize,
|
|
||||||
predictor_depth: usize,
|
|
||||||
tau_start: f64,
|
|
||||||
tau_end: f64,
|
|
||||||
total_steps: usize,
|
|
||||||
) -> Self {
|
|
||||||
let embed_dim = context_enc.embed_dim();
|
|
||||||
let num_patches = context_enc.num_patches();
|
|
||||||
let num_heads = (predictor_dim / 64).max(1).min(8);
|
|
||||||
|
|
||||||
let predictor =
|
|
||||||
JepaPredictor::new(embed_dim, predictor_dim, predictor_depth, num_heads, num_patches);
|
|
||||||
let target_enc = EmaTargetEncoderDyn::from_encoder(target_enc_clone, tau_start);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
context_encoder: context_enc,
|
|
||||||
predictor,
|
|
||||||
target_encoder: target_enc,
|
|
||||||
mask_strategy: BlockMaskStrategy::default_ijepa(),
|
|
||||||
step: 0,
|
|
||||||
tau_schedule: (tau_start, tau_end, total_steps),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current EMA tau derived from the linear schedule.
|
|
||||||
pub fn current_tau(&self) -> f64 {
|
|
||||||
let (start, end, total) = self.tau_schedule;
|
|
||||||
if total == 0 {
|
|
||||||
return end;
|
|
||||||
}
|
|
||||||
start + (end - start) * (self.step as f64 / total as f64).min(1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run one training step with externally-provided patch indices.
|
|
||||||
///
|
|
||||||
/// Core I-JEPA forward pass:
|
|
||||||
/// context_encoder(context_patches) → predictor → compare with target_encoder(target_patches)
|
|
||||||
pub fn step_with_indices(
|
|
||||||
&mut self,
|
|
||||||
context_indices: &[usize],
|
|
||||||
target_indices: &[usize],
|
|
||||||
) -> JepaStepOutput {
|
|
||||||
let t0 = Instant::now();
|
|
||||||
self.step += 1;
|
|
||||||
let tau = self.current_tau();
|
|
||||||
|
|
||||||
// 1. Context encoding
|
|
||||||
let ctx_reps = self.context_encoder.encode(context_indices);
|
|
||||||
|
|
||||||
// 2. Target encoding (EMA, no gradient)
|
|
||||||
let tgt_reps = self.target_encoder.encode(target_indices);
|
|
||||||
|
|
||||||
// 3. Prediction: context representations → predicted target representations
|
|
||||||
let predictions = self.predictor.forward(&ctx_reps, context_indices, target_indices);
|
|
||||||
|
|
||||||
let encoder_dim = self.context_encoder.embed_dim();
|
|
||||||
|
|
||||||
// 4. Loss: predicted vs target (L2 in representation space)
|
|
||||||
let target_blocks = vec![target_indices.to_vec()];
|
|
||||||
let target_block_offsets = vec![0usize];
|
|
||||||
let loss_result = jepa_loss(
|
|
||||||
&predictions,
|
|
||||||
&tgt_reps,
|
|
||||||
encoder_dim,
|
|
||||||
&target_blocks,
|
|
||||||
&target_block_offsets,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 5. Update EMA tau
|
|
||||||
self.target_encoder.update_tau(tau);
|
|
||||||
|
|
||||||
let step_us = t0.elapsed().as_micros() as u64;
|
|
||||||
|
|
||||||
JepaStepOutput {
|
|
||||||
loss: loss_result.loss,
|
|
||||||
loss_result,
|
|
||||||
n_context: context_indices.len(),
|
|
||||||
n_target: target_indices.len(),
|
|
||||||
ema_tau: tau,
|
|
||||||
step_us,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run one training step using block masking (masks generated internally).
|
|
||||||
///
|
|
||||||
/// `image_seed` deterministically varies the mask per sample.
|
|
||||||
pub fn step_with_mask(&mut self, image_seed: u64) -> JepaStepOutput {
|
|
||||||
let num_patches = self.context_encoder.num_patches();
|
|
||||||
// Approximate a square grid; non-square images are rare in JEPA pre-training.
|
|
||||||
let grid = (num_patches as f64).sqrt() as usize;
|
|
||||||
let grid_h = grid;
|
|
||||||
let grid_w = (num_patches + grid - 1) / grid;
|
|
||||||
|
|
||||||
let mask = self.mask_strategy.generate(grid_h, grid_w, image_seed);
|
|
||||||
|
|
||||||
self.step_with_indices(&mask.context_indices, &mask.all_target_indices)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run one step with a real image: augment → mask → step.
|
|
||||||
///
|
|
||||||
/// The augmented pixel buffer is computed but not yet fed into the encoder
|
|
||||||
/// (CpuViTEncoder generates its own deterministic embeddings from patch indices).
|
|
||||||
/// A full implementation would pass `pixels` through a patch-embedding layer.
|
|
||||||
pub fn step_with_image(
|
|
||||||
&mut self,
|
|
||||||
image: &ImageRecord,
|
|
||||||
aug_pipeline: &JepaAugmentationPipeline,
|
|
||||||
seed: u64,
|
|
||||||
) -> JepaStepOutput {
|
|
||||||
// Augment the image (side-effectfully exercises the data pipeline).
|
|
||||||
let _pixels = aug_pipeline.process(image, seed);
|
|
||||||
// Delegate to mask-based step (pixel values not consumed by CpuViTEncoder).
|
|
||||||
self.step_with_mask(seed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::ssl::jepa_vit::JepaViTConfig;
|
|
||||||
use crate::ssl::jepa_data::{ImageRecord, JepaAugmentationPipeline, JepaDataConfig};
|
|
||||||
|
|
||||||
/// A tiny pipeline for fast tests: embed_dim=32, depth=1, 2 heads.
|
|
||||||
fn tiny_pipeline() -> JepaFullPipeline {
|
|
||||||
let vit_cfg = JepaViTConfig {
|
|
||||||
embed_dim: 32,
|
|
||||||
depth: 1,
|
|
||||||
num_heads: 2,
|
|
||||||
mlp_ratio: 2.0,
|
|
||||||
patch_size: 16,
|
|
||||||
image_size: 64,
|
|
||||||
};
|
|
||||||
JepaFullPipeline::from_vit_config(vit_cfg, 16, 1, 0.996, 1.0, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. step_with_indices returns finite loss
|
|
||||||
#[test]
|
|
||||||
fn test_step_with_indices_finite() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let out = p.step_with_indices(&[0, 1, 2, 3, 4, 5], &[6, 7, 8, 9]);
|
|
||||||
assert!(out.loss.is_finite(), "loss={}", out.loss);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. n_context matches input
|
|
||||||
#[test]
|
|
||||||
fn test_step_context_count() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let out = p.step_with_indices(&[0, 1, 2], &[4, 5]);
|
|
||||||
assert_eq!(out.n_context, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. n_target matches input
|
|
||||||
#[test]
|
|
||||||
fn test_step_target_count() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let out = p.step_with_indices(&[0, 1, 2], &[4, 5, 6, 7]);
|
|
||||||
assert_eq!(out.n_target, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. step counter increments
|
|
||||||
#[test]
|
|
||||||
fn test_step_counter() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
p.step_with_indices(&[0], &[1]);
|
|
||||||
p.step_with_indices(&[0], &[1]);
|
|
||||||
assert_eq!(p.step, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. step_with_mask produces finite loss
|
|
||||||
#[test]
|
|
||||||
fn test_step_with_mask_finite() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let out = p.step_with_mask(42);
|
|
||||||
assert!(out.loss.is_finite(), "mask-step loss={}", out.loss);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. step_with_mask: mask-generated n_context > 0
|
|
||||||
#[test]
|
|
||||||
fn test_step_with_mask_context_nonzero() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let out = p.step_with_mask(0);
|
|
||||||
assert!(out.n_context > 0, "must have context patches");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. step_with_mask: mask-generated n_target > 0
|
|
||||||
#[test]
|
|
||||||
fn test_step_with_mask_target_nonzero() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let out = p.step_with_mask(0);
|
|
||||||
assert!(out.n_target > 0, "must have target patches");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. current_tau at step 0 = tau_start
|
|
||||||
#[test]
|
|
||||||
fn test_tau_at_step_zero() {
|
|
||||||
let p = tiny_pipeline();
|
|
||||||
let tau = p.current_tau();
|
|
||||||
assert!((tau - 0.996).abs() < 1e-6, "tau={tau}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. tau increases toward tau_end over steps
|
|
||||||
#[test]
|
|
||||||
fn test_tau_increases() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let tau0 = p.current_tau();
|
|
||||||
p.step_with_mask(0);
|
|
||||||
p.step_with_mask(1);
|
|
||||||
let tau1 = p.current_tau();
|
|
||||||
assert!(tau1 >= tau0, "tau must not decrease");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10. step_with_image runs without panic
|
|
||||||
#[test]
|
|
||||||
fn test_step_with_image() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let image = ImageRecord {
|
|
||||||
pixels: vec![0.5f32; 64 * 64 * 3],
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
channels: 3,
|
|
||||||
label: None,
|
|
||||||
key: "test".to_string(),
|
|
||||||
};
|
|
||||||
// Use the actual JepaDataConfig fields (not those in the spec).
|
|
||||||
let cfg = JepaDataConfig {
|
|
||||||
image_size: 64,
|
|
||||||
patch_size: 16,
|
|
||||||
batch_size: 1,
|
|
||||||
num_workers: 0,
|
|
||||||
shard_paths: Vec::new(),
|
|
||||||
scale_range: (0.2, 1.0),
|
|
||||||
ratio_range: (0.75, 1.33),
|
|
||||||
use_horizontal_flip: false,
|
|
||||||
imagenet_normalize: false,
|
|
||||||
};
|
|
||||||
let aug = JepaAugmentationPipeline::from_config(&cfg);
|
|
||||||
let out = p.step_with_image(&image, &aug, 42);
|
|
||||||
assert!(out.loss.is_finite());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11. step_us is a valid u64 (timing works)
|
|
||||||
#[test]
|
|
||||||
fn test_step_timing() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let out = p.step_with_mask(0);
|
|
||||||
// step_us can be 0 on very fast systems; just assert it is a valid value.
|
|
||||||
let _ = out.step_us;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12. JepaStepOutput.loss_result.loss == JepaStepOutput.loss
|
|
||||||
#[test]
|
|
||||||
fn test_loss_consistency() {
|
|
||||||
let mut p = tiny_pipeline();
|
|
||||||
let out = p.step_with_indices(&[0, 1, 2, 3], &[5, 6, 7, 8]);
|
|
||||||
assert!(
|
|
||||||
(out.loss - out.loss_result.loss).abs() < 1e-6,
|
|
||||||
"loss={} vs loss_result.loss={}",
|
|
||||||
out.loss,
|
|
||||||
out.loss_result.loss
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,647 +0,0 @@
|
|||||||
//! SimMIM (Simple Masked Image Modeling) Implementation
|
|
||||||
//!
|
|
||||||
//! SimMIM is a simple framework for masked image modeling that directly predicts
|
|
||||||
//! raw pixel values of masked patches. Based on "SimMIM: A Simple Framework for
|
|
||||||
//! Masked Image Modeling" (Xie et al., 2021).
|
|
||||||
//!
|
|
||||||
//! Key features:
|
|
||||||
//! - Random masking of image patches (60% for ViT, 32% for Swin)
|
|
||||||
//! - Direct raw pixel regression (no tokenizer needed)
|
|
||||||
//! - Simple prediction head (linear layer)
|
|
||||||
//! - L1 loss for pixel prediction
|
|
||||||
//! - Support for both ViT and Swin Transformer
|
|
||||||
//!
|
|
||||||
//! Algorithm:
|
|
||||||
//! 1. Random masking: Randomly mask patches
|
|
||||||
//! 2. Mask tokens: Replace masked patches with learnable mask token
|
|
||||||
//! 3. Encoder: Pass through ViT/Swin transformer
|
|
||||||
//! 4. Prediction: Simple linear layer to predict raw pixels
|
|
||||||
//! 5. Loss: L1 loss between predicted and actual pixels (only on masked patches)
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
|
|
||||||
/// Configuration for different encoder types
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum EncoderType {
|
|
||||||
/// Vision Transformer encoder
|
|
||||||
ViT,
|
|
||||||
/// Swin Transformer encoder
|
|
||||||
Swin,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type of prediction head
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum PredictionHeadType {
|
|
||||||
/// Simple linear layer
|
|
||||||
Linear,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loss function type
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum LossType {
|
|
||||||
/// L1 (MAE) loss
|
|
||||||
L1,
|
|
||||||
/// L2 (MSE) loss
|
|
||||||
L2,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// SimMIM configuration parameters
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SimMIMConfig {
|
|
||||||
/// Fraction of patches to mask
|
|
||||||
pub mask_ratio: f32,
|
|
||||||
/// Mask patch size (for hierarchical masking in Swin)
|
|
||||||
pub mask_patch_size: usize,
|
|
||||||
/// Type of prediction head
|
|
||||||
pub prediction_head: PredictionHeadType,
|
|
||||||
/// Loss function type
|
|
||||||
pub loss_type: LossType,
|
|
||||||
/// Whether to normalize pixel targets
|
|
||||||
pub norm_pix_loss: bool,
|
|
||||||
/// Encoder embedding dimension
|
|
||||||
pub encoder_dim: usize,
|
|
||||||
/// Image patch size
|
|
||||||
pub patch_size: usize,
|
|
||||||
/// Type of encoder (ViT or Swin)
|
|
||||||
pub encoder_type: EncoderType,
|
|
||||||
/// Input image size
|
|
||||||
pub image_size: usize,
|
|
||||||
/// Number of input channels
|
|
||||||
pub in_channels: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SimMIMConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
mask_ratio: 0.6,
|
|
||||||
mask_patch_size: 32,
|
|
||||||
prediction_head: PredictionHeadType::Linear,
|
|
||||||
loss_type: LossType::L1,
|
|
||||||
norm_pix_loss: false,
|
|
||||||
encoder_dim: 768,
|
|
||||||
patch_size: 16,
|
|
||||||
encoder_type: EncoderType::ViT,
|
|
||||||
image_size: 224,
|
|
||||||
in_channels: 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimMIMConfig {
|
|
||||||
/// Create configuration optimized for ViT
|
|
||||||
pub fn for_vit() -> Self {
|
|
||||||
Self {
|
|
||||||
encoder_type: EncoderType::ViT,
|
|
||||||
mask_ratio: 0.6,
|
|
||||||
mask_patch_size: 16,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create configuration optimized for Swin Transformer
|
|
||||||
pub fn for_swin() -> Self {
|
|
||||||
Self {
|
|
||||||
encoder_type: EncoderType::Swin,
|
|
||||||
mask_ratio: 0.32, // Lower mask ratio for hierarchical architecture
|
|
||||||
mask_patch_size: 32, // Larger patch size for hierarchical masking
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set mask ratio
|
|
||||||
pub fn with_mask_ratio(mut self, mask_ratio: f32) -> Self {
|
|
||||||
self.mask_ratio = mask_ratio.clamp(0.0, 1.0);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set encoder type
|
|
||||||
pub fn with_encoder_type(mut self, encoder_type: EncoderType) -> Self {
|
|
||||||
self.encoder_type = encoder_type;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set whether to normalize pixel loss
|
|
||||||
pub fn with_norm_pix_loss(mut self, norm_pix_loss: bool) -> Self {
|
|
||||||
self.norm_pix_loss = norm_pix_loss;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set encoder dimension
|
|
||||||
pub fn with_encoder_dim(mut self, encoder_dim: usize) -> Self {
|
|
||||||
self.encoder_dim = encoder_dim;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result from random patch masking
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PatchMaskResult {
|
|
||||||
/// Boolean mask for each patch (true = masked, false = visible)
|
|
||||||
pub mask_indices: Vec<Vec<bool>>,
|
|
||||||
/// Indices of visible patches for each batch item
|
|
||||||
pub visible_indices: Vec<Vec<usize>>,
|
|
||||||
/// Total number of masked patches per batch item
|
|
||||||
pub num_masked_patches: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Random patch masker for SimMIM
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RandomPatchMasker {
|
|
||||||
mask_ratio: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RandomPatchMasker {
|
|
||||||
/// Create new random patch masker
|
|
||||||
pub fn new(mask_ratio: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
mask_ratio: mask_ratio.clamp(0.0, 1.0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get mask ratio
|
|
||||||
pub fn mask_ratio(&self) -> f32 {
|
|
||||||
self.mask_ratio
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate random patch mask for a batch
|
|
||||||
pub fn mask_patches(&self, patches: &Tensor, seed: Option<u64>) -> Result<PatchMaskResult> {
|
|
||||||
let shape = patches.shape();
|
|
||||||
if shape.len() != 3 {
|
|
||||||
return Err(TransformerError::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(TransformerError::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 {
|
|
||||||
// Create all patch indices
|
|
||||||
let mut indices: Vec<usize> = (0..num_patches).collect();
|
|
||||||
|
|
||||||
// Shuffle indices deterministically
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select visible patches
|
|
||||||
let visible = indices[..num_visible].to_vec();
|
|
||||||
visible_indices.push(visible.clone());
|
|
||||||
|
|
||||||
// Create boolean mask
|
|
||||||
let mut mask = vec![true; num_patches]; // Start with all masked
|
|
||||||
for &idx in &visible {
|
|
||||||
mask[idx] = false; // Mark visible patches
|
|
||||||
}
|
|
||||||
mask_indices.push(mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(PatchMaskResult {
|
|
||||||
mask_indices,
|
|
||||||
visible_indices,
|
|
||||||
num_masked_patches: num_masked,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Learnable mask token embedding
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MaskTokenEmbedding {
|
|
||||||
token: Arc<RwLock<Tensor>>,
|
|
||||||
embed_dim: usize,
|
|
||||||
device: Device,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MaskTokenEmbedding {
|
|
||||||
/// Create new mask token embedding
|
|
||||||
pub fn new(embed_dim: usize, device: &Device) -> Result<Self> {
|
|
||||||
let token = Tensor::randn(vec![1, embed_dim], DType::F32, device)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
token: Arc::new(RwLock::new(token)),
|
|
||||||
embed_dim,
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get embedding dimension
|
|
||||||
pub fn embed_dim(&self) -> usize {
|
|
||||||
self.embed_dim
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the mask token
|
|
||||||
pub fn get_token(&self) -> Result<Tensor> {
|
|
||||||
let token = self.token.read();
|
|
||||||
Ok(token.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Broadcast mask token to specified shape
|
|
||||||
pub fn broadcast(&self, batch_size: usize, num_masked: usize) -> Result<Tensor> {
|
|
||||||
let token = self.token.read();
|
|
||||||
|
|
||||||
// Create broadcasted tensor
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tensor::from_data(
|
|
||||||
broadcasted_data,
|
|
||||||
vec![batch_size, num_masked, self.embed_dim],
|
|
||||||
DType::F32,
|
|
||||||
&self.device,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Linear prediction head for pixel reconstruction
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct LinearPredictionHead {
|
|
||||||
linear: Arc<RwLock<Tensor>>,
|
|
||||||
bias: Arc<RwLock<Tensor>>,
|
|
||||||
input_dim: usize,
|
|
||||||
output_dim: usize,
|
|
||||||
device: Device,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinearPredictionHead {
|
|
||||||
/// Create new linear prediction head
|
|
||||||
pub fn new(input_dim: usize, output_dim: usize, device: &Device) -> Result<Self> {
|
|
||||||
let linear = Tensor::randn(vec![input_dim, output_dim], DType::F32, device)?;
|
|
||||||
let bias = Tensor::zeros(vec![output_dim], device)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
linear: Arc::new(RwLock::new(linear)),
|
|
||||||
bias: Arc::new(RwLock::new(bias)),
|
|
||||||
input_dim,
|
|
||||||
output_dim,
|
|
||||||
device: device.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get input dimension
|
|
||||||
pub fn input_dim(&self) -> usize {
|
|
||||||
self.input_dim
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get output dimension
|
|
||||||
pub fn output_dim(&self) -> usize {
|
|
||||||
self.output_dim
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Forward pass through prediction head
|
|
||||||
pub fn forward(&self, input: &Tensor) -> Result<Tensor> {
|
|
||||||
let linear = self.linear.read();
|
|
||||||
let bias = self.bias.read();
|
|
||||||
|
|
||||||
let output = input.matmul(&*linear)?;
|
|
||||||
output.add(&*bias)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// SimMIM loss computation utilities
|
|
||||||
pub struct SimMIMLoss;
|
|
||||||
|
|
||||||
impl SimMIMLoss {
|
|
||||||
/// Compute L1 (MAE) loss between predicted and target pixels
|
|
||||||
pub fn compute_l1_loss(predicted: &Tensor, target: &Tensor) -> Result<Tensor> {
|
|
||||||
// Check tensor shapes match
|
|
||||||
let pred_shape = predicted.shape();
|
|
||||||
let target_shape = target.shape();
|
|
||||||
|
|
||||||
if pred_shape != target_shape {
|
|
||||||
return Err(TransformerError::InvalidInput(
|
|
||||||
format!("Predicted and target shapes must match: {:?} vs {:?}", pred_shape, target_shape)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute L1 loss: mean(|predicted - target|)
|
|
||||||
let diff = predicted.sub(target)?;
|
|
||||||
let abs_diff = diff.abs()?;
|
|
||||||
abs_diff.mean(&[])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute normalized L1 loss (normalize pixel targets first)
|
|
||||||
pub fn compute_l1_loss_normalized(predicted: &Tensor, target: &Tensor) -> Result<Tensor> {
|
|
||||||
// Normalize target pixels to have zero mean and unit variance per patch
|
|
||||||
let normalized_target = Self::normalize_pixels(target)?;
|
|
||||||
let normalized_predicted = Self::normalize_pixels(predicted)?;
|
|
||||||
|
|
||||||
Self::compute_l1_loss(&normalized_predicted, &normalized_target)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute L2 (MSE) loss between predicted and target pixels
|
|
||||||
pub fn compute_l2_loss(predicted: &Tensor, target: &Tensor) -> Result<Tensor> {
|
|
||||||
let pred_shape = predicted.shape();
|
|
||||||
let target_shape = target.shape();
|
|
||||||
|
|
||||||
if pred_shape != target_shape {
|
|
||||||
return Err(TransformerError::InvalidInput(
|
|
||||||
format!("Predicted and target shapes must match: {:?} vs {:?}", pred_shape, target_shape)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute L2 loss: mean((predicted - target)^2)
|
|
||||||
let diff = predicted.sub(target)?;
|
|
||||||
let squared_diff = diff.pow_scalar(2.0)?;
|
|
||||||
squared_diff.mean(&[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_pixels(pixels: &Tensor) -> Result<Tensor> {
|
|
||||||
// Simple normalization: (x - mean) / std per patch
|
|
||||||
let mean = pixels.mean(&[2], true)?; // Keep last dimension for broadcasting
|
|
||||||
let centered = pixels.sub(&mean)?;
|
|
||||||
|
|
||||||
let var = centered.pow_scalar(2.0)?.mean(&[2], true)?;
|
|
||||||
let std = var.sqrt()?;
|
|
||||||
|
|
||||||
// Add small epsilon to avoid division by zero
|
|
||||||
let eps = Tensor::full(&std.shape(), 1e-6, DType::F32, pixels.device())?;
|
|
||||||
let std_safe = std.add(&eps)?;
|
|
||||||
|
|
||||||
centered.div(&std_safe)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result from SimMIM training step
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SimMIMTrainingResult {
|
|
||||||
/// Reconstruction loss
|
|
||||||
pub loss: Tensor,
|
|
||||||
/// Number of masked patches in this batch
|
|
||||||
pub num_masked_patches: usize,
|
|
||||||
/// Actual mask ratio used
|
|
||||||
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<Tensor>>, // Simplified encoder (in practice would use full ViT/Swin)
|
|
||||||
device: Device,
|
|
||||||
is_training: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimMIMTrainer {
|
|
||||||
/// Create new SimMIM trainer
|
|
||||||
pub fn new(
|
|
||||||
config: SimMIMConfig,
|
|
||||||
in_channels: usize,
|
|
||||||
image_size: usize,
|
|
||||||
device: &Device,
|
|
||||||
) -> Result<Self> {
|
|
||||||
// Create masker
|
|
||||||
let masker = RandomPatchMasker::new(config.mask_ratio);
|
|
||||||
|
|
||||||
// Create mask token embedding
|
|
||||||
let mask_token = MaskTokenEmbedding::new(config.encoder_dim, device)?;
|
|
||||||
|
|
||||||
// Create prediction head
|
|
||||||
let patch_volume = config.patch_size * config.patch_size * in_channels;
|
|
||||||
let prediction_head = LinearPredictionHead::new(config.encoder_dim, patch_volume, device)?;
|
|
||||||
|
|
||||||
// Create simplified encoder (in practice would use full transformer)
|
|
||||||
let num_patches = (image_size / config.patch_size).pow(2);
|
|
||||||
let encoder = Tensor::randn(
|
|
||||||
vec![patch_volume, config.encoder_dim],
|
|
||||||
DType::F32,
|
|
||||||
device,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
masker,
|
|
||||||
mask_token,
|
|
||||||
prediction_head,
|
|
||||||
encoder: Arc::new(RwLock::new(encoder)),
|
|
||||||
device: device.clone(),
|
|
||||||
is_training: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get configuration
|
|
||||||
pub fn config(&self) -> &SimMIMConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get device
|
|
||||||
pub fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set to training mode
|
|
||||||
pub fn train(&mut self) {
|
|
||||||
self.is_training = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set to evaluation mode
|
|
||||||
pub fn eval(&mut self) {
|
|
||||||
self.is_training = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if in training mode
|
|
||||||
pub fn is_training(&self) -> bool {
|
|
||||||
self.is_training
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform one SimMIM training step
|
|
||||||
pub fn train_step(&mut self, images: &Tensor, seed: Option<u64>) -> Result<SimMIMTrainingResult> {
|
|
||||||
let batch_size = images.shape()[0];
|
|
||||||
|
|
||||||
// Step 1: Convert images to patches
|
|
||||||
let patches = self.extract_patches(images)?;
|
|
||||||
let target_pixels = patches.clone(); // Keep original patches as targets
|
|
||||||
|
|
||||||
// Step 2: Generate random mask
|
|
||||||
let mask_result = self.masker.mask_patches(&patches, seed)?;
|
|
||||||
|
|
||||||
// Step 3: Apply masking and encode
|
|
||||||
let masked_patches = self.apply_mask_tokens(&patches, &mask_result)?;
|
|
||||||
let encoded_features = self.encode_patches(&masked_patches)?;
|
|
||||||
|
|
||||||
// Step 4: Predict pixels for masked patches only
|
|
||||||
let masked_features = self.extract_masked_features(&encoded_features, &mask_result)?;
|
|
||||||
let predicted_pixels = self.prediction_head.forward(&masked_features)?;
|
|
||||||
|
|
||||||
// Step 5: Extract target pixels for masked patches
|
|
||||||
let target_masked_pixels = self.extract_masked_targets(&target_pixels, &mask_result)?;
|
|
||||||
|
|
||||||
// Step 6: Compute loss (L1 loss on masked patches only)
|
|
||||||
let loss = match self.config.loss_type {
|
|
||||||
LossType::L1 => {
|
|
||||||
if self.config.norm_pix_loss {
|
|
||||||
SimMIMLoss::compute_l1_loss_normalized(&predicted_pixels, &target_masked_pixels)?
|
|
||||||
} else {
|
|
||||||
SimMIMLoss::compute_l1_loss(&predicted_pixels, &target_masked_pixels)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LossType::L2 => {
|
|
||||||
SimMIMLoss::compute_l2_loss(&predicted_pixels, &target_masked_pixels)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(SimMIMTrainingResult {
|
|
||||||
loss,
|
|
||||||
num_masked_patches: mask_result.num_masked_patches,
|
|
||||||
mask_ratio: self.masker.mask_ratio(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract features in evaluation mode (no masking)
|
|
||||||
pub fn extract_features(&self, images: &Tensor) -> Result<Tensor> {
|
|
||||||
let patches = self.extract_patches(images)?;
|
|
||||||
let encoded = self.encode_patches(&patches)?;
|
|
||||||
|
|
||||||
// Global average pooling to get image-level features
|
|
||||||
encoded.mean(&[1]) // Average over patch dimension
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_patches(&self, images: &Tensor) -> Result<Tensor> {
|
|
||||||
// Simplified patch extraction - reshape image into patches
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Simulate patch extraction by reshaping
|
|
||||||
images.reshape(&[batch_size, num_patches, patch_volume])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_mask_tokens(&self, patches: &Tensor, mask_result: &PatchMaskResult) -> Result<Tensor> {
|
|
||||||
let shape = patches.shape();
|
|
||||||
let batch_size = shape[0];
|
|
||||||
let num_patches = shape[1];
|
|
||||||
let patch_volume = shape[2];
|
|
||||||
|
|
||||||
// Create tensor with mask tokens replacing masked patches
|
|
||||||
let mut masked_data = vec![0.0f32; batch_size * num_patches * patch_volume];
|
|
||||||
let patches_data = patches.to_vec::<f32>()?;
|
|
||||||
|
|
||||||
// Get mask token data
|
|
||||||
let mask_token = self.mask_token.get_token()?;
|
|
||||||
let mask_token_data = mask_token.to_vec::<f32>()?;
|
|
||||||
|
|
||||||
for b in 0..batch_size {
|
|
||||||
for p in 0..num_patches {
|
|
||||||
let base_idx = b * num_patches * patch_volume + p * patch_volume;
|
|
||||||
|
|
||||||
if mask_result.mask_indices[b][p] {
|
|
||||||
// Use mask token (broadcast to patch volume)
|
|
||||||
for i in 0..patch_volume {
|
|
||||||
masked_data[base_idx + i] = mask_token_data[i % self.config.encoder_dim];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Keep original patch
|
|
||||||
for i in 0..patch_volume {
|
|
||||||
masked_data[base_idx + i] = patches_data[base_idx + i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tensor::from_data(masked_data, shape, &device)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_patches(&self, patches: &Tensor) -> Result<Tensor> {
|
|
||||||
let encoder = self.encoder.read();
|
|
||||||
patches.matmul(&*encoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_masked_features(&self, features: &Tensor, mask_result: &PatchMaskResult) -> Result<Tensor> {
|
|
||||||
let shape = features.shape();
|
|
||||||
let batch_size = shape[0];
|
|
||||||
let num_patches = shape[1];
|
|
||||||
let embed_dim = shape[2];
|
|
||||||
let num_masked = mask_result.num_masked_patches;
|
|
||||||
|
|
||||||
let mut masked_features = vec![0.0f32; batch_size * num_masked * embed_dim];
|
|
||||||
let features_data = features.to_vec::<f32>()?;
|
|
||||||
|
|
||||||
for b in 0..batch_size {
|
|
||||||
let mut masked_idx = 0;
|
|
||||||
for p in 0..num_patches {
|
|
||||||
if mask_result.mask_indices[b][p] && masked_idx < num_masked {
|
|
||||||
let src_base = b * num_patches * embed_dim + p * embed_dim;
|
|
||||||
let dst_base = b * num_masked * embed_dim + masked_idx * embed_dim;
|
|
||||||
|
|
||||||
for d in 0..embed_dim {
|
|
||||||
masked_features[dst_base + d] = features_data[src_base + d];
|
|
||||||
}
|
|
||||||
masked_idx += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tensor::from_data(
|
|
||||||
masked_features,
|
|
||||||
vec![batch_size, num_masked, embed_dim],
|
|
||||||
DType::F32,
|
|
||||||
&self.device,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_masked_targets(&self, targets: &Tensor, mask_result: &PatchMaskResult) -> Result<Tensor> {
|
|
||||||
let shape = targets.shape();
|
|
||||||
let batch_size = shape[0];
|
|
||||||
let num_patches = shape[1];
|
|
||||||
let patch_volume = shape[2];
|
|
||||||
let num_masked = mask_result.num_masked_patches;
|
|
||||||
|
|
||||||
let mut masked_targets = vec![0.0f32; batch_size * num_masked * patch_volume];
|
|
||||||
let targets_data = targets.to_vec::<f32>()?;
|
|
||||||
|
|
||||||
for b in 0..batch_size {
|
|
||||||
let mut masked_idx = 0;
|
|
||||||
for p in 0..num_patches {
|
|
||||||
if mask_result.mask_indices[b][p] && masked_idx < num_masked {
|
|
||||||
let src_base = b * num_patches * patch_volume + p * patch_volume;
|
|
||||||
let dst_base = b * num_masked * patch_volume + masked_idx * patch_volume;
|
|
||||||
|
|
||||||
for d in 0..patch_volume {
|
|
||||||
masked_targets[dst_base + d] = targets_data[src_base + d];
|
|
||||||
}
|
|
||||||
masked_idx += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tensor::from_data(
|
|
||||||
masked_targets,
|
|
||||||
vec![batch_size, num_masked, patch_volume],
|
|
||||||
DType::F32,
|
|
||||||
&self.device,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
//! Simple standalone test for SimMIM compilation
|
|
||||||
//! This test verifies the basic structure works before running the full test suite
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_basic_compilation() {
|
|
||||||
// Just test that types can be created
|
|
||||||
use super::simmim::*;
|
|
||||||
|
|
||||||
let mask_ratio = 0.6;
|
|
||||||
let _config = SimMIMConfig::default().with_mask_ratio(mask_ratio);
|
|
||||||
|
|
||||||
// Test basic masker creation
|
|
||||||
let _masker = RandomPatchMasker::new(mask_ratio);
|
|
||||||
|
|
||||||
assert!(true, "SimMIM types compile successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_config_methods() {
|
|
||||||
use super::simmim::*;
|
|
||||||
|
|
||||||
let config = SimMIMConfig::default()
|
|
||||||
.with_mask_ratio(0.75)
|
|
||||||
.with_encoder_type(EncoderType::Swin);
|
|
||||||
|
|
||||||
assert_eq!(config.mask_ratio, 0.75);
|
|
||||||
assert_eq!(config.encoder_type, EncoderType::Swin);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_enums() {
|
|
||||||
use super::simmim::*;
|
|
||||||
|
|
||||||
assert_eq!(EncoderType::ViT, EncoderType::ViT);
|
|
||||||
assert_eq!(PredictionHeadType::Linear, PredictionHeadType::Linear);
|
|
||||||
assert_eq!(LossType::L1, LossType::L1);
|
|
||||||
}
|
|
||||||
@@ -1,522 +0,0 @@
|
|||||||
//! Comprehensive test suite for SimMIM (Simple Masked Image Modeling) implementation
|
|
||||||
//!
|
|
||||||
//! Tests follow strict TDD - these failing tests drive the implementation.
|
|
||||||
//! All tests must pass without mocks, stubs, or TODOs.
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use super::simmim::*;
|
|
||||||
|
|
||||||
/// Test random patch masking functionality
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod random_masking_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_random_masker_creation() {
|
|
||||||
let mask_ratio = 0.6;
|
|
||||||
let masker = RandomPatchMasker::new(mask_ratio);
|
|
||||||
assert_eq!(masker.mask_ratio(), mask_ratio);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_random_masker_mask_ratio_validation() {
|
|
||||||
// Test valid mask ratios
|
|
||||||
let valid_ratios = [0.0, 0.3, 0.6, 0.75, 1.0];
|
|
||||||
for ratio in valid_ratios {
|
|
||||||
let masker = RandomPatchMasker::new(ratio);
|
|
||||||
assert!((masker.mask_ratio() - ratio).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_random_patch_masking() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let masker = RandomPatchMasker::new(0.6);
|
|
||||||
|
|
||||||
let batch_size = 2;
|
|
||||||
let num_patches = 196; // 14x14 patches
|
|
||||||
let embed_dim = 768;
|
|
||||||
|
|
||||||
let patches = Tensor::randn(vec![batch_size, num_patches, embed_dim], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let result = masker.mask_patches(&patches, Some(42)).unwrap();
|
|
||||||
|
|
||||||
// Check mask structure
|
|
||||||
assert_eq!(result.mask_indices.len(), batch_size);
|
|
||||||
for mask in &result.mask_indices {
|
|
||||||
assert_eq!(mask.len(), num_patches);
|
|
||||||
|
|
||||||
let masked_count = mask.iter().filter(|&&x| x).count();
|
|
||||||
let visible_count = mask.iter().filter(|&&x| !x).count();
|
|
||||||
|
|
||||||
// Should mask approximately 60% of patches
|
|
||||||
let expected_masked = (num_patches as f32 * 0.6).round() as usize;
|
|
||||||
assert!((masked_count as i32 - expected_masked as i32).abs() <= 2);
|
|
||||||
assert_eq!(masked_count + visible_count, num_patches);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check visible indices consistency
|
|
||||||
assert_eq!(result.visible_indices.len(), batch_size);
|
|
||||||
for (b, indices) in result.visible_indices.iter().enumerate() {
|
|
||||||
for &idx in indices {
|
|
||||||
assert!(!result.mask_indices[b][idx]); // visible indices should not be masked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_random_masking_deterministic_with_seed() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let masker = RandomPatchMasker::new(0.6);
|
|
||||||
|
|
||||||
let patches = Tensor::randn(vec![1, 196, 768], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
// Same seed should produce same mask
|
|
||||||
let result1 = masker.mask_patches(&patches, Some(42)).unwrap();
|
|
||||||
let result2 = masker.mask_patches(&patches, Some(42)).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result1.mask_indices, result2.mask_indices);
|
|
||||||
assert_eq!(result1.visible_indices, result2.visible_indices);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_different_mask_ratios() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let patches = Tensor::randn(vec![1, 196, 768], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let ratios = [0.3, 0.6, 0.75];
|
|
||||||
for ratio in ratios {
|
|
||||||
let masker = RandomPatchMasker::new(ratio);
|
|
||||||
let result = masker.mask_patches(&patches, Some(42)).unwrap();
|
|
||||||
|
|
||||||
let masked_count = result.mask_indices[0].iter().filter(|&&x| x).count();
|
|
||||||
let expected = (196.0 * ratio).round() as usize;
|
|
||||||
|
|
||||||
// Allow small deviation due to rounding
|
|
||||||
assert!((masked_count as i32 - expected as i32).abs() <= 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test mask token embedding functionality
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod mask_token_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mask_token_creation() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let embed_dim = 768;
|
|
||||||
|
|
||||||
let mask_token = MaskTokenEmbedding::new(embed_dim, &device).unwrap();
|
|
||||||
assert_eq!(mask_token.embed_dim(), embed_dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mask_token_shape() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let embed_dim = 512;
|
|
||||||
|
|
||||||
let mask_token = MaskTokenEmbedding::new(embed_dim, &device).unwrap();
|
|
||||||
let token = mask_token.get_token().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(token.shape(), vec![1, embed_dim]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mask_token_broadcast() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let embed_dim = 768;
|
|
||||||
let batch_size = 4;
|
|
||||||
let num_masked = 100;
|
|
||||||
|
|
||||||
let mask_token = MaskTokenEmbedding::new(embed_dim, &device).unwrap();
|
|
||||||
let broadcasted = mask_token.broadcast(batch_size, num_masked).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(broadcasted.shape(), vec![batch_size, num_masked, embed_dim]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test linear prediction head functionality
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod prediction_head_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_linear_prediction_head_creation() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let embed_dim = 768;
|
|
||||||
let patch_size = 16;
|
|
||||||
let in_channels = 3;
|
|
||||||
|
|
||||||
let output_dim = patch_size * patch_size * in_channels; // 768
|
|
||||||
let head = LinearPredictionHead::new(embed_dim, output_dim, &device).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(head.input_dim(), embed_dim);
|
|
||||||
assert_eq!(head.output_dim(), output_dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_prediction_head_forward() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let embed_dim = 768;
|
|
||||||
let output_dim = 768; // 16x16x3
|
|
||||||
let batch_size = 2;
|
|
||||||
let num_masked = 100;
|
|
||||||
|
|
||||||
let head = LinearPredictionHead::new(embed_dim, output_dim, &device).unwrap();
|
|
||||||
let input = Tensor::randn(vec![batch_size, num_masked, embed_dim], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let output = head.forward(&input).unwrap();
|
|
||||||
assert_eq!(output.shape(), vec![batch_size, num_masked, output_dim]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_prediction_head_different_dimensions() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
// Test various common configurations
|
|
||||||
let configs = vec![
|
|
||||||
(768, 768), // ViT-Base 16x16 patch
|
|
||||||
(1024, 1024), // ViT-Large 16x16 patch
|
|
||||||
(512, 3072), // Custom config
|
|
||||||
];
|
|
||||||
|
|
||||||
for (input_dim, output_dim) in configs {
|
|
||||||
let head = LinearPredictionHead::new(input_dim, output_dim, &device).unwrap();
|
|
||||||
let input = Tensor::randn(vec![1, 10, input_dim], DType::F32, &device).unwrap();
|
|
||||||
let output = head.forward(&input).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(output.shape(), vec![1, 10, output_dim]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test L1 loss computation
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod loss_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_l1_loss_computation() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let batch_size = 2;
|
|
||||||
let num_patches = 10;
|
|
||||||
let patch_dim = 768;
|
|
||||||
|
|
||||||
let predicted = Tensor::randn(vec![batch_size, num_patches, patch_dim], DType::F32, &device).unwrap();
|
|
||||||
let target = Tensor::randn(vec![batch_size, num_patches, patch_dim], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let loss = SimMIMLoss::compute_l1_loss(&predicted, &target).unwrap();
|
|
||||||
|
|
||||||
// Loss should be a scalar
|
|
||||||
assert_eq!(loss.shape(), vec![]);
|
|
||||||
|
|
||||||
// Loss should be non-negative
|
|
||||||
let loss_value = loss.to_vec::<f32>().unwrap()[0];
|
|
||||||
assert!(loss_value >= 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_l1_loss_identical_inputs() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let input = Tensor::ones(vec![2, 5, 100], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let loss = SimMIMLoss::compute_l1_loss(&input, &input).unwrap();
|
|
||||||
let loss_value = loss.to_vec::<f32>().unwrap()[0];
|
|
||||||
|
|
||||||
// Loss should be zero for identical inputs
|
|
||||||
assert!(loss_value < 1e-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_l1_loss_known_values() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
// Create tensors with known values for deterministic testing
|
|
||||||
let predicted = Tensor::from_data(vec![1.0, 2.0, 3.0], vec![1, 3, 1], DType::F32, &device).unwrap();
|
|
||||||
let target = Tensor::from_data(vec![2.0, 3.0, 1.0], vec![1, 3, 1], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let loss = SimMIMLoss::compute_l1_loss(&predicted, &target).unwrap();
|
|
||||||
let loss_value = loss.to_vec::<f32>().unwrap()[0];
|
|
||||||
|
|
||||||
// Expected L1 loss: mean(|1-2| + |2-3| + |3-1|) = mean(1 + 1 + 2) = 4/3
|
|
||||||
let expected = 4.0 / 3.0;
|
|
||||||
assert!((loss_value - expected).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_l1_loss_with_normalization() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let batch_size = 2;
|
|
||||||
let num_patches = 8;
|
|
||||||
let patch_dim = 768;
|
|
||||||
|
|
||||||
let predicted = Tensor::randn(vec![batch_size, num_patches, patch_dim], DType::F32, &device).unwrap();
|
|
||||||
let target = Tensor::randn(vec![batch_size, num_patches, patch_dim], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let loss_normalized = SimMIMLoss::compute_l1_loss_normalized(&predicted, &target).unwrap();
|
|
||||||
let loss_regular = SimMIMLoss::compute_l1_loss(&predicted, &target).unwrap();
|
|
||||||
|
|
||||||
// Both should be scalars
|
|
||||||
assert_eq!(loss_normalized.shape(), vec![]);
|
|
||||||
assert_eq!(loss_regular.shape(), vec![]);
|
|
||||||
|
|
||||||
// Normalized loss might be different depending on implementation
|
|
||||||
let norm_value = loss_normalized.to_vec::<f32>().unwrap()[0];
|
|
||||||
let reg_value = loss_regular.to_vec::<f32>().unwrap()[0];
|
|
||||||
|
|
||||||
assert!(norm_value >= 0.0);
|
|
||||||
assert!(reg_value >= 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test main SimMIM trainer
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod trainer_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_trainer_creation() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = SimMIMConfig::default();
|
|
||||||
|
|
||||||
let trainer = SimMIMTrainer::new(config, 3, 224, &device).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(trainer.device(), &device);
|
|
||||||
assert!(trainer.is_training());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_config_defaults() {
|
|
||||||
let config = SimMIMConfig::default();
|
|
||||||
|
|
||||||
assert_eq!(config.mask_ratio, 0.6);
|
|
||||||
assert_eq!(config.mask_patch_size, 32);
|
|
||||||
assert_eq!(config.prediction_head, PredictionHeadType::Linear);
|
|
||||||
assert_eq!(config.loss_type, LossType::L1);
|
|
||||||
assert!(!config.norm_pix_loss);
|
|
||||||
assert_eq!(config.encoder_dim, 768);
|
|
||||||
assert_eq!(config.patch_size, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_config_with_methods() {
|
|
||||||
let config = SimMIMConfig::default()
|
|
||||||
.with_mask_ratio(0.75)
|
|
||||||
.with_encoder_type(EncoderType::Swin)
|
|
||||||
.with_norm_pix_loss(true);
|
|
||||||
|
|
||||||
assert_eq!(config.mask_ratio, 0.75);
|
|
||||||
assert_eq!(config.encoder_type, EncoderType::Swin);
|
|
||||||
assert!(config.norm_pix_loss);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_training_step() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = SimMIMConfig::default();
|
|
||||||
let mut trainer = SimMIMTrainer::new(config, 3, 224, &device).unwrap();
|
|
||||||
|
|
||||||
let batch_size = 2;
|
|
||||||
let images = Tensor::randn(vec![batch_size, 3, 224, 224], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let result = trainer.train_step(&images, Some(42)).unwrap();
|
|
||||||
|
|
||||||
// Check training result structure
|
|
||||||
assert_eq!(result.loss.shape(), vec![]);
|
|
||||||
assert!(result.num_masked_patches > 0);
|
|
||||||
assert!(result.mask_ratio > 0.0 && result.mask_ratio <= 1.0);
|
|
||||||
|
|
||||||
let loss_value = result.loss.to_vec::<f32>().unwrap()[0];
|
|
||||||
assert!(loss_value >= 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_eval_mode() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = SimMIMConfig::default();
|
|
||||||
let mut trainer = SimMIMTrainer::new(config, 3, 224, &device).unwrap();
|
|
||||||
|
|
||||||
// Switch to eval mode
|
|
||||||
trainer.eval();
|
|
||||||
assert!(!trainer.is_training());
|
|
||||||
|
|
||||||
// Extract features should work in eval mode
|
|
||||||
let images = Tensor::randn(vec![1, 3, 224, 224], DType::F32, &device).unwrap();
|
|
||||||
let features = trainer.extract_features(&images).unwrap();
|
|
||||||
|
|
||||||
// Should return feature tensor
|
|
||||||
assert_eq!(features.shape()[0], 1); // batch size
|
|
||||||
assert!(features.shape().len() >= 2); // at least [batch, features]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_different_encoders() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let encoder_types = vec![EncoderType::ViT, EncoderType::Swin];
|
|
||||||
|
|
||||||
for encoder_type in encoder_types {
|
|
||||||
let config = SimMIMConfig::default()
|
|
||||||
.with_encoder_type(encoder_type.clone());
|
|
||||||
|
|
||||||
let trainer = SimMIMTrainer::new(config, 3, 224, &device).unwrap();
|
|
||||||
assert_eq!(trainer.config().encoder_type, encoder_type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_swin_specific_config() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
// Swin transformer should use different default mask ratio
|
|
||||||
let config = SimMIMConfig::for_swin();
|
|
||||||
|
|
||||||
assert_eq!(config.encoder_type, EncoderType::Swin);
|
|
||||||
assert_eq!(config.mask_ratio, 0.32); // Lower mask ratio for hierarchical architecture
|
|
||||||
assert_eq!(config.mask_patch_size, 32); // Larger patch size for hierarchical masking
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_vit_specific_config() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let config = SimMIMConfig::for_vit();
|
|
||||||
|
|
||||||
assert_eq!(config.encoder_type, EncoderType::ViT);
|
|
||||||
assert_eq!(config.mask_ratio, 0.6); // Standard mask ratio for ViT
|
|
||||||
assert_eq!(config.mask_patch_size, 16); // Standard patch size
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_reproducibility() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = SimMIMConfig::default();
|
|
||||||
let mut trainer1 = SimMIMTrainer::new(config.clone(), 3, 224, &device).unwrap();
|
|
||||||
let mut trainer2 = SimMIMTrainer::new(config, 3, 224, &device).unwrap();
|
|
||||||
|
|
||||||
let images = Tensor::randn(vec![2, 3, 224, 224], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
// Same seed should produce same results
|
|
||||||
let result1 = trainer1.train_step(&images, Some(42)).unwrap();
|
|
||||||
let result2 = trainer2.train_step(&images, Some(42)).unwrap();
|
|
||||||
|
|
||||||
let loss1 = result1.loss.to_vec::<f32>().unwrap()[0];
|
|
||||||
let loss2 = result2.loss.to_vec::<f32>().unwrap()[0];
|
|
||||||
|
|
||||||
// Should be very close (within floating point precision)
|
|
||||||
assert!((loss1 - loss2).abs() < 1e-5);
|
|
||||||
assert_eq!(result1.num_masked_patches, result2.num_masked_patches);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test SimMIM integration with other components
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod integration_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_with_different_image_sizes() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = SimMIMConfig::default();
|
|
||||||
|
|
||||||
let image_sizes = vec![224, 256, 384];
|
|
||||||
|
|
||||||
for size in image_sizes {
|
|
||||||
let trainer = SimMIMTrainer::new(config.clone(), 3, size, &device);
|
|
||||||
assert!(trainer.is_ok(), "Failed to create trainer for image size {}", size);
|
|
||||||
|
|
||||||
let trainer = trainer.unwrap();
|
|
||||||
let images = Tensor::randn(vec![1, 3, size, size], DType::F32, &device).unwrap();
|
|
||||||
let features = trainer.extract_features(&images);
|
|
||||||
assert!(features.is_ok(), "Failed to extract features for image size {}", size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_memory_efficiency() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = SimMIMConfig::default();
|
|
||||||
let mut trainer = SimMIMTrainer::new(config, 3, 224, &device).unwrap();
|
|
||||||
|
|
||||||
// Test with larger batch to ensure memory efficiency
|
|
||||||
let batch_size = 8;
|
|
||||||
let images = Tensor::randn(vec![batch_size, 3, 224, 224], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
let result = trainer.train_step(&images, Some(42));
|
|
||||||
assert!(result.is_ok(), "Failed to handle larger batch size");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simmim_patch_reconstruction_consistency() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let config = SimMIMConfig::default();
|
|
||||||
let trainer = SimMIMTrainer::new(config, 3, 224, &device).unwrap();
|
|
||||||
|
|
||||||
// Test that patch dimensions are consistent
|
|
||||||
let images = Tensor::randn(vec![2, 3, 224, 224], DType::F32, &device).unwrap();
|
|
||||||
|
|
||||||
// Extract patch information for validation
|
|
||||||
let image_size = 224;
|
|
||||||
let patch_size = trainer.config().patch_size;
|
|
||||||
let num_patches = (image_size / patch_size).pow(2);
|
|
||||||
|
|
||||||
assert!(num_patches > 0);
|
|
||||||
assert_eq!(num_patches, 196); // 14x14 patches for 224x224 image with 16x16 patches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test error handling and edge cases
|
|
||||||
#[cfg(all(test, feature = "disabled_tests"))]
|
|
||||||
mod error_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_mask_ratio() {
|
|
||||||
// Test mask ratios outside valid range
|
|
||||||
let invalid_ratios = vec![-0.1, 1.1, 2.0];
|
|
||||||
|
|
||||||
for ratio in invalid_ratios {
|
|
||||||
let masker = RandomPatchMasker::new(ratio);
|
|
||||||
// Should clamp to valid range [0.0, 1.0]
|
|
||||||
assert!(masker.mask_ratio() >= 0.0 && masker.mask_ratio() <= 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_empty_tensor_handling() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let masker = RandomPatchMasker::new(0.5);
|
|
||||||
|
|
||||||
// Test with empty tensor - should handle gracefully
|
|
||||||
let empty_patches = Tensor::zeros(vec![0, 0, 768], DType::F32, &device).unwrap();
|
|
||||||
let result = masker.mask_patches(&empty_patches, Some(42));
|
|
||||||
|
|
||||||
// Should either handle gracefully or return appropriate error
|
|
||||||
assert!(result.is_err() || result.unwrap().mask_indices.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mismatched_tensor_dimensions() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
|
|
||||||
let predicted = Tensor::randn(vec![2, 10, 768], DType::F32, &device).unwrap();
|
|
||||||
let target = Tensor::randn(vec![2, 12, 768], DType::F32, &device).unwrap(); // Different sequence length
|
|
||||||
|
|
||||||
let loss = SimMIMLoss::compute_l1_loss(&predicted, &target);
|
|
||||||
assert!(loss.is_err(), "Should fail with mismatched dimensions");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_zero_patches() {
|
|
||||||
let device = Device::cuda(0).unwrap_or(Device::default());
|
|
||||||
let masker = RandomPatchMasker::new(0.5);
|
|
||||||
|
|
||||||
let patches = Tensor::randn(vec![1, 0, 768], DType::F32, &device).unwrap();
|
|
||||||
let result = masker.mask_patches(&patches, Some(42));
|
|
||||||
|
|
||||||
// Should handle zero patches gracefully
|
|
||||||
assert!(result.is_err() || result.unwrap().visible_indices[0].is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Duplicate-implementation consolidation notes
|
||||||
|
|
||||||
|
Recorded during the 2026-07-09 wiring/dead-code sweep. These are known
|
||||||
|
duplications that were **not** consolidated in that pass because doing so
|
||||||
|
requires rewriting call sites; new code should target the canonical
|
||||||
|
implementation listed here.
|
||||||
|
|
||||||
|
## Mixture of Experts
|
||||||
|
|
||||||
|
Canonical: `rtx-transformers/src/layers/mixture_of_experts/`
|
||||||
|
|
||||||
|
Competing implementations still present (divergent internal APIs):
|
||||||
|
- `rtx-transformers/src/layers/metal_moe.rs` (Metal-specific)
|
||||||
|
- `rtx-transformers/src/modular/router.rs` (routing only)
|
||||||
|
- `rtx-transformers/src/architectures/glam.rs` (architecture-embedded)
|
||||||
|
|
||||||
|
Deleted in the sweep (were orphaned, never declared by `mod`):
|
||||||
|
`layers/moe_layer.rs`, `layers/moe_integration.rs`.
|
||||||
|
|
||||||
|
## Flash Attention
|
||||||
|
|
||||||
|
Canonical: the `rtx-flash-attention` crate (v2+v3 kernels, CUDA + Metal).
|
||||||
|
|
||||||
|
Reimplementations inside rtx-transformers that should eventually delegate:
|
||||||
|
- `src/tensor_core_kernels.rs`
|
||||||
|
- `src/revolutionary/orchestrator_core.rs`
|
||||||
|
- `src/ssl/jepa_gpu.rs`
|
||||||
|
- `src/training/training_loop.rs`
|
||||||
|
|
||||||
|
## Speculative decoding
|
||||||
|
|
||||||
|
Layering is intentional (documented in `rtx-inference/src/speculative/mod.rs`):
|
||||||
|
`speculative/` is the orchestration layer (traits/configs/trees);
|
||||||
|
`rtx-inference/src/medusa.rs` and `src/lookahead.rs` are the concrete
|
||||||
|
implementations. Not a duplication to remove, but keep the two `MedusaConfig`
|
||||||
|
types (orchestration vs implementation, re-exported as `MedusaHeadsConfig`)
|
||||||
|
from drifting.
|
||||||
Reference in New Issue
Block a user