1059 lines
31 KiB
Rust
1059 lines
31 KiB
Rust
//! Collective Operation Fusion
|
|
//!
|
|
//! This module provides fusion of collective operations for efficiency:
|
|
//! - Batches multiple small collectives into larger operations
|
|
//! - Reduces kernel launch overhead
|
|
//! - Optimizes network utilization
|
|
//! - Supports various fusion strategies
|
|
//!
|
|
//! Fusion can improve throughput by 10-50% for models with many
|
|
//! small parameter tensors.
|
|
|
|
use crate::error::{DistributedError, Result};
|
|
use parking_lot::{Mutex, RwLock};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
use std::time::{Duration, Instant};
|
|
|
|
// =============================================================================
|
|
// Configuration
|
|
// =============================================================================
|
|
|
|
/// Fusion strategy
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum FusionStrategy {
|
|
/// No fusion - execute immediately
|
|
None,
|
|
/// Size-based fusion - fuse until buffer is full
|
|
SizeBased,
|
|
/// Count-based fusion - fuse until N operations
|
|
CountBased,
|
|
/// Time-based fusion - fuse until timeout
|
|
TimeBased,
|
|
/// Adaptive fusion based on throughput
|
|
Adaptive,
|
|
}
|
|
|
|
/// Collective operation type
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum CollectiveType {
|
|
/// AllReduce
|
|
AllReduce,
|
|
/// AllGather
|
|
AllGather,
|
|
/// ReduceScatter
|
|
ReduceScatter,
|
|
/// Broadcast
|
|
Broadcast,
|
|
/// Reduce
|
|
Reduce,
|
|
/// Scatter
|
|
Scatter,
|
|
/// Gather
|
|
Gather,
|
|
}
|
|
|
|
/// Configuration for collective fusion
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FusionConfig {
|
|
/// Fusion strategy
|
|
pub strategy: FusionStrategy,
|
|
/// Maximum fusion buffer size (bytes)
|
|
pub max_buffer_size: usize,
|
|
/// Maximum number of operations to fuse
|
|
pub max_ops_per_fusion: usize,
|
|
/// Fusion timeout (ms)
|
|
pub fusion_timeout_ms: u64,
|
|
/// Minimum size to consider for fusion (bytes)
|
|
pub min_fusion_size: usize,
|
|
/// Enable per-collective-type buffers
|
|
pub per_type_buffers: bool,
|
|
/// Alignment for fused buffers
|
|
pub alignment: usize,
|
|
/// Enable overlap of fusion with computation
|
|
pub overlap_fusion: bool,
|
|
}
|
|
|
|
impl Default for FusionConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
strategy: FusionStrategy::SizeBased,
|
|
max_buffer_size: 25 * 1024 * 1024, // 25MB
|
|
max_ops_per_fusion: 64,
|
|
fusion_timeout_ms: 10,
|
|
min_fusion_size: 256,
|
|
per_type_buffers: true,
|
|
alignment: 256,
|
|
overlap_fusion: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fusion Operation
|
|
// =============================================================================
|
|
|
|
/// A single operation to be fused
|
|
#[derive(Debug, Clone)]
|
|
pub struct FusionOp {
|
|
/// Operation ID
|
|
pub id: u64,
|
|
/// Collective type
|
|
pub collective_type: CollectiveType,
|
|
/// Parameter name
|
|
pub name: String,
|
|
/// Data size in bytes
|
|
pub size_bytes: usize,
|
|
/// Offset in fusion buffer
|
|
pub buffer_offset: Option<usize>,
|
|
/// Creation time
|
|
pub created_at: Instant,
|
|
/// Priority (lower = higher priority)
|
|
pub priority: i32,
|
|
/// Is ready for execution
|
|
pub ready: bool,
|
|
}
|
|
|
|
impl FusionOp {
|
|
/// Create a new fusion operation
|
|
pub fn new(id: u64, collective_type: CollectiveType, name: String, size_bytes: usize) -> Self {
|
|
Self {
|
|
id,
|
|
collective_type,
|
|
name,
|
|
size_bytes,
|
|
buffer_offset: None,
|
|
created_at: Instant::now(),
|
|
priority: 0,
|
|
ready: true,
|
|
}
|
|
}
|
|
|
|
/// Get age of this operation
|
|
pub fn age(&self) -> Duration {
|
|
self.created_at.elapsed()
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fusion Group
|
|
// =============================================================================
|
|
|
|
/// A group of fused operations
|
|
#[derive(Debug, Clone)]
|
|
pub struct FusionGroup {
|
|
/// Group ID
|
|
pub id: u64,
|
|
/// Collective type
|
|
pub collective_type: CollectiveType,
|
|
/// Operations in this group
|
|
pub ops: Vec<FusionOp>,
|
|
/// Total size in bytes
|
|
pub total_size: usize,
|
|
/// Fused buffer data
|
|
pub buffer: Vec<u8>,
|
|
/// Creation time
|
|
pub created_at: Instant,
|
|
/// Is group sealed (no more additions)
|
|
pub sealed: bool,
|
|
}
|
|
|
|
impl FusionGroup {
|
|
/// Create a new fusion group
|
|
pub fn new(id: u64, collective_type: CollectiveType) -> Self {
|
|
Self {
|
|
id,
|
|
collective_type,
|
|
ops: Vec::new(),
|
|
total_size: 0,
|
|
buffer: Vec::new(),
|
|
created_at: Instant::now(),
|
|
sealed: false,
|
|
}
|
|
}
|
|
|
|
/// Add an operation to the group
|
|
pub fn add_op(&mut self, mut op: FusionOp, alignment: usize) -> bool {
|
|
if self.sealed {
|
|
return false;
|
|
}
|
|
|
|
if op.collective_type != self.collective_type {
|
|
return false;
|
|
}
|
|
|
|
// Calculate aligned offset
|
|
let aligned_offset = (self.total_size + alignment - 1) & !(alignment - 1);
|
|
let padding = aligned_offset - self.total_size;
|
|
|
|
op.buffer_offset = Some(aligned_offset);
|
|
self.total_size = aligned_offset + op.size_bytes;
|
|
self.ops.push(op);
|
|
|
|
// Add padding to buffer
|
|
self.buffer.extend(std::iter::repeat_n(0u8, padding));
|
|
|
|
true
|
|
}
|
|
|
|
/// Get number of operations
|
|
pub fn num_ops(&self) -> usize {
|
|
self.ops.len()
|
|
}
|
|
|
|
/// Check if group is empty
|
|
pub fn is_empty(&self) -> bool {
|
|
self.ops.is_empty()
|
|
}
|
|
|
|
/// Seal the group (prevent further additions)
|
|
pub fn seal(&mut self) {
|
|
self.sealed = true;
|
|
}
|
|
|
|
/// Get age of this group
|
|
pub fn age(&self) -> Duration {
|
|
self.created_at.elapsed()
|
|
}
|
|
|
|
/// Get operation by name
|
|
pub fn get_op(&self, name: &str) -> Option<&FusionOp> {
|
|
self.ops.iter().find(|op| op.name == name)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fusion Buffer
|
|
// =============================================================================
|
|
|
|
/// Buffer for accumulating operations to fuse
|
|
pub struct FusionBuffer {
|
|
/// Configuration
|
|
config: FusionConfig,
|
|
/// Current fusion group
|
|
current_group: Mutex<Option<FusionGroup>>,
|
|
/// Next group ID
|
|
next_group_id: AtomicU64,
|
|
/// Next operation ID
|
|
next_op_id: AtomicU64,
|
|
/// Collective type this buffer handles
|
|
collective_type: CollectiveType,
|
|
/// Ready groups waiting to execute
|
|
ready_groups: Mutex<VecDeque<FusionGroup>>,
|
|
/// Statistics
|
|
stats: RwLock<FusionBufferStats>,
|
|
}
|
|
|
|
impl FusionBuffer {
|
|
/// Create a new fusion buffer
|
|
pub fn new(config: FusionConfig, collective_type: CollectiveType) -> Self {
|
|
Self {
|
|
config,
|
|
current_group: Mutex::new(None),
|
|
next_group_id: AtomicU64::new(1),
|
|
next_op_id: AtomicU64::new(1),
|
|
collective_type,
|
|
ready_groups: Mutex::new(VecDeque::new()),
|
|
stats: RwLock::new(FusionBufferStats::default()),
|
|
}
|
|
}
|
|
|
|
/// Submit an operation for fusion
|
|
pub fn submit(&self, name: String, size_bytes: usize) -> u64 {
|
|
let op_id = self.next_op_id.fetch_add(1, Ordering::SeqCst);
|
|
let op = FusionOp::new(op_id, self.collective_type, name, size_bytes);
|
|
|
|
let mut current = self.current_group.lock();
|
|
|
|
// Check if we need a new group
|
|
let need_new_group = match &*current {
|
|
None => true,
|
|
Some(group) => self.should_flush_group(group, size_bytes),
|
|
};
|
|
|
|
if need_new_group {
|
|
// Flush current group if exists
|
|
if let Some(mut group) = current.take() {
|
|
if !group.is_empty() {
|
|
group.seal();
|
|
self.ready_groups.lock().push_back(group);
|
|
|
|
let mut stats = self.stats.write();
|
|
stats.groups_flushed += 1;
|
|
}
|
|
}
|
|
|
|
// Create new group
|
|
let group_id = self.next_group_id.fetch_add(1, Ordering::SeqCst);
|
|
*current = Some(FusionGroup::new(group_id, self.collective_type));
|
|
}
|
|
|
|
// Add to current group
|
|
if let Some(group) = current.as_mut() {
|
|
group.add_op(op, self.config.alignment);
|
|
|
|
let mut stats = self.stats.write();
|
|
stats.ops_submitted += 1;
|
|
stats.bytes_submitted += size_bytes;
|
|
}
|
|
|
|
op_id
|
|
}
|
|
|
|
/// Check if group should be flushed
|
|
fn should_flush_group(&self, group: &FusionGroup, new_size: usize) -> bool {
|
|
match self.config.strategy {
|
|
FusionStrategy::None => true,
|
|
FusionStrategy::SizeBased => group.total_size + new_size > self.config.max_buffer_size,
|
|
FusionStrategy::CountBased => group.num_ops() >= self.config.max_ops_per_fusion,
|
|
FusionStrategy::TimeBased => {
|
|
group.age() >= Duration::from_millis(self.config.fusion_timeout_ms)
|
|
}
|
|
FusionStrategy::Adaptive => {
|
|
group.total_size + new_size > self.config.max_buffer_size
|
|
|| group.num_ops() >= self.config.max_ops_per_fusion
|
|
|| group.age() >= Duration::from_millis(self.config.fusion_timeout_ms)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Force flush the current group
|
|
pub fn flush(&self) -> Option<FusionGroup> {
|
|
let mut current = self.current_group.lock();
|
|
|
|
if let Some(mut group) = current.take() {
|
|
if !group.is_empty() {
|
|
group.seal();
|
|
|
|
let mut stats = self.stats.write();
|
|
stats.groups_flushed += 1;
|
|
stats.ops_fused += group.num_ops();
|
|
stats.bytes_fused += group.total_size;
|
|
|
|
return Some(group);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Get next ready group
|
|
pub fn pop_ready(&self) -> Option<FusionGroup> {
|
|
self.ready_groups.lock().pop_front()
|
|
}
|
|
|
|
/// Check if there are ready groups
|
|
pub fn has_ready(&self) -> bool {
|
|
!self.ready_groups.lock().is_empty()
|
|
}
|
|
|
|
/// Get number of ready groups
|
|
pub fn ready_count(&self) -> usize {
|
|
self.ready_groups.lock().len()
|
|
}
|
|
|
|
/// Get current group size
|
|
pub fn current_size(&self) -> usize {
|
|
self.current_group
|
|
.lock()
|
|
.as_ref()
|
|
.map_or(0, |g| g.total_size)
|
|
}
|
|
|
|
/// Get current operation count
|
|
pub fn current_ops(&self) -> usize {
|
|
self.current_group
|
|
.lock()
|
|
.as_ref()
|
|
.map_or(0, FusionGroup::num_ops)
|
|
}
|
|
|
|
/// Get statistics
|
|
pub fn stats(&self) -> FusionBufferStats {
|
|
self.stats.read().clone()
|
|
}
|
|
|
|
/// Get collective type
|
|
pub fn collective_type(&self) -> CollectiveType {
|
|
self.collective_type
|
|
}
|
|
}
|
|
|
|
/// Statistics for fusion buffer
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct FusionBufferStats {
|
|
/// Operations submitted
|
|
pub ops_submitted: usize,
|
|
/// Bytes submitted
|
|
pub bytes_submitted: usize,
|
|
/// Operations fused
|
|
pub ops_fused: usize,
|
|
/// Bytes fused
|
|
pub bytes_fused: usize,
|
|
/// Groups flushed
|
|
pub groups_flushed: usize,
|
|
}
|
|
|
|
impl FusionBufferStats {
|
|
/// Get average ops per fusion
|
|
pub fn avg_ops_per_fusion(&self) -> f32 {
|
|
if self.groups_flushed == 0 {
|
|
0.0
|
|
} else {
|
|
self.ops_fused as f32 / self.groups_flushed as f32
|
|
}
|
|
}
|
|
|
|
/// Get average bytes per fusion
|
|
pub fn avg_bytes_per_fusion(&self) -> f32 {
|
|
if self.groups_flushed == 0 {
|
|
0.0
|
|
} else {
|
|
self.bytes_fused as f32 / self.groups_flushed as f32
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Collective Fusion Manager
|
|
// =============================================================================
|
|
|
|
/// Manages fusion of collective operations
|
|
pub struct CollectiveFusionManager {
|
|
/// Configuration
|
|
config: FusionConfig,
|
|
/// Buffers per collective type
|
|
buffers: HashMap<CollectiveType, Arc<FusionBuffer>>,
|
|
/// Unified buffer (when per_type_buffers is false)
|
|
unified_buffer: Option<Arc<FusionBuffer>>,
|
|
/// Is fusion enabled
|
|
enabled: AtomicBool,
|
|
/// Statistics
|
|
stats: RwLock<FusionManagerStats>,
|
|
}
|
|
|
|
impl CollectiveFusionManager {
|
|
/// Create a new fusion manager
|
|
pub fn new(config: FusionConfig) -> Self {
|
|
let mut buffers = HashMap::new();
|
|
|
|
if config.per_type_buffers {
|
|
// Create buffer for each collective type
|
|
for collective_type in [
|
|
CollectiveType::AllReduce,
|
|
CollectiveType::AllGather,
|
|
CollectiveType::ReduceScatter,
|
|
CollectiveType::Broadcast,
|
|
] {
|
|
buffers.insert(
|
|
collective_type,
|
|
Arc::new(FusionBuffer::new(config.clone(), collective_type)),
|
|
);
|
|
}
|
|
}
|
|
|
|
let unified_buffer = if !config.per_type_buffers {
|
|
Some(Arc::new(FusionBuffer::new(
|
|
config.clone(),
|
|
CollectiveType::AllReduce,
|
|
)))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Self {
|
|
config,
|
|
buffers,
|
|
unified_buffer,
|
|
enabled: AtomicBool::new(true),
|
|
stats: RwLock::new(FusionManagerStats::default()),
|
|
}
|
|
}
|
|
|
|
/// Submit an operation for fusion
|
|
pub fn submit(
|
|
&self,
|
|
collective_type: CollectiveType,
|
|
name: String,
|
|
size_bytes: usize,
|
|
) -> Result<u64> {
|
|
if !self.enabled.load(Ordering::SeqCst) {
|
|
return Err(DistributedError::runtime("Fusion manager is disabled"));
|
|
}
|
|
|
|
// Skip fusion for large operations
|
|
if size_bytes > self.config.max_buffer_size {
|
|
let mut stats = self.stats.write();
|
|
stats.skipped_large += 1;
|
|
// Return fake ID for immediate execution
|
|
return Ok(0);
|
|
}
|
|
|
|
// Skip fusion for very small operations
|
|
if size_bytes < self.config.min_fusion_size {
|
|
let mut stats = self.stats.write();
|
|
stats.skipped_small += 1;
|
|
return Ok(0);
|
|
}
|
|
|
|
let buffer = self.get_buffer(collective_type)?;
|
|
let id = buffer.submit(name, size_bytes);
|
|
|
|
let mut stats = self.stats.write();
|
|
stats.ops_received += 1;
|
|
|
|
Ok(id)
|
|
}
|
|
|
|
/// Get buffer for collective type
|
|
fn get_buffer(&self, collective_type: CollectiveType) -> Result<Arc<FusionBuffer>> {
|
|
if let Some(ref unified) = self.unified_buffer {
|
|
return Ok(unified.clone());
|
|
}
|
|
|
|
self.buffers.get(&collective_type).cloned().ok_or_else(|| {
|
|
DistributedError::runtime(format!(
|
|
"No buffer for collective type {:?}",
|
|
collective_type
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Flush all buffers
|
|
pub fn flush_all(&self) -> Vec<FusionGroup> {
|
|
let mut groups = Vec::new();
|
|
|
|
if let Some(ref unified) = self.unified_buffer {
|
|
if let Some(group) = unified.flush() {
|
|
groups.push(group);
|
|
}
|
|
} else {
|
|
for buffer in self.buffers.values() {
|
|
if let Some(group) = buffer.flush() {
|
|
groups.push(group);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut stats = self.stats.write();
|
|
stats.flush_count += 1;
|
|
|
|
groups
|
|
}
|
|
|
|
/// Flush specific buffer
|
|
pub fn flush(&self, collective_type: CollectiveType) -> Option<FusionGroup> {
|
|
if let Ok(buffer) = self.get_buffer(collective_type) {
|
|
buffer.flush()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Get all ready groups
|
|
pub fn get_ready_groups(&self) -> Vec<FusionGroup> {
|
|
let mut groups = Vec::new();
|
|
|
|
if let Some(ref unified) = self.unified_buffer {
|
|
while let Some(group) = unified.pop_ready() {
|
|
groups.push(group);
|
|
}
|
|
} else {
|
|
for buffer in self.buffers.values() {
|
|
while let Some(group) = buffer.pop_ready() {
|
|
groups.push(group);
|
|
}
|
|
}
|
|
}
|
|
|
|
groups
|
|
}
|
|
|
|
/// Check if any buffer has ready groups
|
|
pub fn has_ready(&self) -> bool {
|
|
if let Some(ref unified) = self.unified_buffer {
|
|
return unified.has_ready();
|
|
}
|
|
|
|
self.buffers.values().any(|b| b.has_ready())
|
|
}
|
|
|
|
/// Enable fusion
|
|
pub fn enable(&self) {
|
|
self.enabled.store(true, Ordering::SeqCst);
|
|
}
|
|
|
|
/// Disable fusion
|
|
pub fn disable(&self) {
|
|
self.enabled.store(false, Ordering::SeqCst);
|
|
}
|
|
|
|
/// Check if enabled
|
|
pub fn is_enabled(&self) -> bool {
|
|
self.enabled.load(Ordering::SeqCst)
|
|
}
|
|
|
|
/// Get statistics
|
|
pub fn stats(&self) -> FusionManagerStats {
|
|
self.stats.read().clone()
|
|
}
|
|
|
|
/// Get buffer statistics
|
|
pub fn buffer_stats(&self, collective_type: CollectiveType) -> Option<FusionBufferStats> {
|
|
self.get_buffer(collective_type).ok().map(|b| b.stats())
|
|
}
|
|
|
|
/// Get configuration
|
|
pub fn config(&self) -> &FusionConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Get total pending size across all buffers
|
|
pub fn total_pending_size(&self) -> usize {
|
|
if let Some(ref unified) = self.unified_buffer {
|
|
return unified.current_size();
|
|
}
|
|
|
|
self.buffers.values().map(|b| b.current_size()).sum()
|
|
}
|
|
|
|
/// Get total pending ops across all buffers
|
|
pub fn total_pending_ops(&self) -> usize {
|
|
if let Some(ref unified) = self.unified_buffer {
|
|
return unified.current_ops();
|
|
}
|
|
|
|
self.buffers.values().map(|b| b.current_ops()).sum()
|
|
}
|
|
}
|
|
|
|
/// Statistics for fusion manager
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct FusionManagerStats {
|
|
/// Operations received
|
|
pub ops_received: usize,
|
|
/// Flush operations
|
|
pub flush_count: usize,
|
|
/// Skipped (too large)
|
|
pub skipped_large: usize,
|
|
/// Skipped (too small)
|
|
pub skipped_small: usize,
|
|
}
|
|
|
|
// =============================================================================
|
|
// Thread-Safe Wrappers
|
|
// =============================================================================
|
|
|
|
/// Thread-safe shared fusion manager
|
|
pub type SharedFusionManager = Arc<CollectiveFusionManager>;
|
|
|
|
/// Create a shared fusion manager
|
|
pub fn shared_fusion_manager(config: FusionConfig) -> SharedFusionManager {
|
|
Arc::new(CollectiveFusionManager::new(config))
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_fusion_config_default() {
|
|
let config = FusionConfig::default();
|
|
assert_eq!(config.strategy, FusionStrategy::SizeBased);
|
|
assert_eq!(config.max_buffer_size, 25 * 1024 * 1024);
|
|
assert!(config.per_type_buffers);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_op_creation() {
|
|
let op = FusionOp::new(1, CollectiveType::AllReduce, "grad".to_string(), 1024);
|
|
assert_eq!(op.id, 1);
|
|
assert_eq!(op.collective_type, CollectiveType::AllReduce);
|
|
assert_eq!(op.size_bytes, 1024);
|
|
assert!(op.buffer_offset.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_op_age() {
|
|
let op = FusionOp::new(1, CollectiveType::AllReduce, "grad".to_string(), 1024);
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
assert!(op.age() >= Duration::from_millis(5));
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_group_creation() {
|
|
let group = FusionGroup::new(1, CollectiveType::AllReduce);
|
|
assert_eq!(group.id, 1);
|
|
assert!(group.is_empty());
|
|
assert!(!group.sealed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_group_add_op() {
|
|
let mut group = FusionGroup::new(1, CollectiveType::AllReduce);
|
|
|
|
let op = FusionOp::new(1, CollectiveType::AllReduce, "grad1".to_string(), 1024);
|
|
assert!(group.add_op(op, 256));
|
|
|
|
assert_eq!(group.num_ops(), 1);
|
|
assert_eq!(group.total_size, 1024);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_group_alignment() {
|
|
let mut group = FusionGroup::new(1, CollectiveType::AllReduce);
|
|
|
|
// First op: 100 bytes
|
|
let op1 = FusionOp::new(1, CollectiveType::AllReduce, "g1".to_string(), 100);
|
|
group.add_op(op1, 256);
|
|
|
|
// Second op should be at aligned offset
|
|
let op2 = FusionOp::new(2, CollectiveType::AllReduce, "g2".to_string(), 50);
|
|
group.add_op(op2, 256);
|
|
|
|
let second_offset = group.ops[1].buffer_offset.unwrap();
|
|
assert_eq!(second_offset, 256); // Aligned to 256
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_group_seal() {
|
|
let mut group = FusionGroup::new(1, CollectiveType::AllReduce);
|
|
|
|
let op1 = FusionOp::new(1, CollectiveType::AllReduce, "g1".to_string(), 100);
|
|
assert!(group.add_op(op1, 256));
|
|
|
|
group.seal();
|
|
assert!(group.sealed);
|
|
|
|
// Cannot add after seal
|
|
let op2 = FusionOp::new(2, CollectiveType::AllReduce, "g2".to_string(), 100);
|
|
assert!(!group.add_op(op2, 256));
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_group_wrong_type() {
|
|
let mut group = FusionGroup::new(1, CollectiveType::AllReduce);
|
|
|
|
// Try to add AllGather to AllReduce group
|
|
let op = FusionOp::new(1, CollectiveType::AllGather, "g".to_string(), 100);
|
|
assert!(!group.add_op(op, 256));
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_buffer_creation() {
|
|
let config = FusionConfig::default();
|
|
let buffer = FusionBuffer::new(config, CollectiveType::AllReduce);
|
|
|
|
assert_eq!(buffer.collective_type(), CollectiveType::AllReduce);
|
|
assert_eq!(buffer.current_size(), 0);
|
|
assert_eq!(buffer.current_ops(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_buffer_submit() {
|
|
let config = FusionConfig::default();
|
|
let buffer = FusionBuffer::new(config, CollectiveType::AllReduce);
|
|
|
|
let id = buffer.submit("grad1".to_string(), 1024);
|
|
assert!(id > 0);
|
|
assert_eq!(buffer.current_ops(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_buffer_multiple_submit() {
|
|
let config = FusionConfig::default();
|
|
let buffer = FusionBuffer::new(config, CollectiveType::AllReduce);
|
|
|
|
buffer.submit("grad1".to_string(), 1024);
|
|
buffer.submit("grad2".to_string(), 2048);
|
|
buffer.submit("grad3".to_string(), 512);
|
|
|
|
assert_eq!(buffer.current_ops(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_buffer_flush() {
|
|
let config = FusionConfig::default();
|
|
let buffer = FusionBuffer::new(config, CollectiveType::AllReduce);
|
|
|
|
buffer.submit("grad1".to_string(), 1024);
|
|
buffer.submit("grad2".to_string(), 2048);
|
|
|
|
let group = buffer.flush().unwrap();
|
|
assert_eq!(group.num_ops(), 2);
|
|
assert!(group.sealed);
|
|
|
|
// Buffer should be empty now
|
|
assert_eq!(buffer.current_ops(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_buffer_size_trigger() {
|
|
let config = FusionConfig {
|
|
strategy: FusionStrategy::SizeBased,
|
|
max_buffer_size: 1000,
|
|
..Default::default()
|
|
};
|
|
let buffer = FusionBuffer::new(config, CollectiveType::AllReduce);
|
|
|
|
// First op fits
|
|
buffer.submit("grad1".to_string(), 500);
|
|
assert_eq!(buffer.ready_count(), 0);
|
|
|
|
// Second op exceeds limit, triggers flush
|
|
buffer.submit("grad2".to_string(), 600);
|
|
assert_eq!(buffer.ready_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_buffer_count_trigger() {
|
|
let config = FusionConfig {
|
|
strategy: FusionStrategy::CountBased,
|
|
max_ops_per_fusion: 3,
|
|
..Default::default()
|
|
};
|
|
let buffer = FusionBuffer::new(config, CollectiveType::AllReduce);
|
|
|
|
buffer.submit("g1".to_string(), 100);
|
|
buffer.submit("g2".to_string(), 100);
|
|
assert_eq!(buffer.ready_count(), 0);
|
|
|
|
buffer.submit("g3".to_string(), 100);
|
|
buffer.submit("g4".to_string(), 100); // Triggers flush
|
|
assert_eq!(buffer.ready_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_buffer_stats() {
|
|
let config = FusionConfig::default();
|
|
let buffer = FusionBuffer::new(config, CollectiveType::AllReduce);
|
|
|
|
buffer.submit("g1".to_string(), 1000);
|
|
buffer.submit("g2".to_string(), 2000);
|
|
buffer.flush();
|
|
|
|
let stats = buffer.stats();
|
|
assert_eq!(stats.ops_submitted, 2);
|
|
assert_eq!(stats.bytes_submitted, 3000);
|
|
assert_eq!(stats.groups_flushed, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_manager_creation() {
|
|
let config = FusionConfig::default();
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
assert!(manager.is_enabled());
|
|
assert_eq!(manager.total_pending_ops(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_manager_submit() {
|
|
let config = FusionConfig::default();
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
let id = manager
|
|
.submit(CollectiveType::AllReduce, "grad".to_string(), 1024)
|
|
.unwrap();
|
|
assert!(id > 0);
|
|
assert_eq!(manager.total_pending_ops(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_manager_multiple_types() {
|
|
let config = FusionConfig {
|
|
per_type_buffers: true,
|
|
..Default::default()
|
|
};
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
manager
|
|
.submit(CollectiveType::AllReduce, "ar".to_string(), 1024)
|
|
.unwrap();
|
|
manager
|
|
.submit(CollectiveType::AllGather, "ag".to_string(), 2048)
|
|
.unwrap();
|
|
|
|
// Should have ops in different buffers
|
|
let ar_stats = manager.buffer_stats(CollectiveType::AllReduce).unwrap();
|
|
let ag_stats = manager.buffer_stats(CollectiveType::AllGather).unwrap();
|
|
|
|
assert_eq!(ar_stats.ops_submitted, 1);
|
|
assert_eq!(ag_stats.ops_submitted, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_manager_flush_all() {
|
|
let config = FusionConfig::default();
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
manager
|
|
.submit(CollectiveType::AllReduce, "g1".to_string(), 1024)
|
|
.unwrap();
|
|
manager
|
|
.submit(CollectiveType::AllReduce, "g2".to_string(), 2048)
|
|
.unwrap();
|
|
|
|
let groups = manager.flush_all();
|
|
assert_eq!(groups.len(), 1);
|
|
assert_eq!(groups[0].num_ops(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_manager_skip_large() {
|
|
let config = FusionConfig {
|
|
max_buffer_size: 1000,
|
|
..Default::default()
|
|
};
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
// Operation larger than buffer
|
|
let id = manager
|
|
.submit(CollectiveType::AllReduce, "big".to_string(), 5000)
|
|
.unwrap();
|
|
assert_eq!(id, 0); // Skipped
|
|
|
|
let stats = manager.stats();
|
|
assert_eq!(stats.skipped_large, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_manager_skip_small() {
|
|
let config = FusionConfig {
|
|
min_fusion_size: 500,
|
|
..Default::default()
|
|
};
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
// Operation smaller than minimum
|
|
let id = manager
|
|
.submit(CollectiveType::AllReduce, "tiny".to_string(), 100)
|
|
.unwrap();
|
|
assert_eq!(id, 0); // Skipped
|
|
|
|
let stats = manager.stats();
|
|
assert_eq!(stats.skipped_small, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_manager_enable_disable() {
|
|
let config = FusionConfig::default();
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
assert!(manager.is_enabled());
|
|
|
|
manager.disable();
|
|
assert!(!manager.is_enabled());
|
|
|
|
let result = manager.submit(CollectiveType::AllReduce, "g".to_string(), 1024);
|
|
assert!(result.is_err());
|
|
|
|
manager.enable();
|
|
assert!(manager.is_enabled());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_manager_ready_groups() {
|
|
let config = FusionConfig {
|
|
strategy: FusionStrategy::SizeBased,
|
|
max_buffer_size: 500,
|
|
..Default::default()
|
|
};
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
// Trigger automatic flush
|
|
manager
|
|
.submit(CollectiveType::AllReduce, "g1".to_string(), 400)
|
|
.unwrap();
|
|
manager
|
|
.submit(CollectiveType::AllReduce, "g2".to_string(), 400)
|
|
.unwrap();
|
|
|
|
assert!(manager.has_ready());
|
|
|
|
let groups = manager.get_ready_groups();
|
|
assert!(!groups.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_shared_fusion_manager() {
|
|
let config = FusionConfig::default();
|
|
let manager = shared_fusion_manager(config);
|
|
|
|
manager
|
|
.submit(CollectiveType::AllReduce, "g".to_string(), 1024)
|
|
.unwrap();
|
|
assert_eq!(manager.total_pending_ops(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_buffer_stats_averages() {
|
|
let stats = FusionBufferStats {
|
|
ops_fused: 100,
|
|
bytes_fused: 50000,
|
|
groups_flushed: 10,
|
|
..Default::default()
|
|
};
|
|
|
|
assert!((stats.avg_ops_per_fusion() - 10.0).abs() < 0.01);
|
|
assert!((stats.avg_bytes_per_fusion() - 5000.0).abs() < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fusion_group_get_op() {
|
|
let mut group = FusionGroup::new(1, CollectiveType::AllReduce);
|
|
|
|
let op = FusionOp::new(1, CollectiveType::AllReduce, "my_grad".to_string(), 1024);
|
|
group.add_op(op, 256);
|
|
|
|
assert!(group.get_op("my_grad").is_some());
|
|
assert!(group.get_op("other").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_strategy_none() {
|
|
let config = FusionConfig {
|
|
strategy: FusionStrategy::None,
|
|
..Default::default()
|
|
};
|
|
let buffer = FusionBuffer::new(config, CollectiveType::AllReduce);
|
|
|
|
// Each submit should trigger immediate flush to ready queue
|
|
buffer.submit("g1".to_string(), 100);
|
|
assert_eq!(buffer.ready_count(), 0); // First op creates group
|
|
|
|
buffer.submit("g2".to_string(), 100);
|
|
assert_eq!(buffer.ready_count(), 1); // Triggers flush
|
|
}
|
|
|
|
#[test]
|
|
fn test_unified_buffer() {
|
|
let config = FusionConfig {
|
|
per_type_buffers: false,
|
|
min_fusion_size: 0, // Allow any size for this test
|
|
..Default::default()
|
|
};
|
|
let manager = CollectiveFusionManager::new(config);
|
|
|
|
// All types go to same buffer
|
|
manager
|
|
.submit(CollectiveType::AllReduce, "ar".to_string(), 100)
|
|
.unwrap();
|
|
manager
|
|
.submit(CollectiveType::AllGather, "ag".to_string(), 100)
|
|
.unwrap();
|
|
|
|
assert_eq!(manager.total_pending_ops(), 2);
|
|
|
|
let groups = manager.flush_all();
|
|
assert_eq!(groups.len(), 1);
|
|
assert_eq!(groups[0].num_ops(), 2);
|
|
}
|
|
}
|