Files
rustytorch/crates/training/rtx-distributed/src/activation_checkpointing.rs
T
osobhandClaude Sonnet 5 4aaa36a57a style: cargo fmt --workspace (whitespace/wrapping only, no semantic change)
Whole-workspace rustfmt pass picked up while iterating on Mamba GPU
backward work. Verified formatting-only via diff sampling; no logic
changed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-10 07:09:36 -07:00

1304 lines
43 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Activation Checkpointing for Memory Optimization
//!
//! This module implements gradient checkpointing (also known as activation
//! checkpointing or rematerialization) to reduce memory usage during training.
//! Instead of storing all activations for the backward pass, we selectively
//! store checkpoints and recompute activations when needed.
//!
//! # Memory Savings
//! - Without checkpointing: O(n) memory for n layers
//! - With sqrt checkpointing: O(sqrt(n)) memory
//! - With selective checkpointing: O(k) memory for k checkpoints
//!
//! # Example
//! ```rust,ignore
//! use rtx_distributed::activation_checkpointing::{
//! CheckpointedFunction, CheckpointPolicy, CheckpointManager
//! };
//!
//! let policy = CheckpointPolicy::sqrt(); // Checkpoint every sqrt(n) layers
//! let manager = CheckpointManager::new(policy);
//!
//! // Wrap forward function
//! let output = manager.checkpoint(|| {
//! model.forward(&input)
//! });
//! ```
use crate::error::{DistributedError, Result};
use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
// =============================================================================
// Checkpoint Policy
// =============================================================================
/// Checkpointing policy determines which layers to checkpoint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CheckpointPolicy {
/// No checkpointing (store all activations)
None,
/// Checkpoint every N layers
Every { interval: usize },
/// Checkpoint at sqrt(n) intervals (optimal for memory/compute tradeoff)
Sqrt,
/// Checkpoint specific layers by name or index
Selective { layers: Vec<String> },
/// Checkpoint all layers (maximum memory savings, highest compute cost)
All,
/// Adaptive based on available memory.
///
/// Uses a tiered heuristic based on `target_memory_mb`:
/// - `>4096 MB` (generous): checkpoint every `ceil(sqrt(n))` layers
/// - `10244096 MB` (moderate): checkpoint every other layer
/// - `<1024 MB` (tight): checkpoint every layer
Adaptive { target_memory_mb: usize },
/// Custom policy with a function
Custom { name: String },
/// Checkpoint only attention-family layers (identified by name matching).
///
/// FFN/MLP layers are intentionally skipped — they are cheaper to recompute
/// from a saved input than to store, yielding ~40% memory savings with
/// ~60% less recompute overhead compared to `CheckpointPolicy::All`.
AttentionSelective {
/// Substrings that identify attention layers in their name.
/// A layer is checkpointed when its name contains any of these patterns.
attention_patterns: Vec<String>,
},
}
impl CheckpointPolicy {
/// Create policy with no checkpointing
pub fn none() -> Self {
CheckpointPolicy::None
}
/// Checkpoint every N layers
pub fn every(n: usize) -> Self {
CheckpointPolicy::Every { interval: n }
}
/// Optimal sqrt(n) checkpointing
pub fn sqrt() -> Self {
CheckpointPolicy::Sqrt
}
/// Checkpoint specific layers
pub fn selective(layers: Vec<String>) -> Self {
CheckpointPolicy::Selective { layers }
}
/// Checkpoint all layers (maximum memory savings)
pub fn all() -> Self {
CheckpointPolicy::All
}
/// Adaptive based on memory target
pub fn adaptive(target_memory_mb: usize) -> Self {
CheckpointPolicy::Adaptive { target_memory_mb }
}
/// Checkpoint only attention-family layers.
///
/// Uses the standard set of attention-related name patterns:
/// `"attn"`, `"attention"`, `"self_attn"`, `"cross_attn"`, `"mha"`.
/// FFN/MLP blocks are not checkpointed, cutting recompute cost by ~60%
/// relative to `CheckpointPolicy::All` while still saving ~40% of
/// transformer activation memory.
pub fn attention_selective() -> Self {
CheckpointPolicy::AttentionSelective {
attention_patterns: vec![
"attn".to_string(),
"attention".to_string(),
"self_attn".to_string(),
"cross_attn".to_string(),
"mha".to_string(),
],
}
}
/// Check if a layer should be checkpointed
pub fn should_checkpoint(
&self,
layer_index: usize,
layer_name: &str,
total_layers: usize,
) -> bool {
match self {
CheckpointPolicy::None => false,
CheckpointPolicy::Every { interval } => layer_index % interval == 0,
CheckpointPolicy::Sqrt => {
let sqrt_n = (total_layers as f64).sqrt().ceil() as usize;
layer_index % sqrt_n == 0
}
CheckpointPolicy::Selective { layers } => {
layers.contains(&layer_name.to_string())
|| layers.contains(&layer_index.to_string())
}
CheckpointPolicy::All => true,
CheckpointPolicy::Adaptive { target_memory_mb } => {
// Tiered heuristic based on memory target.
// We cannot query live GPU memory here without a device handle, so
// we use the target as a proxy for how aggressively to checkpoint:
// - Generous target (>4096 MB): checkpoint every ceil(sqrt(n)) layers
// - Moderate target (10244096 MB): checkpoint every other layer
// - Tight target (<1024 MB): checkpoint every layer
let tier = match *target_memory_mb {
t if t > 4096 => (total_layers as f64).sqrt().ceil() as usize,
t if t > 1024 => 2,
_ => 1,
};
// tier is always >= 1 (sqrt of at least 1 layer rounds up to 1)
layer_index % tier == 0
}
CheckpointPolicy::Custom { .. } => false, // Custom logic handled elsewhere
CheckpointPolicy::AttentionSelective { attention_patterns } => {
// Checkpoint the layer if its name contains any attention pattern.
// Layer index and total_layers are intentionally not used: the
// decision is purely structural (attention vs. non-attention).
attention_patterns
.iter()
.any(|pat| layer_name.contains(pat.as_str()))
}
}
}
/// Estimate memory savings factor
pub fn memory_savings_factor(&self, total_layers: usize) -> f64 {
match self {
CheckpointPolicy::None => 1.0,
CheckpointPolicy::Every { interval } => *interval as f64,
CheckpointPolicy::Sqrt => (total_layers as f64).sqrt(),
CheckpointPolicy::Selective { layers } => {
total_layers as f64 / (layers.len().max(1) as f64)
}
CheckpointPolicy::All => total_layers as f64,
CheckpointPolicy::Adaptive { .. } => 2.0, // Rough estimate
CheckpointPolicy::Custom { .. } => 1.5, // Rough estimate
CheckpointPolicy::AttentionSelective { .. } => {
// Attention blocks constitute ~40% of transformer activation memory.
// Checkpointing only them therefore saves ~40% of total activations,
// expressed here as the number of layers' worth of memory freed.
total_layers as f64 * 0.4
}
}
}
}
// =============================================================================
// Activation Storage
// =============================================================================
/// Stored activation data
#[derive(Debug)]
pub struct StoredActivation {
/// Layer name or identifier
pub layer_id: String,
/// Activation tensor data (serialized)
pub data: Vec<u8>,
/// Original shape
pub shape: Vec<usize>,
/// Data type (e.g., "f32", "f16")
pub dtype: String,
/// Memory size in bytes
pub size_bytes: usize,
/// Whether this is a checkpoint (will be recomputed if false)
pub is_checkpoint: bool,
/// Timestamp when stored
pub stored_at: std::time::Instant,
}
impl StoredActivation {
/// Create new stored activation
pub fn new(layer_id: String, data: Vec<u8>, shape: Vec<usize>, dtype: String) -> Self {
let size_bytes = data.len();
Self {
layer_id,
data,
shape,
dtype,
size_bytes,
is_checkpoint: false,
stored_at: std::time::Instant::now(),
}
}
/// Mark as checkpoint
pub fn as_checkpoint(mut self) -> Self {
self.is_checkpoint = true;
self
}
}
/// Activation storage manager
#[derive(Debug)]
pub struct ActivationStorage {
/// Stored activations by layer ID
activations: HashMap<String, StoredActivation>,
/// Total memory used
total_memory: usize,
/// Memory limit
memory_limit: usize,
/// Statistics
stats: ActivationStats,
}
/// Activation storage statistics
#[derive(Debug, Default, Clone)]
pub struct ActivationStats {
/// Number of activations stored
pub stored_count: usize,
/// Number of activations recomputed
pub recomputed_count: usize,
/// Total bytes stored
pub bytes_stored: usize,
/// Bytes saved by checkpointing
pub bytes_saved: usize,
/// Peak memory usage
pub peak_memory: usize,
}
impl ActivationStorage {
/// Create new activation storage
pub fn new(memory_limit: usize) -> Self {
Self {
activations: HashMap::new(),
total_memory: 0,
memory_limit,
stats: ActivationStats::default(),
}
}
/// Store an activation
pub fn store(&mut self, activation: StoredActivation) -> Result<()> {
let size = activation.size_bytes;
// Check memory limit
if self.total_memory + size > self.memory_limit {
// Try to evict non-checkpoint activations
self.evict_until_free(size)?;
}
let layer_id = activation.layer_id.clone();
self.total_memory += size;
self.stats.stored_count += 1;
self.stats.bytes_stored += size;
self.stats.peak_memory = self.stats.peak_memory.max(self.total_memory);
self.activations.insert(layer_id, activation);
Ok(())
}
/// Get an activation
pub fn get(&self, layer_id: &str) -> Option<&StoredActivation> {
self.activations.get(layer_id)
}
/// Remove an activation
pub fn remove(&mut self, layer_id: &str) -> Option<StoredActivation> {
if let Some(activation) = self.activations.remove(layer_id) {
self.total_memory -= activation.size_bytes;
Some(activation)
} else {
None
}
}
/// Clear all activations
pub fn clear(&mut self) {
self.activations.clear();
self.total_memory = 0;
}
/// Evict non-checkpoint activations until enough space is free
fn evict_until_free(&mut self, needed: usize) -> Result<()> {
let mut to_remove = Vec::new();
for (id, activation) in &self.activations {
if !activation.is_checkpoint {
to_remove.push(id.clone());
self.stats.bytes_saved += activation.size_bytes;
}
}
for id in to_remove {
self.remove(&id);
if self.memory_limit - self.total_memory >= needed {
break;
}
}
if self.memory_limit - self.total_memory < needed {
return Err(DistributedError::configuration(format!(
"Cannot free enough memory: need {} bytes, have {} available",
needed,
self.memory_limit - self.total_memory
)));
}
Ok(())
}
/// Get current memory usage
pub fn memory_usage(&self) -> usize {
self.total_memory
}
/// Get statistics
pub fn stats(&self) -> &ActivationStats {
&self.stats
}
}
// =============================================================================
// Checkpoint Context
// =============================================================================
/// Context for checkpointed computation
#[derive(Debug)]
pub struct CheckpointContext {
/// Forward function inputs (saved for recomputation)
pub inputs: Vec<Box<dyn Any + Send + Sync>>,
/// Saved tensors for backward
pub saved_tensors: Vec<Box<dyn Any + Send + Sync>>,
/// Whether we need to recompute on backward
pub needs_recompute: bool,
/// Layer identifier
pub layer_id: String,
}
impl CheckpointContext {
/// Create new checkpoint context
pub fn new(layer_id: String) -> Self {
Self {
inputs: Vec::new(),
saved_tensors: Vec::new(),
needs_recompute: false,
layer_id,
}
}
/// Save input for potential recomputation
pub fn save_input<T: Any + Send + Sync + 'static>(&mut self, input: T) {
self.inputs.push(Box::new(input));
}
/// Save tensor for backward pass
pub fn save_for_backward<T: Any + Send + Sync + 'static>(&mut self, tensor: T) {
self.saved_tensors.push(Box::new(tensor));
}
/// Mark that this context needs recomputation
pub fn mark_recompute(&mut self) {
self.needs_recompute = true;
}
}
// =============================================================================
// Checkpointed Segment
// =============================================================================
/// A segment of the model that can be checkpointed
#[derive(Debug)]
pub struct CheckpointedSegment {
/// Segment identifier
pub id: String,
/// Layers in this segment
pub layers: Vec<String>,
/// Input tensors (saved for recomputation)
inputs: RwLock<Vec<Vec<u8>>>,
/// Output tensors
outputs: RwLock<Vec<Vec<u8>>>,
/// Whether outputs are currently stored
outputs_stored: RwLock<bool>,
}
impl CheckpointedSegment {
/// Create new checkpointed segment
pub fn new(id: String, layers: Vec<String>) -> Self {
Self {
id,
layers,
inputs: RwLock::new(Vec::new()),
outputs: RwLock::new(Vec::new()),
outputs_stored: RwLock::new(false),
}
}
/// Save inputs for potential recomputation
pub fn save_inputs(&self, inputs: Vec<Vec<u8>>) {
*self.inputs.write() = inputs;
}
/// Save outputs (only done for checkpoints)
pub fn save_outputs(&self, outputs: Vec<Vec<u8>>) {
*self.outputs.write() = outputs;
*self.outputs_stored.write() = true;
}
/// Get saved inputs
pub fn get_inputs(&self) -> Vec<Vec<u8>> {
self.inputs.read().clone()
}
/// Get saved outputs
pub fn get_outputs(&self) -> Option<Vec<Vec<u8>>> {
if *self.outputs_stored.read() {
Some(self.outputs.read().clone())
} else {
None
}
}
/// Clear saved outputs (after recomputation)
pub fn clear_outputs(&self) {
self.outputs.write().clear();
*self.outputs_stored.write() = false;
}
/// Check if outputs are available
pub fn has_outputs(&self) -> bool {
*self.outputs_stored.read()
}
}
// =============================================================================
// Checkpoint Manager
// =============================================================================
/// Manager for activation checkpointing
#[derive(Debug)]
pub struct ActivationCheckpointManager {
/// Checkpointing policy
policy: CheckpointPolicy,
/// Activation storage
storage: Mutex<ActivationStorage>,
/// Registered segments
segments: RwLock<HashMap<String, Arc<CheckpointedSegment>>>,
/// Current layer index (for policy decisions)
current_layer: RwLock<usize>,
/// Total number of layers
total_layers: RwLock<usize>,
/// Recomputation count
recompute_count: RwLock<usize>,
/// Whether checkpointing is enabled
enabled: RwLock<bool>,
}
impl ActivationCheckpointManager {
/// Create new checkpoint manager
pub fn new(policy: CheckpointPolicy) -> Self {
Self {
policy,
storage: Mutex::new(ActivationStorage::new(8 * 1024 * 1024 * 1024)), // 8GB default
segments: RwLock::new(HashMap::new()),
current_layer: RwLock::new(0),
total_layers: RwLock::new(0),
recompute_count: RwLock::new(0),
enabled: RwLock::new(true),
}
}
/// Create with memory limit
pub fn with_memory_limit(policy: CheckpointPolicy, memory_limit: usize) -> Self {
Self {
policy,
storage: Mutex::new(ActivationStorage::new(memory_limit)),
segments: RwLock::new(HashMap::new()),
current_layer: RwLock::new(0),
total_layers: RwLock::new(0),
recompute_count: RwLock::new(0),
enabled: RwLock::new(true),
}
}
/// Set total number of layers
pub fn set_total_layers(&self, n: usize) {
*self.total_layers.write() = n;
}
/// Enable or disable checkpointing
pub fn set_enabled(&self, enabled: bool) {
*self.enabled.write() = enabled;
}
/// Check if checkpointing is enabled
pub fn is_enabled(&self) -> bool {
*self.enabled.read()
}
/// Register a segment for checkpointing
pub fn register_segment(&self, segment: CheckpointedSegment) -> Arc<CheckpointedSegment> {
let segment = Arc::new(segment);
self.segments
.write()
.insert(segment.id.clone(), segment.clone());
segment
}
/// Get a registered segment
pub fn get_segment(&self, id: &str) -> Option<Arc<CheckpointedSegment>> {
self.segments.read().get(id).cloned()
}
/// Check if current layer should be checkpointed
pub fn should_checkpoint_current(&self, layer_name: &str) -> bool {
if !*self.enabled.read() {
return false;
}
let layer_index = *self.current_layer.read();
let total = *self.total_layers.read();
self.policy
.should_checkpoint(layer_index, layer_name, total)
}
/// Advance to next layer
pub fn next_layer(&self) {
*self.current_layer.write() += 1;
}
/// Reset layer counter
pub fn reset_layers(&self) {
*self.current_layer.write() = 0;
}
/// Store activation
pub fn store_activation(
&self,
layer_id: String,
data: Vec<u8>,
shape: Vec<usize>,
dtype: String,
) -> Result<()> {
let activation = StoredActivation::new(layer_id, data, shape, dtype);
self.storage.lock().store(activation)
}
/// Store checkpoint activation
pub fn store_checkpoint(
&self,
layer_id: String,
data: Vec<u8>,
shape: Vec<usize>,
dtype: String,
) -> Result<()> {
let activation = StoredActivation::new(layer_id, data, shape, dtype).as_checkpoint();
self.storage.lock().store(activation)
}
/// Get stored activation
pub fn get_activation(&self, layer_id: &str) -> Option<(Vec<u8>, Vec<usize>, String)> {
self.storage
.lock()
.get(layer_id)
.map(|a| (a.data.clone(), a.shape.clone(), a.dtype.clone()))
}
/// Record a recomputation
pub fn record_recompute(&self) {
*self.recompute_count.write() += 1;
self.storage.lock().stats.recomputed_count += 1;
}
/// Clear all stored activations
pub fn clear(&self) {
self.storage.lock().clear();
self.reset_layers();
}
/// Get memory usage
pub fn memory_usage(&self) -> usize {
self.storage.lock().memory_usage()
}
/// Get statistics
pub fn stats(&self) -> CheckpointStats {
let storage_stats = self.storage.lock().stats().clone();
CheckpointStats {
activations_stored: storage_stats.stored_count,
activations_recomputed: storage_stats.recomputed_count,
bytes_stored: storage_stats.bytes_stored,
bytes_saved: storage_stats.bytes_saved,
peak_memory: storage_stats.peak_memory,
recompute_count: *self.recompute_count.read(),
memory_savings_factor: self.policy.memory_savings_factor(*self.total_layers.read()),
}
}
/// Get estimated memory savings
pub fn estimated_memory_savings(&self) -> f64 {
let total_layers = *self.total_layers.read();
if total_layers == 0 {
return 1.0;
}
self.policy.memory_savings_factor(total_layers)
}
}
/// Checkpoint statistics
#[derive(Debug, Clone)]
pub struct CheckpointStats {
/// Number of activations stored
pub activations_stored: usize,
/// Number of activations recomputed
pub activations_recomputed: usize,
/// Bytes stored
pub bytes_stored: usize,
/// Bytes saved by checkpointing
pub bytes_saved: usize,
/// Peak memory usage
pub peak_memory: usize,
/// Number of recomputations
pub recompute_count: usize,
/// Memory savings factor
pub memory_savings_factor: f64,
}
// =============================================================================
// Checkpoint Function Wrapper
// =============================================================================
/// Trait for functions that can be checkpointed
pub trait CheckpointedFunction {
/// Output type
type Output;
/// Run the function with checkpointing
fn run(&self, ctx: &mut CheckpointContext) -> Self::Output;
/// Recompute the function (for backward pass)
fn recompute(&self, ctx: &CheckpointContext) -> Self::Output;
}
/// Wrapper to checkpoint a closure
pub struct CheckpointedClosure<F, O>
where
F: Fn() -> O,
{
/// The function to checkpoint
func: F,
/// Layer ID for this checkpoint
layer_id: String,
}
impl<F, O> CheckpointedClosure<F, O>
where
F: Fn() -> O,
{
/// Create new checkpointed closure
pub fn new(layer_id: String, func: F) -> Self {
Self { func, layer_id }
}
/// Execute the closure
pub fn execute(&self) -> O {
(self.func)()
}
/// Get layer ID
pub fn layer_id(&self) -> &str {
&self.layer_id
}
}
// =============================================================================
// Shared Checkpoint Manager
// =============================================================================
/// Thread-safe shared checkpoint manager
pub type SharedActivationCheckpointManager = Arc<ActivationCheckpointManager>;
/// Create a shared checkpoint manager
pub fn shared_activation_checkpoint_manager(
policy: CheckpointPolicy,
) -> SharedActivationCheckpointManager {
Arc::new(ActivationCheckpointManager::new(policy))
}
/// Create a shared checkpoint manager with memory limit
pub fn shared_activation_checkpoint_manager_with_limit(
policy: CheckpointPolicy,
memory_limit: usize,
) -> SharedActivationCheckpointManager {
Arc::new(ActivationCheckpointManager::with_memory_limit(
policy,
memory_limit,
))
}
// =============================================================================
// Utility Functions
// =============================================================================
/// Calculate optimal checkpoint interval for given memory budget
pub fn optimal_checkpoint_interval(
num_layers: usize,
activation_size_per_layer: usize,
memory_budget: usize,
) -> usize {
// With checkpointing every k layers:
// Memory = k * activation_size (for storing between checkpoints)
// Recomputation cost = O(k) per backward step
// Optimal is sqrt(n) checkpoints for balanced memory/compute
let sqrt_n = (num_layers as f64).sqrt().ceil() as usize;
// Adjust based on memory budget
let max_interval = memory_budget / activation_size_per_layer.max(1);
sqrt_n.min(max_interval).max(1)
}
/// Estimate memory savings for a given configuration
pub fn estimate_memory_savings(
num_layers: usize,
activation_size_per_layer: usize,
checkpoint_interval: usize,
) -> (usize, usize) {
// Without checkpointing
let without = num_layers * activation_size_per_layer;
// With checkpointing: store only checkpoint interval activations
let num_checkpoints = num_layers / checkpoint_interval.max(1);
let with = checkpoint_interval * activation_size_per_layer
+ num_checkpoints * activation_size_per_layer;
(without, with)
}
// =============================================================================
// Memory-Aware Checkpointer
// =============================================================================
/// Applies adaptive checkpointing decisions based on live memory tracking.
///
/// `MemoryAwareCheckpointer` wraps a [`CheckpointPolicy`] and augments it with
/// a running estimate of activation memory consumed during a forward pass.
/// When that estimate exceeds `target_bytes`, every layer is checkpointed
/// regardless of the underlying policy, preventing OOM conditions.
///
/// The internal counter uses `AtomicUsize` so the checkpointer can be shared
/// across threads without locking overhead.
///
/// # Example
/// ```rust,ignore
/// use rtx_distributed::activation_checkpointing::{
/// CheckpointPolicy, MemoryAwareCheckpointer
/// };
///
/// let checkpointer = MemoryAwareCheckpointer::new(
/// CheckpointPolicy::attention_selective(),
/// 24, // 24 transformer layers
/// 4096, // 4 GB target
/// );
///
/// // During forward pass:
/// checkpointer.record_activation(64 * 1024 * 1024); // 64 MB for this layer
/// let do_ckpt = checkpointer.should_checkpoint(3, "self_attn_0");
/// ```
pub struct MemoryAwareCheckpointer {
/// The base policy used when memory is below the target.
pub policy: CheckpointPolicy,
/// Total number of layers in the model (used by the base policy).
pub total_layers: usize,
/// Estimated bytes of activation memory currently live.
/// Incremented by [`record_activation`] and decremented by
/// [`free_activation`]. Uses `Relaxed` ordering — exact byte counts
/// are not required for correctness, only approximate pressure detection.
estimated_memory_bytes: std::sync::atomic::AtomicUsize,
/// Memory target in bytes. When `estimated_memory_bytes` exceeds this,
/// the checkpointer forces checkpointing on every layer.
pub target_bytes: usize,
}
impl std::fmt::Debug for MemoryAwareCheckpointer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryAwareCheckpointer")
.field("policy", &self.policy)
.field("total_layers", &self.total_layers)
.field(
"estimated_memory_bytes",
&self
.estimated_memory_bytes
.load(std::sync::atomic::Ordering::Relaxed),
)
.field("target_bytes", &self.target_bytes)
.finish()
}
}
impl MemoryAwareCheckpointer {
/// Create a new `MemoryAwareCheckpointer`.
///
/// # Parameters
/// - `policy`: Base checkpointing policy applied when memory is within budget.
/// - `total_layers`: Number of layers in the model.
/// - `target_mb`: Memory budget in mebibytes (MiB = 1024 * 1024 bytes).
pub fn new(policy: CheckpointPolicy, total_layers: usize, target_mb: usize) -> Self {
Self {
policy,
total_layers,
estimated_memory_bytes: std::sync::atomic::AtomicUsize::new(0),
target_bytes: target_mb * 1024 * 1024,
}
}
/// Record that a layer consumed `bytes` of activation memory.
///
/// Call this after each layer's forward pass to keep the pressure
/// estimate current.
pub fn record_activation(&self, bytes: usize) {
self.estimated_memory_bytes
.fetch_add(bytes, std::sync::atomic::Ordering::Relaxed);
}
/// Free tracked memory when activations are released (e.g. after a
/// segment's backward pass completes).
///
/// Saturates at zero; cannot underflow.
pub fn free_activation(&self, bytes: usize) {
// Clamp the subtraction so we never wrap on underflow.
let current = self
.estimated_memory_bytes
.load(std::sync::atomic::Ordering::Relaxed);
let to_sub = bytes.min(current);
self.estimated_memory_bytes
.fetch_sub(to_sub, std::sync::atomic::Ordering::Relaxed);
}
/// Decide whether to checkpoint this layer.
///
/// If `estimated_memory_bytes > target_bytes`, returns `true` unconditionally
/// (memory-pressure override). Otherwise delegates to `self.policy`.
pub fn should_checkpoint(&self, layer_index: usize, layer_name: &str) -> bool {
let current = self
.estimated_memory_bytes
.load(std::sync::atomic::Ordering::Relaxed);
if current > self.target_bytes {
// Memory pressure: checkpoint everything to stay under target.
return true;
}
self.policy
.should_checkpoint(layer_index, layer_name, self.total_layers)
}
/// Current memory pressure as a fraction of the target (0.0 = empty, 1.0 = at target).
///
/// Values above 1.0 indicate the memory budget has been exceeded.
pub fn memory_pressure(&self) -> f32 {
let current = self
.estimated_memory_bytes
.load(std::sync::atomic::Ordering::Relaxed);
current as f32 / self.target_bytes as f32
}
}
// =============================================================================
// Tests
// =============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checkpoint_policy_none() {
let policy = CheckpointPolicy::none();
assert!(!policy.should_checkpoint(0, "layer0", 10));
assert!(!policy.should_checkpoint(5, "layer5", 10));
}
#[test]
fn test_checkpoint_policy_every() {
let policy = CheckpointPolicy::every(3);
assert!(policy.should_checkpoint(0, "layer0", 10));
assert!(!policy.should_checkpoint(1, "layer1", 10));
assert!(!policy.should_checkpoint(2, "layer2", 10));
assert!(policy.should_checkpoint(3, "layer3", 10));
assert!(policy.should_checkpoint(6, "layer6", 10));
}
#[test]
fn test_checkpoint_policy_sqrt() {
let policy = CheckpointPolicy::sqrt();
// For 16 layers, sqrt is 4, so checkpoint every 4 layers
assert!(policy.should_checkpoint(0, "layer0", 16));
assert!(!policy.should_checkpoint(1, "layer1", 16));
assert!(policy.should_checkpoint(4, "layer4", 16));
assert!(policy.should_checkpoint(8, "layer8", 16));
}
#[test]
fn test_checkpoint_policy_selective() {
let policy = CheckpointPolicy::selective(vec!["attention".to_string(), "5".to_string()]);
assert!(policy.should_checkpoint(0, "attention", 10));
assert!(policy.should_checkpoint(5, "mlp", 10));
assert!(!policy.should_checkpoint(3, "norm", 10));
}
#[test]
fn test_checkpoint_policy_all() {
let policy = CheckpointPolicy::all();
for i in 0..10 {
assert!(policy.should_checkpoint(i, &format!("layer{}", i), 10));
}
}
#[test]
fn test_memory_savings_factor() {
let none = CheckpointPolicy::none();
assert_eq!(none.memory_savings_factor(10), 1.0);
let every_2 = CheckpointPolicy::every(2);
assert_eq!(every_2.memory_savings_factor(10), 2.0);
let sqrt = CheckpointPolicy::sqrt();
let factor = sqrt.memory_savings_factor(16);
assert!((factor - 4.0).abs() < 0.01); // sqrt(16) = 4
}
#[test]
fn test_activation_storage() {
let mut storage = ActivationStorage::new(1024 * 1024); // 1MB limit
let activation = StoredActivation::new(
"layer1".to_string(),
vec![0u8; 1024],
vec![32, 32],
"f32".to_string(),
);
assert!(storage.store(activation).is_ok());
assert_eq!(storage.memory_usage(), 1024);
let retrieved = storage.get("layer1");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().shape, vec![32, 32]);
}
#[test]
fn test_activation_storage_eviction() {
let mut storage = ActivationStorage::new(2048); // 2KB limit
// Store non-checkpoint activation
let act1 = StoredActivation::new(
"layer1".to_string(),
vec![0u8; 1024],
vec![32, 32],
"f32".to_string(),
);
storage.store(act1).unwrap();
// Store checkpoint activation
let act2 = StoredActivation::new(
"layer2".to_string(),
vec![0u8; 512],
vec![16, 32],
"f32".to_string(),
)
.as_checkpoint();
storage.store(act2).unwrap();
// Try to store another large activation - should evict non-checkpoint
let act3 = StoredActivation::new(
"layer3".to_string(),
vec![0u8; 1024],
vec![32, 32],
"f32".to_string(),
);
storage.store(act3).unwrap();
// layer1 should be evicted, layer2 (checkpoint) should remain
assert!(storage.get("layer1").is_none());
assert!(storage.get("layer2").is_some());
assert!(storage.get("layer3").is_some());
}
#[test]
fn test_checkpoint_manager() {
let manager = ActivationCheckpointManager::new(CheckpointPolicy::every(2));
manager.set_total_layers(10);
assert!(manager.is_enabled());
assert!(manager.should_checkpoint_current("layer0")); // Index 0
manager.next_layer();
assert!(!manager.should_checkpoint_current("layer1")); // Index 1
manager.next_layer();
assert!(manager.should_checkpoint_current("layer2")); // Index 2
}
#[test]
fn test_checkpoint_manager_disable() {
let manager = ActivationCheckpointManager::new(CheckpointPolicy::all());
manager.set_total_layers(10);
assert!(manager.should_checkpoint_current("layer0"));
manager.set_enabled(false);
assert!(!manager.should_checkpoint_current("layer0"));
manager.set_enabled(true);
assert!(manager.should_checkpoint_current("layer0"));
}
#[test]
fn test_checkpointed_segment() {
let segment = CheckpointedSegment::new(
"segment1".to_string(),
vec!["layer1".to_string(), "layer2".to_string()],
);
assert!(!segment.has_outputs());
segment.save_inputs(vec![vec![1, 2, 3]]);
segment.save_outputs(vec![vec![4, 5, 6]]);
assert!(segment.has_outputs());
assert_eq!(segment.get_inputs(), vec![vec![1, 2, 3]]);
assert_eq!(segment.get_outputs(), Some(vec![vec![4, 5, 6]]));
segment.clear_outputs();
assert!(!segment.has_outputs());
}
#[test]
fn test_optimal_checkpoint_interval() {
// For 16 layers, optimal is sqrt(16) = 4
let interval = optimal_checkpoint_interval(16, 1024 * 1024, 100 * 1024 * 1024);
assert!(interval >= 4);
// With very limited memory, should reduce interval
let interval = optimal_checkpoint_interval(16, 1024 * 1024, 2 * 1024 * 1024);
assert!(interval <= 2);
}
#[test]
fn test_estimate_memory_savings() {
let (without, with) = estimate_memory_savings(16, 1024 * 1024, 4);
// Without: 16 * 1MB = 16MB
assert_eq!(without, 16 * 1024 * 1024);
// With: 4 * 1MB (interval) + 4 * 1MB (checkpoints) = 8MB
// Actually: interval activations + checkpoint activations
assert!(with < without);
}
#[test]
fn test_checkpoint_context() {
let mut ctx = CheckpointContext::new("layer1".to_string());
ctx.save_input(vec![1.0f32, 2.0, 3.0]);
ctx.save_for_backward(vec![4.0f32, 5.0, 6.0]);
ctx.mark_recompute();
assert!(ctx.needs_recompute);
assert_eq!(ctx.inputs.len(), 1);
assert_eq!(ctx.saved_tensors.len(), 1);
}
#[test]
fn test_checkpoint_stats() {
let manager = ActivationCheckpointManager::new(CheckpointPolicy::sqrt());
manager.set_total_layers(16);
manager
.store_activation(
"layer0".to_string(),
vec![0u8; 1024],
vec![32, 32],
"f32".to_string(),
)
.unwrap();
manager.record_recompute();
manager.record_recompute();
let stats = manager.stats();
assert_eq!(stats.activations_stored, 1);
assert_eq!(stats.recompute_count, 2);
assert!((stats.memory_savings_factor - 4.0).abs() < 0.01);
}
// -------------------------------------------------------------------------
// AttentionSelective tests
// -------------------------------------------------------------------------
#[test]
fn test_attention_selective_matches_attn_layers() {
let policy = CheckpointPolicy::attention_selective();
// Standard attention name patterns must be recognised.
assert!(policy.should_checkpoint(0, "self_attn", 12));
assert!(policy.should_checkpoint(0, "cross_attention_layer", 12));
assert!(policy.should_checkpoint(0, "mha_block", 12));
// FFN / MLP layers must not be checkpointed.
assert!(!policy.should_checkpoint(0, "ffn_layer", 12));
assert!(!policy.should_checkpoint(0, "mlp_dense", 12));
assert!(!policy.should_checkpoint(0, "layer_norm", 12));
}
#[test]
fn test_attention_selective_memory_savings_factor() {
let policy = CheckpointPolicy::attention_selective();
let factor = policy.memory_savings_factor(12);
// 12 * 0.4 = 4.8
assert!((factor - 4.8).abs() < 1e-6, "expected 4.8, got {factor}");
}
#[test]
fn test_custom_attention_patterns() {
let policy = CheckpointPolicy::AttentionSelective {
attention_patterns: vec!["transformer_block".to_string()],
};
assert!(policy.should_checkpoint(0, "transformer_block_0", 12));
assert!(!policy.should_checkpoint(0, "linear_layer", 12));
}
// -------------------------------------------------------------------------
// Adaptive (fixed) tests
// -------------------------------------------------------------------------
#[test]
fn test_adaptive_tight_memory_checkpoints_all() {
let policy = CheckpointPolicy::Adaptive {
target_memory_mb: 512,
};
// tier == 1 → every layer is checkpointed
for i in 0..8 {
assert!(
policy.should_checkpoint(i, "any_layer", 8),
"layer {i} should be checkpointed under tight target"
);
}
}
#[test]
fn test_adaptive_moderate_memory_checkpoints_every_other() {
let policy = CheckpointPolicy::Adaptive {
target_memory_mb: 2048,
};
// tier == 2 → even indices only
for i in 0..8usize {
let expected = i % 2 == 0;
assert_eq!(
policy.should_checkpoint(i, "layer", 8),
expected,
"layer {i}: expected={expected}"
);
}
}
#[test]
fn test_adaptive_generous_memory_checkpoints_sqrt() {
let policy = CheckpointPolicy::Adaptive {
target_memory_mb: 8192,
};
let total = 16usize;
// ceil(sqrt(16)) = 4 → checkpoint at indices 0, 4, 8, 12
assert!(policy.should_checkpoint(0, "layer", total));
assert!(!policy.should_checkpoint(1, "layer", total));
assert!(!policy.should_checkpoint(2, "layer", total));
assert!(!policy.should_checkpoint(3, "layer", total));
assert!(policy.should_checkpoint(4, "layer", total));
assert!(!policy.should_checkpoint(5, "layer", total));
assert!(policy.should_checkpoint(8, "layer", total));
assert!(policy.should_checkpoint(12, "layer", total));
assert!(!policy.should_checkpoint(13, "layer", total));
}
// -------------------------------------------------------------------------
// MemoryAwareCheckpointer tests
// -------------------------------------------------------------------------
#[test]
fn test_memory_aware_checkpointer_pressure() {
let checkpointer = MemoryAwareCheckpointer::new(
CheckpointPolicy::attention_selective(),
12,
1024, // 1 GiB target
);
// Freshly created — no memory recorded yet.
assert!(
checkpointer.memory_pressure() < 0.01,
"initial pressure should be near zero"
);
checkpointer.record_activation(512 * 1024 * 1024); // 512 MiB
let pressure = checkpointer.memory_pressure();
assert!(
pressure > 0.49 && pressure < 0.51,
"pressure after 512 MiB / 1024 MiB should be ~0.5, got {pressure}"
);
}
#[test]
fn test_memory_aware_checkpointer_fallback_under_pressure() {
let checkpointer = MemoryAwareCheckpointer::new(
CheckpointPolicy::None, // normally checkpoints nothing
12,
256, // 256 MiB target
);
checkpointer.record_activation(300 * 1024 * 1024); // exceeds target
// Memory pressure override must force checkpointing even though policy is None.
assert!(
checkpointer.should_checkpoint(0, "ffn_layer"),
"should force-checkpoint under memory pressure regardless of policy"
);
}
#[test]
fn test_memory_aware_free_reduces_pressure() {
let checkpointer = MemoryAwareCheckpointer::new(CheckpointPolicy::None, 12, 1024);
checkpointer.record_activation(200 * 1024 * 1024);
checkpointer.free_activation(200 * 1024 * 1024);
assert!(
checkpointer.memory_pressure() < 0.01,
"pressure should return to near zero after freeing all recorded memory"
);
}
#[test]
fn test_memory_aware_free_does_not_underflow() {
// Freeing more than was recorded must saturate at zero, not wrap.
let checkpointer = MemoryAwareCheckpointer::new(CheckpointPolicy::None, 4, 512);
checkpointer.record_activation(10 * 1024 * 1024);
checkpointer.free_activation(50 * 1024 * 1024); // larger than recorded
assert_eq!(
checkpointer
.estimated_memory_bytes
.load(std::sync::atomic::Ordering::Relaxed),
0,
"counter must not underflow to usize::MAX"
);
}
#[test]
fn test_memory_aware_checkpointer_delegates_to_policy() {
// Below pressure threshold the policy should be respected.
let checkpointer = MemoryAwareCheckpointer::new(
CheckpointPolicy::attention_selective(),
12,
4096, // large target — won't be exceeded
);
checkpointer.record_activation(1024); // trivial amount
// Attention layer — policy says checkpoint.
assert!(checkpointer.should_checkpoint(0, "self_attn"));
// FFN layer — policy says don't checkpoint.
assert!(!checkpointer.should_checkpoint(0, "ffn_dense"));
}
#[test]
fn test_memory_aware_checkpointer_debug_impl() {
// Ensure the manual Debug impl doesn't panic.
let c = MemoryAwareCheckpointer::new(CheckpointPolicy::None, 8, 512);
let _ = format!("{c:?}");
}
}