use std::fmt::Debug;
use scirs2_core::numeric::Float;
use std::collections::{BTreeMap, HashMap};
use super::super::frontend::{
DataType, Layout, MemorySpace, Operand, OperandId, OperationId, XLAComputation,
};
use super::super::{TPUConfig, TPUVersion};
use crate::error::{OptimError, Result};
fn data_type_size(dtype: DataType) -> usize {
match dtype {
DataType::F16 | DataType::BF16 => 2,
DataType::F32 => 4,
DataType::F64 => 8,
DataType::S8 | DataType::U8 | DataType::Pred => 1,
DataType::S16 | DataType::U16 => 2,
DataType::S32 | DataType::U32 => 4,
DataType::S64 | DataType::U64 => 8,
DataType::C64 => 8,
DataType::C128 => 16,
}
}
fn on_chip_budget_bytes(version: TPUVersion) -> usize {
let gib = 1024usize * 1024 * 1024;
let per_core = match version {
TPUVersion::V2 => 8 * gib,
TPUVersion::V3 => 16 * gib,
TPUVersion::V4 => 32 * gib,
TPUVersion::V5e => 16 * gib,
TPUVersion::V5p => 95 * gib,
};
per_core / 1024
}
pub struct MemoryPlanner<T: Float + Debug + Send + Sync + 'static> {
allocation_strategy: AllocationStrategy,
layout_optimizer: LayoutOptimizer<T>,
buffer_manager: BufferManager<T>,
bandwidth_optimizer: BandwidthOptimizer<T>,
hierarchy_manager: MemoryHierarchyManager<T>,
planning_stats: MemoryPlanningStats,
}
#[derive(Debug, Clone)]
pub enum AllocationStrategy {
FirstFit,
BestFit,
WorstFit,
BuddySystem,
PoolBased,
Linear,
}
pub struct LayoutOptimizer<T: Float + Debug + Send + Sync + 'static> {
access_analyzer: AccessPatternAnalyzer,
on_chip_budget_bytes: usize,
_phantom: std::marker::PhantomData<T>,
}
pub struct BufferManager<T: Float + Debug + Send + Sync + 'static> {
allocator: MemoryAllocator,
_phantom: std::marker::PhantomData<T>,
}
pub struct BandwidthOptimizer<T: Float + Debug + Send + Sync + 'static> {
_phantom: std::marker::PhantomData<T>,
}
pub struct MemoryHierarchyManager<T: Float + Debug + Send + Sync + 'static> {
memory_levels: Vec<MemoryLevel>,
placement_strategy: PlacementStrategy,
_phantom: std::marker::PhantomData<T>,
}
#[derive(Debug, Default)]
pub struct MemoryPlanningStats {
pub total_memory_allocated: usize,
pub peak_memory_usage: usize,
pub fragmentation_ratio: f64,
pub buffer_reuse_ratio: f64,
pub bandwidth_utilization: f64,
pub layout_transformations: usize,
pub level_utilization: HashMap<String, f64>,
}
#[derive(Debug)]
pub struct MemoryPlan<T: Float + Debug + Send + Sync + 'static> {
pub buffer_allocations: HashMap<OperandId, BufferAllocation>,
pub layout_assignments: HashMap<OperandId, Layout>,
pub memory_assignments: HashMap<OperandId, MemorySpace>,
pub execution_order: Vec<MemoryOperation>,
pub total_memory: usize,
pub performance_info: MemoryPerformanceInfo,
_phantom: std::marker::PhantomData<T>,
}
impl<T: Float + Debug + Send + Sync + 'static> MemoryPlan<T> {
pub fn empty() -> Self {
Self {
buffer_allocations: HashMap::new(),
layout_assignments: HashMap::new(),
memory_assignments: HashMap::new(),
execution_order: Vec::new(),
total_memory: 0,
performance_info: MemoryPerformanceInfo::default(),
_phantom: std::marker::PhantomData,
}
}
}
#[derive(Debug, Clone)]
pub struct BufferAllocation {
pub buffer_id: String,
pub address: usize,
pub size: usize,
pub alignment: usize,
pub lifetime: BufferLifetime,
pub access_pattern: AccessPattern,
}
#[derive(Debug, Clone)]
pub struct BufferLifetime {
pub first_use: OperationId,
pub last_use: OperationId,
pub live_range: (usize, usize),
pub reuse_opportunities: Vec<OperandId>,
}
#[derive(Debug, Clone)]
pub enum AccessPattern {
Sequential,
Random,
Strided { stride: usize },
Block { block_size: usize },
Broadcast,
}
#[derive(Debug, Clone)]
pub struct LayoutFormat {
pub name: String,
pub dimension_order: Vec<usize>,
pub layout_type: LayoutType,
pub tiling: Option<TilingSpec>,
pub alignment: usize,
}
#[derive(Debug, Clone)]
pub enum LayoutType {
RowMajor,
ColumnMajor,
Blocked,
Compressed,
Custom(String),
}
#[derive(Debug, Clone)]
pub struct TilingSpec {
pub tile_dims: Vec<usize>,
pub tile_order: Vec<usize>,
pub padding: PaddingStrategy,
}
#[derive(Debug, Clone)]
pub enum PaddingStrategy {
None,
Zero,
EdgeReplicate,
Mirror,
}
pub struct AccessPatternAnalyzer {}
#[derive(Debug)]
pub struct StrideAnalysis {
pub strides: Vec<i64>,
pub regularity: f64,
pub locality: f64,
}
#[derive(Debug)]
pub struct LayoutTransformationRule {
pub name: String,
pub source_pattern: LayoutPattern,
pub target_pattern: LayoutPattern,
pub conditions: Vec<String>,
pub benefit: f64,
}
#[derive(Debug)]
pub struct LayoutPattern {
pub rank_constraints: Vec<RankConstraint>,
pub dimension_constraints: Vec<DimensionConstraint>,
pub access_requirements: Vec<AccessPattern>,
}
#[derive(Debug)]
pub enum RankConstraint {
Exact(usize),
Minimum(usize),
Maximum(usize),
Range(usize, usize),
}
#[derive(Debug)]
pub struct DimensionConstraint {
pub dimension: usize,
pub size_constraint: SizeConstraint,
pub alignment_constraint: Option<usize>,
}
#[derive(Debug)]
pub enum SizeConstraint {
Exact(usize),
MultipleOf(usize),
Range(usize, usize),
Any,
}
#[derive(Debug)]
pub struct BufferInfo {
pub size: usize,
pub allocation: Option<BufferAllocation>,
pub ref_count: usize,
pub access_stats: AccessStatistics,
}
#[derive(Debug)]
pub struct PooledBuffer {
pub id: String,
pub size: usize,
pub available: bool,
pub last_used: u64,
}
pub struct MemoryAllocator {
strategy: AllocationStrategy,
free_regions: BTreeMap<usize, usize>,
allocated_regions: HashMap<usize, usize>,
total_capacity: usize,
current_usage: usize,
}
pub struct BufferReuseTracker {}
#[derive(Debug)]
pub struct ReuseCandidate {
pub source_buffer: OperandId,
pub target_buffer: OperandId,
pub score: f64,
pub size_compatible: bool,
}
#[derive(Debug, Default)]
pub struct ReuseStatistics {
pub total_opportunities: usize,
pub successful_reuses: usize,
pub memory_saved: usize,
}
#[derive(Debug, Default)]
pub struct AccessStatistics {
pub read_count: usize,
pub write_count: usize,
pub pattern: Option<AccessPattern>,
pub avg_access_size: usize,
}
pub struct MemoryAccessSchedule {}
#[derive(Debug)]
pub struct ScheduledAccess {
pub operation_id: OperationId,
pub access_type: MemoryAccessType,
pub buffer_id: OperandId,
pub scheduled_time: u64,
pub size: usize,
}
#[derive(Debug)]
pub enum MemoryAccessType {
Read,
Write,
ReadModifyWrite,
Prefetch,
}
#[derive(Debug)]
pub struct MemoryPressurePoint {
pub time: u64,
pub memory_usage: usize,
pub bandwidth_usage: f64,
}
#[derive(Debug)]
pub struct PrefetchStrategy {
pub name: String,
pub distance: usize,
pub confidence_threshold: f64,
pub target_level: MemorySpace,
}
pub struct CacheManager {}
#[derive(Debug)]
pub struct CacheLevel {
pub id: String,
pub size: usize,
pub line_size: usize,
pub associativity: usize,
pub latency: u32,
}
#[derive(Debug)]
pub enum CachePolicy {
LRU,
LFU,
FIFO,
Random,
Optimal,
}
#[derive(Debug)]
pub struct MemoryLevel {
pub id: String,
pub memory_space: MemorySpace,
pub capacity: usize,
pub bandwidth: f64,
pub latency: u32,
pub power: f64,
}
#[derive(Debug)]
pub enum PlacementStrategy {
FastestAvailable,
AccessFrequency,
SizeBased,
Manual,
MLGuided,
}
#[derive(Debug)]
pub struct MigrationPolicy {
pub name: String,
pub trigger: MigrationTrigger,
pub source_level: MemorySpace,
pub target_level: MemorySpace,
pub cost_model: CostModel,
}
#[derive(Debug)]
pub enum MigrationTrigger {
AccessFrequency(f64),
MemoryPressure(f64),
TimeBased(u64),
Predictive,
}
#[derive(Debug)]
pub struct CostModel {
pub migration_cost: f64,
pub access_cost_diff: f64,
pub energy_cost_diff: f64,
}
#[derive(Debug)]
pub enum MemoryOperation {
Allocate {
buffer_id: OperandId,
size: usize,
alignment: usize,
},
Deallocate { buffer_id: OperandId },
Copy {
source: OperandId,
target: OperandId,
size: usize,
},
Prefetch {
buffer_id: OperandId,
target_level: MemorySpace,
},
}
#[derive(Debug, Default)]
pub struct MemoryPerformanceInfo {
pub bandwidth_utilization: f64,
pub avg_access_latency: f64,
pub efficiency_score: f64,
pub cache_hit_rates: HashMap<String, f64>,
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> MemoryPlanner<T> {
pub fn new(target_hardware: TPUConfig) -> Self {
Self {
layout_optimizer: LayoutOptimizer::new(&target_hardware),
buffer_manager: BufferManager::new(),
bandwidth_optimizer: BandwidthOptimizer::new(&target_hardware),
hierarchy_manager: MemoryHierarchyManager::new(&target_hardware),
allocation_strategy: AllocationStrategy::BestFit,
planning_stats: MemoryPlanningStats::default(),
}
}
pub fn create_memory_plan(&mut self, computation: &XLAComputation<T>) -> Result<MemoryPlan<T>> {
let memory_analysis = self.analyze_memory_requirements(computation)?;
let layout_assignments = self.layout_optimizer.optimize_layouts(computation)?;
let buffer_allocations = self
.buffer_manager
.allocate_buffers(&memory_analysis, self.allocation_strategy.clone())?;
let memory_assignments = self
.hierarchy_manager
.assign_memory_levels(&memory_analysis)?;
let execution_order = self
.bandwidth_optimizer
.schedule_memory_operations(computation)?;
let performance_info =
self.calculate_performance_info(&buffer_allocations, &memory_assignments)?;
let total_memory: usize = buffer_allocations.values().map(|alloc| alloc.size).sum();
let largest = buffer_allocations
.values()
.map(|alloc| alloc.size)
.max()
.unwrap_or(0);
self.planning_stats.total_memory_allocated += total_memory;
self.planning_stats.peak_memory_usage =
self.planning_stats.peak_memory_usage.max(total_memory);
self.planning_stats.fragmentation_ratio = if total_memory == 0 {
0.0
} else {
1.0 - (largest as f64 / total_memory as f64)
};
self.planning_stats.bandwidth_utilization = performance_info.bandwidth_utilization;
self.planning_stats.layout_transformations += layout_assignments.len();
self.planning_stats.level_utilization.clear();
for (operand_id, space) in &memory_assignments {
let bytes = buffer_allocations
.get(operand_id)
.map(|alloc| alloc.size)
.unwrap_or(0);
*self
.planning_stats
.level_utilization
.entry(format!("{space:?}"))
.or_insert(0.0) += bytes as f64;
}
Ok(MemoryPlan {
buffer_allocations,
layout_assignments,
memory_assignments,
execution_order,
total_memory,
performance_info,
_phantom: std::marker::PhantomData,
})
}
pub fn planning_statistics(&self) -> &MemoryPlanningStats {
&self.planning_stats
}
pub fn optimize_memory_layout(
&mut self,
computation: XLAComputation<T>,
) -> Result<XLAComputation<T>> {
let memory_plan = self.create_memory_plan(&computation)?;
let mut optimized_computation = computation;
for (operand_id, layout) in memory_plan.layout_assignments {
if let Some(operand) = optimized_computation.operands.get_mut(&operand_id) {
operand.layout = layout;
}
}
for (operand_id, memory_space) in memory_plan.memory_assignments {
if let Some(operand) = optimized_computation.operands.get_mut(&operand_id) {
operand.layout.memory_space = memory_space;
}
}
Ok(optimized_computation)
}
fn analyze_memory_requirements(
&self,
computation: &XLAComputation<T>,
) -> Result<MemoryAnalysis> {
let mut analysis = MemoryAnalysis::default();
for (operand_id, operand) in &computation.operands {
let size = self.calculate_operand_size(operand)?;
let lifetime = self.calculate_operand_lifetime(*operand_id, computation)?;
let access_pattern = self.analyze_access_pattern(*operand_id, computation)?;
analysis.operand_info.insert(
*operand_id,
OperandMemoryInfo {
size,
lifetime,
access_pattern,
alignment_requirements: vec![32], },
);
}
analysis.total_memory = analysis.operand_info.values().map(|info| info.size).sum();
Ok(analysis)
}
fn calculate_operand_size(&self, operand: &Operand<T>) -> Result<usize> {
Ok(operand
.shape
.element_count
.saturating_mul(data_type_size(operand.dtype)))
}
fn calculate_operand_lifetime(
&self,
operand_id: OperandId,
computation: &XLAComputation<T>,
) -> Result<BufferLifetime> {
let mut first_use = None;
let mut last_use = None;
let mut first_index = None;
let mut last_index = None;
for (index, operation) in computation.operations.iter().enumerate() {
if operation.inputs.contains(&operand_id) || operation.output == operand_id {
if first_use.is_none() {
first_use = Some(operation.id);
first_index = Some(index);
}
last_use = Some(operation.id);
last_index = Some(index);
}
}
let live_range = match (first_index, last_index) {
(Some(first), Some(last)) => (first, last),
_ => (0, 0),
};
Ok(BufferLifetime {
first_use: first_use.unwrap_or(super::super::frontend::graph_capture::OperationId(0)),
last_use: last_use.unwrap_or(super::super::frontend::graph_capture::OperationId(0)),
live_range,
reuse_opportunities: vec![],
})
}
fn analyze_access_pattern(
&self,
_operand_id: OperandId,
_computation: &XLAComputation<T>,
) -> Result<AccessPattern> {
Ok(AccessPattern::Sequential)
}
fn calculate_performance_info(
&self,
_buffer_allocations: &HashMap<OperandId, BufferAllocation>,
_memory_assignments: &HashMap<OperandId, MemorySpace>,
) -> Result<MemoryPerformanceInfo> {
Ok(MemoryPerformanceInfo {
bandwidth_utilization: 0.8,
avg_access_latency: 100.0, efficiency_score: 0.85,
cache_hit_rates: HashMap::new(),
})
}
}
#[derive(Debug, Default)]
pub struct MemoryAnalysis {
pub operand_info: HashMap<OperandId, OperandMemoryInfo>,
pub total_memory: usize,
pub peak_memory: usize,
pub access_patterns: HashMap<OperandId, AccessPattern>,
}
#[derive(Debug)]
pub struct OperandMemoryInfo {
pub size: usize,
pub lifetime: BufferLifetime,
pub access_pattern: AccessPattern,
pub alignment_requirements: Vec<usize>,
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> LayoutOptimizer<T> {
pub fn new(target_hardware: &TPUConfig) -> Self {
Self {
access_analyzer: AccessPatternAnalyzer::new(),
on_chip_budget_bytes: on_chip_budget_bytes(target_hardware.tpu_version),
_phantom: std::marker::PhantomData,
}
}
pub fn optimize_layouts(
&mut self,
computation: &XLAComputation<T>,
) -> Result<HashMap<OperandId, Layout>> {
let mut layout_assignments = HashMap::new();
self.access_analyzer.analyze_computation(computation)?;
for (operand_id, operand) in &computation.operands {
let optimal_layout = self.select_optimal_layout(*operand_id, operand)?;
layout_assignments.insert(*operand_id, optimal_layout);
}
Ok(layout_assignments)
}
fn select_optimal_layout(&self, operand_id: OperandId, operand: &Operand<T>) -> Result<Layout> {
let rank = operand.shape.dimensions.len();
let minor_to_major: Vec<usize> = (0..rank).rev().collect();
let element_bytes = data_type_size(operand.dtype);
let operand_bytes = operand.shape.element_count.saturating_mul(element_bytes);
let on_chip_budget = self.on_chip_budget_bytes;
let memory_space = if operand_bytes <= on_chip_budget {
MemorySpace::Device
} else {
MemorySpace::Default
};
debug_assert_eq!(
operand.id, operand_id,
"layout assignment must describe the operand it is keyed by"
);
Ok(Layout {
minor_to_major,
tiles: vec![],
memory_space,
})
}
}
impl Default for AccessPatternAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl AccessPatternAnalyzer {
pub fn new() -> Self {
Self {}
}
pub fn analyze_computation<T: Float + Debug + Send + Sync + 'static>(
&mut self,
_computation: &XLAComputation<T>,
) -> Result<()> {
Ok(())
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> Default
for BufferManager<T>
{
fn default() -> Self {
Self::new()
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> BufferManager<T> {
pub fn new() -> Self {
Self {
allocator: MemoryAllocator::new(AllocationStrategy::BestFit, 1024 * 1024 * 1024), _phantom: std::marker::PhantomData,
}
}
pub fn allocate_buffers(
&mut self,
analysis: &MemoryAnalysis,
strategy: AllocationStrategy,
) -> Result<HashMap<OperandId, BufferAllocation>> {
self.allocator.set_strategy(strategy);
let mut allocations = HashMap::new();
for (operand_id, operand_info) in &analysis.operand_info {
let mut allocation = self.allocator.allocate(operand_info.size, 32)?;
allocation.lifetime = operand_info.lifetime.clone();
allocations.insert(*operand_id, allocation);
}
Ok(allocations)
}
}
impl MemoryAllocator {
pub fn new(strategy: AllocationStrategy, capacity: usize) -> Self {
let mut free_regions = BTreeMap::new();
free_regions.insert(0, capacity);
Self {
strategy,
free_regions,
allocated_regions: HashMap::new(),
total_capacity: capacity,
current_usage: 0,
}
}
pub fn set_strategy(&mut self, strategy: AllocationStrategy) {
self.strategy = strategy;
}
pub fn allocate(&mut self, size: usize, alignment: usize) -> Result<BufferAllocation> {
if alignment == 0 || (alignment & (alignment - 1)) != 0 {
return Err(OptimError::InvalidArgument(
scirs2_core::error::ErrorContext::new(format!(
"Allocation alignment must be a non-zero power of two, got {alignment}"
)),
));
}
let aligned_size = (size.max(1) + alignment - 1) & !(alignment - 1);
let address = self.select_region(aligned_size).ok_or_else(|| {
OptimError::AllocationError(scirs2_core::error::ErrorContext::new(format!(
"Out of memory: cannot allocate {aligned_size} bytes (usage {}/{})",
self.current_usage, self.total_capacity
)))
})?;
let region_size = self.free_regions.remove(&address).ok_or_else(|| {
OptimError::InvalidState(scirs2_core::error::ErrorContext::new(format!(
"Selected free region at address {address} vanished from the free list"
)))
})?;
self.allocated_regions.insert(address, aligned_size);
self.current_usage += aligned_size;
if region_size > aligned_size {
self.free_regions
.insert(address + aligned_size, region_size - aligned_size);
}
Ok(BufferAllocation {
buffer_id: format!("buf_{}", address),
address,
size: aligned_size,
alignment,
lifetime: BufferLifetime {
first_use: super::super::frontend::graph_capture::OperationId(0),
last_use: super::super::frontend::graph_capture::OperationId(0),
live_range: (0, 0),
reuse_opportunities: vec![],
},
access_pattern: AccessPattern::Sequential,
})
}
fn select_region(&self, needed: usize) -> Option<usize> {
let mut chosen: Option<(usize, usize)> = None;
for (&address, &size) in self.free_regions.iter() {
if size < needed {
continue;
}
match self.strategy {
AllocationStrategy::FirstFit
| AllocationStrategy::Linear
| AllocationStrategy::BuddySystem
| AllocationStrategy::PoolBased => return Some(address),
AllocationStrategy::BestFit => {
if chosen.map(|(_, best)| size < best).unwrap_or(true) {
chosen = Some((address, size));
}
}
AllocationStrategy::WorstFit => {
if chosen.map(|(_, best)| size > best).unwrap_or(true) {
chosen = Some((address, size));
}
}
}
}
chosen.map(|(address, _)| address)
}
pub fn deallocate(&mut self, address: usize) -> Result<()> {
let size = self.allocated_regions.remove(&address).ok_or_else(|| {
OptimError::InvalidArgument(scirs2_core::error::ErrorContext::new(format!(
"Cannot free address {address}: it is not an active allocation"
)))
})?;
self.current_usage = self.current_usage.saturating_sub(size);
self.insert_free_region(address, size);
Ok(())
}
pub fn free(&mut self, allocation: &BufferAllocation) -> Result<()> {
self.deallocate(allocation.address)
}
fn insert_free_region(&mut self, address: usize, size: usize) {
let mut start = address;
let mut end = address + size;
if let Some((&prev_addr, &prev_size)) = self.free_regions.range(..start).next_back() {
if prev_addr + prev_size == start {
self.free_regions.remove(&prev_addr);
start = prev_addr;
}
}
if let Some(&next_size) = self.free_regions.get(&end) {
self.free_regions.remove(&end);
end += next_size;
}
self.free_regions.insert(start, end - start);
}
}
impl Default for BufferReuseTracker {
fn default() -> Self {
Self::new()
}
}
impl BufferReuseTracker {
pub fn new() -> Self {
Self {}
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> BandwidthOptimizer<T> {
pub fn new(_target_hardware: &TPUConfig) -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
pub fn schedule_memory_operations(
&mut self,
_computation: &XLAComputation<T>,
) -> Result<Vec<MemoryOperation>> {
Ok(vec![])
}
}
impl Default for MemoryAccessSchedule {
fn default() -> Self {
Self::new()
}
}
impl MemoryAccessSchedule {
pub fn new() -> Self {
Self {}
}
}
impl Default for CacheManager {
fn default() -> Self {
Self::new()
}
}
impl CacheManager {
pub fn new() -> Self {
Self {}
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> MemoryHierarchyManager<T> {
pub fn new(_target_hardware: &TPUConfig) -> Self {
let memory_levels = vec![
MemoryLevel {
id: "L1".to_string(),
memory_space: MemorySpace::Device,
capacity: 1024 * 1024, bandwidth: 1000e9, latency: 1, power: 10.0, },
MemoryLevel {
id: "HBM".to_string(),
memory_space: MemorySpace::Default,
capacity: 32 * 1024 * 1024 * 1024, bandwidth: 1600e9, latency: 100, power: 200.0, },
];
Self {
memory_levels,
placement_strategy: PlacementStrategy::SizeBased,
_phantom: std::marker::PhantomData,
}
}
pub fn assign_memory_levels(
&mut self,
analysis: &MemoryAnalysis,
) -> Result<HashMap<OperandId, MemorySpace>> {
let mut levels: Vec<&MemoryLevel> = self.memory_levels.iter().collect();
levels.sort_by_key(|level| level.latency);
let fallback = levels
.last()
.map(|level| level.memory_space)
.unwrap_or(MemorySpace::Default);
let mut assignments = HashMap::new();
for (operand_id, info) in &analysis.operand_info {
let space = match self.placement_strategy {
PlacementStrategy::FastestAvailable => levels
.first()
.filter(|level| info.size <= level.capacity)
.map(|level| level.memory_space)
.unwrap_or(fallback),
PlacementStrategy::SizeBased | PlacementStrategy::AccessFrequency => levels
.iter()
.find(|level| info.size <= level.capacity)
.map(|level| level.memory_space)
.unwrap_or(fallback),
PlacementStrategy::Manual | PlacementStrategy::MLGuided => fallback,
};
assignments.insert(*operand_id, space);
}
Ok(assignments)
}
}
#[cfg(test)]
mod tests {
use super::super::super::frontend::OperationType;
use super::*;
#[test]
fn test_memory_planner_creation() {
use crate::main_types::{PodTopology, TPUConfig, TPUVersion};
let tpu_config = TPUConfig {
tpu_version: TPUVersion::V4,
num_cores: 8,
enable_xla: true,
xla_optimization_level: crate::main_types::XLAOptimizationLevel::Standard,
mixed_precision: true,
batch_size_per_core: 32,
enable_pod_coordination: false,
pod_topology: PodTopology::Pod2x2,
memory_optimization: crate::main_types::TPUMemoryOptimization::Balanced,
gradient_compression: true,
prefetch_depth: 2,
experimental_features: false,
};
let planner: MemoryPlanner<f32> = MemoryPlanner::new(tpu_config);
assert_eq!(planner.planning_stats.total_memory_allocated, 0);
}
#[test]
fn test_memory_allocator() {
let mut allocator = MemoryAllocator::new(AllocationStrategy::BestFit, 1024);
let allocation = allocator.allocate(256, 32).expect("unwrap failed");
assert_eq!(allocation.size, 256);
assert_eq!(allocation.alignment, 32);
assert!(allocator.current_usage >= 256);
}
fn test_tpu_config() -> crate::main_types::TPUConfig {
use crate::main_types::{PodTopology, TPUConfig, TPUVersion};
TPUConfig {
tpu_version: TPUVersion::V4,
num_cores: 8,
enable_xla: true,
xla_optimization_level: crate::main_types::XLAOptimizationLevel::Standard,
mixed_precision: true,
batch_size_per_core: 32,
enable_pod_coordination: false,
pod_topology: PodTopology::Pod2x2,
memory_optimization: crate::main_types::TPUMemoryOptimization::Balanced,
gradient_compression: true,
prefetch_depth: 2,
experimental_features: false,
}
}
#[test]
fn freed_region_is_reused() {
let mut allocator = MemoryAllocator::new(AllocationStrategy::FirstFit, 1024);
let first = allocator.allocate(128, 32).expect("first allocation");
let _second = allocator.allocate(128, 32).expect("second allocation");
assert_eq!(first.address, 0);
allocator.deallocate(first.address).expect("free first");
let third = allocator.allocate(128, 32).expect("third allocation");
assert_eq!(
third.address, first.address,
"third allocation must reuse the freed hole"
);
}
#[test]
fn adjacent_frees_coalesce() {
let mut allocator = MemoryAllocator::new(AllocationStrategy::FirstFit, 256);
let a = allocator.allocate(128, 32).expect("alloc a");
let b = allocator.allocate(128, 32).expect("alloc b");
assert_eq!(a.address, 0);
assert_eq!(b.address, 128);
allocator.deallocate(a.address).expect("free a");
allocator.deallocate(b.address).expect("free b");
let big = allocator
.allocate(256, 32)
.expect("coalesced region must satisfy the full-size request");
assert_eq!(big.address, 0);
assert_eq!(big.size, 256);
}
fn carve_two_holes(strategy: AllocationStrategy) -> MemoryAllocator {
let mut allocator = MemoryAllocator::new(strategy, 1024);
let hole_big = allocator.allocate(256, 32).expect("carve big");
let _keep1 = allocator.allocate(32, 32).expect("keep 1");
let hole_small = allocator.allocate(64, 32).expect("carve small");
let _keep2 = allocator.allocate(32, 32).expect("keep 2");
allocator
.deallocate(hole_big.address)
.expect("free big hole");
allocator
.deallocate(hole_small.address)
.expect("free small hole");
allocator
}
#[test]
fn strategies_pick_different_regions() {
let mut first_fit = carve_two_holes(AllocationStrategy::FirstFit);
let a = first_fit.allocate(64, 32).expect("first-fit alloc");
assert_eq!(a.address, 0);
let mut best_fit = carve_two_holes(AllocationStrategy::BestFit);
let b = best_fit.allocate(64, 32).expect("best-fit alloc");
assert_eq!(b.address, 288);
let mut worst_fit = carve_two_holes(AllocationStrategy::WorstFit);
let c = worst_fit.allocate(64, 32).expect("worst-fit alloc");
assert_eq!(c.address, 384);
assert_ne!(a.address, b.address);
assert_ne!(a.address, c.address);
assert_ne!(b.address, c.address);
}
#[test]
fn live_range_is_per_operand() {
use crate::xla::frontend::graph_capture::test_support::{add_op, shape};
use crate::xla::frontend::graph_capture::ComputationGraphBuilder;
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut comp = builder.create_computation("live_range");
let a = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
shape(&[4]),
);
let b = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
shape(&[4]),
);
let c = add_op(
&mut builder,
&mut comp,
OperationType::Add,
vec![a, b],
shape(&[4]),
);
let d = add_op(
&mut builder,
&mut comp,
OperationType::Negate,
vec![c],
shape(&[4]),
);
let planner: MemoryPlanner<f32> = MemoryPlanner::new(test_tpu_config());
let analysis = planner
.analyze_memory_requirements(&comp)
.expect("memory analysis");
let ops = comp.operations.len();
let live_range = |id| {
analysis
.operand_info
.get(&id)
.map(|info| info.lifetime.live_range)
.expect("operand info present")
};
assert_eq!(live_range(a), (0, 2));
assert_eq!(live_range(b), (1, 2));
assert_eq!(live_range(c), (2, 3));
assert_eq!(live_range(d), (3, 3));
for id in [a, b, c, d] {
assert_ne!(
live_range(id),
(0, ops),
"live_range must be per-operand, not whole-program"
);
}
}
}