use std::collections::HashSet;
use std::sync::Arc;
use cudarc::driver::CudaDevice;
use super::config::{LoadingStrategy, TieredConfig};
use super::error::TieredError;
use super::nvme_cache::NvmeCache;
use super::ram_cache::{CpuLayerWeights, RamCache};
use super::stats::TieredStats;
use super::vram_cache::{SharedWeights, VramCache};
use crate::adaptive_tiering::AllocationPlan;
use crate::cuda_inference::weight_store::LayerWeights;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayerState {
Vram,
Ram,
Nvme,
Unloaded,
}
pub struct TieredWeightStore {
device: Arc<CudaDevice>,
vram: VramCache,
ram: RamCache,
nvme: Option<NvmeCache>,
plan: AllocationPlan,
config: TieredConfig,
stats: Arc<TieredStats>,
num_layers: usize,
layer_size: Option<usize>,
strategy: LoadingStrategy,
prefetching: HashSet<usize>,
}
impl TieredWeightStore {
pub fn new(
device: Arc<CudaDevice>,
plan: AllocationPlan,
config: TieredConfig,
num_layers: usize,
) -> Result<Self, TieredError> {
let stats = Arc::new(TieredStats::new());
let strategy = config.select_strategy(&plan);
let vram_budget = config.hardware.vram_budget;
let ram_budget = config.hardware.ram_budget;
tracing::info!(
?strategy,
num_layers,
vram_budget_mb = vram_budget / (1024 * 1024),
ram_budget_mb = ram_budget / (1024 * 1024),
"Creating TieredWeightStore"
);
let vram = VramCache::new(device.clone(), vram_budget, stats.clone());
let ram = RamCache::new(ram_budget, config.hardware.use_pinned_memory, stats.clone());
let nvme = if config.progressive.enable_caching && strategy.needs_nvme() {
let nvme_budget = if config.progressive.max_cache_size > 0 {
config.progressive.max_cache_size
} else {
100 * 1024 * 1024 * 1024 };
Some(NvmeCache::new(
&config.progressive.cache_dir,
nvme_budget,
stats.clone(),
)?)
} else {
None
};
Ok(Self {
device,
vram,
ram,
nvme,
plan,
config,
stats,
num_layers,
layer_size: None,
strategy,
prefetching: HashSet::new(),
})
}
pub fn set_shared(&mut self, shared: SharedWeights) {
self.vram.set_shared(shared);
}
pub fn shared(&self) -> Option<&SharedWeights> {
self.vram.shared()
}
pub fn layer_state(&self, layer_idx: usize) -> LayerState {
if self.vram.contains(layer_idx) {
LayerState::Vram
} else if self.ram.contains(layer_idx) {
LayerState::Ram
} else if self
.nvme
.as_ref()
.map(|n| n.contains(layer_idx))
.unwrap_or(false)
{
LayerState::Nvme
} else {
LayerState::Unloaded
}
}
pub fn get_layer(&mut self, layer_idx: usize) -> Result<&LayerWeights, TieredError> {
if self.vram.contains(layer_idx) {
return self
.vram
.get(layer_idx)
.ok_or_else(|| TieredError::LayerNotFound(layer_idx, self.num_layers));
}
if self.ram.contains(layer_idx) {
self.promote_from_ram(layer_idx)?;
return self
.vram
.get(layer_idx)
.ok_or_else(|| TieredError::LayerNotFound(layer_idx, self.num_layers));
}
if let Some(ref nvme) = self.nvme {
if nvme.contains(layer_idx) {
self.promote_from_nvme(layer_idx)?;
return self
.vram
.get(layer_idx)
.ok_or_else(|| TieredError::LayerNotFound(layer_idx, self.num_layers));
}
}
Err(TieredError::LayerNotFound(layer_idx, self.num_layers))
}
pub fn is_vram_resident(&self, layer_idx: usize) -> bool {
self.vram.contains(layer_idx)
}
fn promote_from_ram(&mut self, layer_idx: usize) -> Result<(), TieredError> {
let cpu_weights = self
.ram
.remove(layer_idx)
.ok_or_else(|| TieredError::LayerNotFound(layer_idx, self.num_layers))?;
let size = cpu_weights.size_bytes;
if !self.vram.has_room_for(size) {
self.evict_vram_for_space(size as u64)?;
}
let gpu_weights = self.upload_layer(&cpu_weights)?;
let priority = self.plan.get_layer_priority(layer_idx).unwrap_or(0.5);
self.vram.insert(layer_idx, gpu_weights, priority)?;
self.stats.record_layer_loaded();
tracing::debug!(
layer_idx,
size_mb = size / (1024 * 1024),
"Promoted layer RAM→VRAM"
);
Ok(())
}
fn promote_from_nvme(&mut self, layer_idx: usize) -> Result<(), TieredError> {
let nvme = self
.nvme
.as_ref()
.ok_or_else(|| TieredError::nvme("NVMe cache not configured"))?;
let data = nvme.read_layer(layer_idx)?;
let entry = nvme.get_entry(layer_idx).unwrap();
let cpu_weights = if entry.compressed {
self.decompress_layer(layer_idx, &data)?
} else {
self.deserialize_layer(layer_idx, &data)?
};
let size = cpu_weights.size_bytes;
if !self.vram.has_room_for(size) {
self.evict_vram_for_space(size as u64)?;
}
let gpu_weights = self.upload_layer(&cpu_weights)?;
let priority = self.plan.get_layer_priority(layer_idx).unwrap_or(0.5);
self.vram.insert(layer_idx, gpu_weights, priority)?;
self.stats.record_layer_loaded();
tracing::debug!(
layer_idx,
size_mb = size / (1024 * 1024),
"Promoted layer NVMe→VRAM"
);
Ok(())
}
fn evict_vram_for_space(&mut self, bytes_needed: u64) -> Result<(), TieredError> {
let evicted = self.vram.evict_for_space(bytes_needed, &self.plan);
for (layer_idx, layer) in evicted {
if self.ram.has_room_for(layer.size_bytes()) {
let cpu_weights = self.download_layer(&layer)?;
let priority = self.plan.get_layer_priority(layer_idx).unwrap_or(0.5);
self.ram.insert(layer_idx, cpu_weights, priority)?;
tracing::debug!(layer_idx, "Demoted layer VRAM→RAM");
} else if self.nvme.is_some() {
let data = self.serialize_layer(&layer)?;
if let Some(ref mut nvme) = self.nvme {
nvme.write_layer(layer_idx, &data, false)?;
}
tracing::debug!(layer_idx, "Demoted layer VRAM→NVMe");
}
}
Ok(())
}
fn upload_layer(&self, cpu_weights: &CpuLayerWeights) -> Result<LayerWeights, TieredError> {
let start = std::time::Instant::now();
let layer = self.create_gpu_layer_from_cpu(cpu_weights)?;
let elapsed_ns = start.elapsed().as_nanos() as u64;
self.stats
.record_vram_upload(cpu_weights.size_bytes as u64, elapsed_ns);
Ok(layer)
}
fn download_layer(&self, _layer: &LayerWeights) -> Result<CpuLayerWeights, TieredError> {
todo!("download_layer not yet implemented")
}
fn create_gpu_layer_from_cpu(
&self,
_cpu: &CpuLayerWeights,
) -> Result<LayerWeights, TieredError> {
todo!("create_gpu_layer_from_cpu not yet implemented")
}
fn serialize_layer(&self, _layer: &LayerWeights) -> Result<Vec<u8>, TieredError> {
todo!("serialize_layer not yet implemented")
}
fn deserialize_layer(
&self,
_layer_idx: usize,
_data: &[u8],
) -> Result<CpuLayerWeights, TieredError> {
todo!("deserialize_layer not yet implemented")
}
fn decompress_layer(
&self,
layer_idx: usize,
_data: &[u8],
) -> Result<CpuLayerWeights, TieredError> {
Err(TieredError::decompress(
format!("layer_{}", layer_idx),
"HCT decompression not yet implemented",
))
}
pub fn prefetch(&mut self, current_layer: usize, lookahead: usize) {
if self.config.progressive.prefetch_depth == 0 {
return;
}
for offset in 1..=lookahead {
let target = current_layer + offset;
if target >= self.num_layers {
break;
}
if self.vram.contains(target) || self.prefetching.contains(&target) {
continue;
}
let state = self.layer_state(target);
if state == LayerState::Unloaded {
continue;
}
self.prefetching.insert(target);
self.stats.record_prefetch_request();
tracing::trace!(target, "Prefetching layer");
}
}
pub fn stats(&self) -> &TieredStats {
&self.stats
}
pub fn vram_cache(&self) -> &VramCache {
&self.vram
}
pub fn ram_cache(&self) -> &RamCache {
&self.ram
}
pub fn nvme_cache(&self) -> Option<&NvmeCache> {
self.nvme.as_ref()
}
pub fn vram_cache_mut(&mut self) -> &mut VramCache {
&mut self.vram
}
pub fn ram_cache_mut(&mut self) -> &mut RamCache {
&mut self.ram
}
pub fn nvme_cache_mut(&mut self) -> Option<&mut NvmeCache> {
self.nvme.as_mut()
}
pub fn strategy(&self) -> &LoadingStrategy {
&self.strategy
}
pub fn num_layers(&self) -> usize {
self.num_layers
}
pub fn device(&self) -> &Arc<CudaDevice> {
&self.device
}
pub fn plan(&self) -> &AllocationPlan {
&self.plan
}
pub fn tier_summary(&self) -> TierSummary {
let mut vram_count = 0;
let mut ram_count = 0;
let mut nvme_count = 0;
let mut unloaded_count = 0;
for i in 0..self.num_layers {
match self.layer_state(i) {
LayerState::Vram => vram_count += 1,
LayerState::Ram => ram_count += 1,
LayerState::Nvme => nvme_count += 1,
LayerState::Unloaded => unloaded_count += 1,
}
}
TierSummary {
vram_layers: vram_count,
ram_layers: ram_count,
nvme_layers: nvme_count,
unloaded_layers: unloaded_count,
vram_usage: self.vram.total_usage(),
ram_usage: self.ram.usage(),
nvme_usage: self.nvme.as_ref().map(|n| n.usage()).unwrap_or(0),
}
}
}
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)
}
}
#[derive(Debug, Clone)]
pub struct TierSummary {
pub vram_layers: usize,
pub ram_layers: usize,
pub nvme_layers: usize,
pub unloaded_layers: usize,
pub vram_usage: usize,
pub ram_usage: usize,
pub nvme_usage: u64,
}
impl std::fmt::Display for TierSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Tiers: VRAM={} ({:.1}GB), RAM={} ({:.1}GB), NVMe={} ({:.1}GB), Unloaded={}",
self.vram_layers,
self.vram_usage as f64 / (1024.0 * 1024.0 * 1024.0),
self.ram_layers,
self.ram_usage as f64 / (1024.0 * 1024.0 * 1024.0),
self.nvme_layers,
self.nvme_usage as f64 / (1024.0 * 1024.0 * 1024.0),
self.unloaded_layers,
)
}
}
impl std::fmt::Debug for TieredWeightStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TieredWeightStore")
.field("num_layers", &self.num_layers)
.field("strategy", &self.strategy)
.field("vram", &self.vram)
.field("ram", &self.ram)
.field("nvme", &self.nvme)
.finish()
}
}