use crate::Method;
#[derive(Debug, Clone)]
pub struct MemoryRequirement {
pub model_bytes: u64,
pub adapter_bytes: u64,
pub optimizer_bytes: u64,
pub activation_bytes: u64,
pub total_bytes: u64,
pub savings_percent: f64,
}
impl MemoryRequirement {
pub fn to_human_readable(&self) -> String {
format!(
"Memory Requirement:\n Model: {:.1} GB\n Adapter: {:.1} GB\n Optimizer: {:.1} GB\n Activations: {:.1} GB\n Total: {:.1} GB\n Savings: {:.0}%",
self.model_bytes as f64 / 1e9,
self.adapter_bytes as f64 / 1e9,
self.optimizer_bytes as f64 / 1e9,
self.activation_bytes as f64 / 1e9,
self.total_bytes as f64 / 1e9,
self.savings_percent
)
}
}
#[derive(Debug)]
pub struct MemoryPlanner {
model_params: u64,
hidden_dim: u64,
num_layers: u32,
batch_size: u32,
seq_len: u32,
}
impl MemoryPlanner {
pub fn new(model_params: u64) -> Self {
let (hidden_dim, num_layers) = estimate_architecture(model_params);
Self { model_params, hidden_dim, num_layers, batch_size: 32, seq_len: 512 }
}
pub fn with_batch_size(mut self, batch_size: u32) -> Self {
self.batch_size = batch_size;
self
}
pub fn with_seq_len(mut self, seq_len: u32) -> Self {
self.seq_len = seq_len;
self
}
pub fn estimate_full(&self) -> MemoryRequirement {
let model_bytes = self.model_params * 2; let optimizer_bytes = self.model_params * 8; let activation_bytes = self.estimate_activations();
let total_bytes = model_bytes + optimizer_bytes + activation_bytes;
MemoryRequirement {
model_bytes,
adapter_bytes: 0,
optimizer_bytes,
activation_bytes,
total_bytes,
savings_percent: 0.0,
}
}
pub fn estimate_lora(&self, rank: u32) -> MemoryRequirement {
let model_bytes = self.model_params * 2;
let adapter_params =
(self.hidden_dim * u64::from(rank) * 2) * 4 * u64::from(self.num_layers);
let adapter_bytes = adapter_params * 2;
let optimizer_bytes = adapter_params * 8;
let activation_bytes = self.estimate_activations();
let total_bytes = model_bytes + adapter_bytes + optimizer_bytes + activation_bytes;
let full_total = self.estimate_full().total_bytes;
let savings_percent = (1.0 - total_bytes as f64 / full_total as f64) * 100.0;
MemoryRequirement {
model_bytes,
adapter_bytes,
optimizer_bytes,
activation_bytes,
total_bytes,
savings_percent,
}
}
pub fn estimate_qlora(&self, rank: u32, bits: u8) -> MemoryRequirement {
let model_bytes = self.model_params * u64::from(bits) / 8;
let adapter_params =
(self.hidden_dim * u64::from(rank) * 2) * 4 * u64::from(self.num_layers);
let adapter_bytes = adapter_params * 2;
let optimizer_bytes = adapter_params * 8;
let activation_bytes = self.estimate_activations();
let total_bytes = model_bytes + adapter_bytes + optimizer_bytes + activation_bytes;
let full_total = self.estimate_full().total_bytes;
let savings_percent = (1.0 - total_bytes as f64 / full_total as f64) * 100.0;
MemoryRequirement {
model_bytes,
adapter_bytes,
optimizer_bytes,
activation_bytes,
total_bytes,
savings_percent,
}
}
pub fn estimate(&self, method: Method, rank: u32) -> MemoryRequirement {
match method {
Method::Full => self.estimate_full(),
Method::LoRA => self.estimate_lora(rank),
Method::QLoRA => self.estimate_qlora(rank, 4),
Method::Auto => {
self.estimate_qlora(rank, 4)
}
}
}
fn estimate_activations(&self) -> u64 {
let per_layer =
u64::from(self.batch_size) * u64::from(self.seq_len) * self.hidden_dim * 2 * 2;
per_layer * u64::from(self.num_layers)
}
}
fn estimate_architecture(params: u64) -> (u64, u32) {
if params > 60_000_000_000 {
(8192, 80) } else if params > 10_000_000_000 {
(5120, 40) } else if params > 5_000_000_000 {
(4096, 32) } else if params > 1_000_000_000 {
(2048, 22) } else if params > 300_000_000 {
(1024, 12) } else {
(768, 12) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_planner_7b() {
let planner = MemoryPlanner::new(7_000_000_000);
let full = planner.estimate_full();
let lora = planner.estimate_lora(64);
let qlora = planner.estimate_qlora(64, 4);
assert!(full.total_bytes > lora.total_bytes);
assert!(lora.total_bytes > qlora.total_bytes);
assert!(qlora.savings_percent > 50.0);
}
#[test]
fn test_lora_adapter_memory_scales_with_rank() {
let planner = MemoryPlanner::new(7_000_000_000);
let lora_16 = planner.estimate_lora(16);
let lora_64 = planner.estimate_lora(64);
let lora_128 = planner.estimate_lora(128);
assert!(lora_16.adapter_bytes < lora_64.adapter_bytes);
assert!(lora_64.adapter_bytes < lora_128.adapter_bytes);
}
#[test]
fn test_qlora_4bit_vs_8bit() {
let planner = MemoryPlanner::new(7_000_000_000);
let qlora_4 = planner.estimate_qlora(64, 4);
let qlora_8 = planner.estimate_qlora(64, 8);
assert!(qlora_4.model_bytes < qlora_8.model_bytes);
}
#[test]
fn test_batch_size_affects_activations() {
let planner_small = MemoryPlanner::new(7_000_000_000).with_batch_size(8);
let planner_large = MemoryPlanner::new(7_000_000_000).with_batch_size(64);
let small = planner_small.estimate_full();
let large = planner_large.estimate_full();
assert!(small.activation_bytes < large.activation_bytes);
}
#[test]
fn test_architecture_estimation() {
let (hidden, layers) = estimate_architecture(7_000_000_000);
assert_eq!(hidden, 4096);
assert_eq!(layers, 32);
let (hidden, layers) = estimate_architecture(350_000_000);
assert_eq!(hidden, 1024);
assert_eq!(layers, 12);
}
#[test]
fn test_architecture_estimation_all_tiers() {
let (hidden, layers) = estimate_architecture(70_000_000_000);
assert_eq!(hidden, 8192);
assert_eq!(layers, 80);
let (hidden, layers) = estimate_architecture(13_000_000_000);
assert_eq!(hidden, 5120);
assert_eq!(layers, 40);
let (hidden, layers) = estimate_architecture(2_000_000_000);
assert_eq!(hidden, 2048);
assert_eq!(layers, 22);
let (hidden, layers) = estimate_architecture(100_000_000);
assert_eq!(hidden, 768);
assert_eq!(layers, 12);
}
#[test]
fn test_with_seq_len() {
let planner = MemoryPlanner::new(7_000_000_000).with_seq_len(1024);
let full_1024 = planner.estimate_full();
let planner_short = MemoryPlanner::new(7_000_000_000).with_seq_len(256);
let full_256 = planner_short.estimate_full();
assert!(full_1024.activation_bytes > full_256.activation_bytes);
}
#[test]
fn test_estimate_method_dispatch() {
let planner = MemoryPlanner::new(7_000_000_000);
let full = planner.estimate(Method::Full, 64);
assert_eq!(full.adapter_bytes, 0);
let lora = planner.estimate(Method::LoRA, 64);
assert!(lora.adapter_bytes > 0);
let qlora = planner.estimate(Method::QLoRA, 64);
assert!(qlora.model_bytes < lora.model_bytes);
let auto = planner.estimate(Method::Auto, 64);
assert!(auto.savings_percent > 0.0);
}
#[test]
fn test_to_human_readable() {
let planner = MemoryPlanner::new(7_000_000_000);
let req = planner.estimate_full();
let readable = req.to_human_readable();
assert!(readable.contains("Memory Requirement"));
assert!(readable.contains("GB"));
assert!(readable.contains("Model:"));
assert!(readable.contains("Total:"));
}
#[test]
fn test_full_has_zero_savings() {
let planner = MemoryPlanner::new(7_000_000_000);
let full = planner.estimate_full();
assert_eq!(full.savings_percent, 0.0);
}
#[test]
fn test_lora_has_positive_savings() {
let planner = MemoryPlanner::new(7_000_000_000);
let lora = planner.estimate_lora(64);
assert!(lora.savings_percent > 0.0);
}
#[test]
fn test_qlora_saves_more_than_lora() {
let planner = MemoryPlanner::new(7_000_000_000);
let lora = planner.estimate_lora(64);
let qlora = planner.estimate_qlora(64, 4);
assert!(qlora.savings_percent > lora.savings_percent);
}
}