use std::collections::HashMap;
use std::sync::Arc;
use cudarc::driver::CudaDevice;
use super::error::TieredError;
use super::lru::LruTracker;
use super::stats::TieredStats;
use crate::adaptive_tiering::AllocationPlan;
use crate::cuda_inference::tensor::GpuTensor;
use crate::cuda_inference::weight_store::{LayerWeights, RMSNormWeights};
pub struct SharedWeights {
pub embed_tokens: GpuTensor,
pub final_norm: RMSNormWeights,
pub lm_head: Option<GpuTensor>,
size_bytes: usize,
}
impl SharedWeights {
pub fn new(
embed_tokens: GpuTensor,
final_norm: RMSNormWeights,
lm_head: Option<GpuTensor>,
) -> Self {
let size_bytes = embed_tokens.size_bytes()
+ final_norm.weight.size_bytes()
+ lm_head.as_ref().map(|t| t.size_bytes()).unwrap_or(0);
Self {
embed_tokens,
final_norm,
lm_head,
size_bytes,
}
}
pub fn size_bytes(&self) -> usize {
self.size_bytes
}
}
pub struct VramCache {
device: Arc<CudaDevice>,
layers: HashMap<usize, LayerWeights>,
shared: Option<SharedWeights>,
lru: LruTracker<usize>,
layer_usage: usize,
layer_budget: u64,
layer_size: Option<usize>,
stats: Arc<TieredStats>,
}
impl VramCache {
pub fn new(device: Arc<CudaDevice>, budget: u64, stats: Arc<TieredStats>) -> Self {
Self {
device,
layers: HashMap::new(),
shared: None,
lru: LruTracker::new(),
layer_usage: 0,
layer_budget: budget,
layer_size: None,
stats,
}
}
pub fn set_shared(&mut self, shared: SharedWeights) {
let shared_size = shared.size_bytes() as u64;
self.layer_budget = self.layer_budget.saturating_sub(shared_size);
tracing::info!(
shared_mb = shared_size / (1024 * 1024),
layer_budget_mb = self.layer_budget / (1024 * 1024),
"Set shared weights, adjusted layer budget"
);
self.shared = Some(shared);
}
pub fn shared(&self) -> Option<&SharedWeights> {
self.shared.as_ref()
}
pub fn embed_tokens(&self) -> Option<&GpuTensor> {
self.shared.as_ref().map(|s| &s.embed_tokens)
}
pub fn final_norm(&self) -> Option<&RMSNormWeights> {
self.shared.as_ref().map(|s| &s.final_norm)
}
pub fn lm_head(&self) -> Option<&GpuTensor> {
self.shared.as_ref().and_then(|s| s.lm_head.as_ref())
}
pub fn contains(&self, layer_idx: usize) -> bool {
self.layers.contains_key(&layer_idx)
}
pub fn peek(&self, layer_idx: usize) -> Option<&LayerWeights> {
self.layers.get(&layer_idx)
}
pub fn get(&mut self, layer_idx: usize) -> Option<&LayerWeights> {
if self.layers.contains_key(&layer_idx) {
self.lru.touch(layer_idx);
self.stats.record_vram_hit();
self.layers.get(&layer_idx)
} else {
None
}
}
pub fn insert(
&mut self,
layer_idx: usize,
layer: LayerWeights,
priority: f32,
) -> Result<(), TieredError> {
let size = layer.size_bytes();
if self.layer_usage as u64 + size as u64 > self.layer_budget {
return Err(TieredError::vram_alloc(
format!("layer {} doesn't fit", layer_idx),
size as u64,
self.layer_budget - self.layer_usage as u64,
));
}
if let Some(old) = self.layers.remove(&layer_idx) {
self.layer_usage -= old.size_bytes();
self.lru.remove(&layer_idx);
}
self.layers.insert(layer_idx, layer);
self.lru.touch_with_priority(layer_idx, priority);
self.layer_usage += size;
if self.layer_size.is_none() {
self.layer_size = Some(size);
}
Ok(())
}
pub fn remove(&mut self, layer_idx: usize) -> Option<LayerWeights> {
if let Some(layer) = self.layers.remove(&layer_idx) {
self.layer_usage -= layer.size_bytes();
self.lru.remove(&layer_idx);
Some(layer)
} else {
None
}
}
pub fn layer_usage(&self) -> usize {
self.layer_usage
}
pub fn total_usage(&self) -> usize {
self.layer_usage + self.shared.as_ref().map(|s| s.size_bytes()).unwrap_or(0)
}
pub fn available(&self) -> u64 {
self.layer_budget.saturating_sub(self.layer_usage as u64)
}
pub fn num_layers(&self) -> usize {
self.layers.len()
}
pub fn layer_budget(&self) -> u64 {
self.layer_budget
}
pub fn has_room_for(&self, size: usize) -> bool {
self.layer_usage as u64 + size as u64 <= self.layer_budget
}
pub fn evict_for_space(
&mut self,
bytes_needed: u64,
plan: &AllocationPlan,
) -> Vec<(usize, LayerWeights)> {
let mut evicted = Vec::new();
let mut freed = 0u64;
let layer_sizes: HashMap<usize, usize> = self
.layers
.iter()
.map(|(&idx, layer)| (idx, layer.size_bytes()))
.collect();
for (&idx, _) in &self.layers {
let priority = plan.get_layer_priority(idx).unwrap_or(0.5);
self.lru.touch_with_priority(idx, priority);
}
let candidates = self.lru.eviction_candidates_for_size(bytes_needed, |idx| {
layer_sizes.get(idx).copied().unwrap_or(0) as u64
});
for layer_idx in candidates {
if freed >= bytes_needed {
break;
}
if let Some(layer) = self.layers.remove(&layer_idx) {
let size = layer.size_bytes();
self.layer_usage -= size;
self.lru.remove(&layer_idx);
self.stats.record_vram_eviction(size as u64);
tracing::debug!(
layer_idx,
freed_mb = size / (1024 * 1024),
"Evicted layer from VRAM"
);
freed += size as u64;
evicted.push((layer_idx, layer));
}
}
evicted
}
pub fn evict(&mut self, layer_idx: usize) -> Option<LayerWeights> {
if let Some(layer) = self.layers.remove(&layer_idx) {
let size = layer.size_bytes();
self.layer_usage -= size;
self.lru.remove(&layer_idx);
self.stats.record_vram_eviction(size as u64);
tracing::debug!(layer_idx, freed_mb = size / (1024 * 1024), "Evicted layer");
Some(layer)
} else {
None
}
}
pub fn clear_layers(&mut self) {
self.layers.clear();
self.lru.clear();
self.layer_usage = 0;
}
pub fn device(&self) -> &Arc<CudaDevice> {
&self.device
}
pub fn layer_indices(&self) -> impl Iterator<Item = usize> + '_ {
self.layers.keys().copied()
}
pub fn estimated_capacity(&self) -> usize {
if let Some(layer_size) = self.layer_size {
(self.available() as usize) / layer_size
} else {
0
}
}
}
impl std::fmt::Debug for VramCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VramCache")
.field("num_layers", &self.layers.len())
.field("layer_usage_mb", &(self.layer_usage / (1024 * 1024)))
.field("layer_budget_mb", &(self.layer_budget / (1024 * 1024)))
.field("has_shared", &self.shared.is_some())
.finish()
}
}
trait AllocationPlanExt {
fn get_layer_priority(&self, layer_idx: usize) -> Option<f32>;
}
impl AllocationPlanExt for AllocationPlan {
fn get_layer_priority(&self, layer_idx: usize) -> Option<f32> {
let prefix = format!("model.layers.{layer_idx}.");
self.allocations
.iter()
.find(|(name, _)| name.starts_with(&prefix))
.map(|(_, alloc)| alloc.priority)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vram_cache_capacity() {
}
}