use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use candle_core::{DType, Device, Tensor};
use crate::hct::{HctError, HctLoader};
pub trait TensorProvider: Send + Sync {
fn get(&self, name: &str, device: &Device, dtype: DType) -> Result<Tensor, HctError>;
fn contains(&self, name: &str) -> bool;
fn tensor_names(&self) -> Vec<String>;
fn clear_prefix(&self, _prefix: &str) -> (usize, u64) {
(0, 0)
}
}
#[derive(Debug)]
struct CacheEntry {
tensor: Tensor,
size_bytes: u64,
last_access: std::time::Instant,
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_memory_bytes: u64,
pub max_entries: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_memory_bytes: 60 * 1024 * 1024 * 1024, max_entries: 20000,
}
}
}
pub struct LazyVarBuilder {
provider: Arc<dyn TensorProvider>,
prefix: String,
device: Device,
dtype: DType,
cache: Arc<RwLock<LruCache>>,
cache_config: CacheConfig,
}
struct LruCache {
entries: HashMap<String, CacheEntry>,
total_bytes: u64,
max_bytes: u64,
max_entries: usize,
}
impl LruCache {
fn new(config: &CacheConfig) -> Self {
Self {
entries: HashMap::new(),
total_bytes: 0,
max_bytes: config.max_memory_bytes,
max_entries: config.max_entries,
}
}
fn get(&mut self, name: &str) -> Option<Tensor> {
if let Some(entry) = self.entries.get_mut(name) {
entry.last_access = std::time::Instant::now();
Some(entry.tensor.clone())
} else {
None
}
}
fn insert(&mut self, name: String, tensor: Tensor, size_bytes: u64) {
if self.max_bytes == 0 || self.max_entries == 0 {
return;
}
while self.total_bytes + size_bytes > self.max_bytes
|| self.entries.len() >= self.max_entries
{
if !self.evict_lru() {
break; }
}
if self.total_bytes + size_bytes > self.max_bytes || self.entries.len() >= self.max_entries
{
return;
}
self.entries.insert(
name,
CacheEntry {
tensor,
size_bytes,
last_access: std::time::Instant::now(),
},
);
self.total_bytes += size_bytes;
}
fn evict_lru(&mut self) -> bool {
if self.entries.is_empty() {
return false;
}
let oldest = self
.entries
.iter()
.min_by_key(|(_, entry)| entry.last_access)
.map(|(name, _)| name.clone());
if let Some(name) = oldest {
if let Some(entry) = self.entries.remove(&name) {
self.total_bytes = self.total_bytes.saturating_sub(entry.size_bytes);
tracing::debug!(
name = %name,
size_bytes = entry.size_bytes,
"Evicted tensor from cache"
);
return true;
}
}
false
}
fn clear_prefix(&mut self, prefix: &str) -> (usize, u64) {
let keys_to_remove: Vec<String> = self
.entries
.keys()
.filter(|k| k.starts_with(prefix))
.cloned()
.collect();
let mut evicted_count = 0;
let mut evicted_bytes = 0u64;
for key in keys_to_remove {
if let Some(entry) = self.entries.remove(&key) {
self.total_bytes = self.total_bytes.saturating_sub(entry.size_bytes);
evicted_bytes += entry.size_bytes;
evicted_count += 1;
}
}
if evicted_count > 0 {
tracing::debug!(
prefix = %prefix,
evicted_count = evicted_count,
evicted_bytes = evicted_bytes,
"Cleared tensors by prefix"
);
}
(evicted_count, evicted_bytes)
}
fn memory_used(&self) -> u64 {
self.total_bytes
}
fn len(&self) -> usize {
self.entries.len()
}
}
impl LazyVarBuilder {
pub fn new(provider: Arc<dyn TensorProvider>, device: Device, dtype: DType) -> Self {
Self::with_cache_config(provider, device, dtype, CacheConfig::default())
}
pub fn with_cache_config(
provider: Arc<dyn TensorProvider>,
device: Device,
dtype: DType,
cache_config: CacheConfig,
) -> Self {
let cache = Arc::new(RwLock::new(LruCache::new(&cache_config)));
Self {
provider,
prefix: String::new(),
device,
dtype,
cache,
cache_config,
}
}
pub fn pp(&self, prefix: impl AsRef<str>) -> Self {
let new_prefix = if self.prefix.is_empty() {
prefix.as_ref().to_string()
} else {
format!("{}.{}", self.prefix, prefix.as_ref())
};
Self {
provider: Arc::clone(&self.provider),
prefix: new_prefix,
device: self.device.clone(),
dtype: self.dtype,
cache: Arc::clone(&self.cache),
cache_config: self.cache_config.clone(),
}
}
pub fn get(&self, name: &str) -> Result<Tensor, HctError> {
let full_name = if self.prefix.is_empty() {
name.to_string()
} else {
format!("{}.{}", self.prefix, name)
};
{
let mut cache = self.cache.write().map_err(|_| HctError::Tensor {
message: "Cache lock poisoned".to_string(),
})?;
if let Some(tensor) = cache.get(&full_name) {
return Ok(tensor);
}
}
let tensor = self.provider.get(&full_name, &self.device, self.dtype)?;
let size_bytes = tensor.elem_count() as u64 * dtype_size(tensor.dtype());
{
let mut cache = self.cache.write().map_err(|_| HctError::Tensor {
message: "Cache lock poisoned".to_string(),
})?;
cache.insert(full_name, tensor.clone(), size_bytes);
}
Ok(tensor)
}
pub fn get_with_hints<S: Into<candle_core::Shape>>(
&self,
name: &str,
_hint: S,
) -> Result<Tensor, HctError> {
self.get(name)
}
pub fn contains(&self, name: &str) -> bool {
let full_name = if self.prefix.is_empty() {
name.to_string()
} else {
format!("{}.{}", self.prefix, name)
};
self.provider.contains(&full_name)
}
pub fn device(&self) -> &Device {
&self.device
}
pub fn dtype(&self) -> DType {
self.dtype
}
pub fn cache_stats(&self) -> (usize, u64) {
if let Ok(cache) = self.cache.read() {
(cache.len(), cache.memory_used())
} else {
(0, 0)
}
}
pub fn clear_prefix(&self, prefix: &str) -> (usize, u64) {
tracing::debug!(prefix = %prefix, "LazyVarBuilder: clear_prefix called");
let (local_count, local_bytes) = if let Ok(mut cache) = self.cache.write() {
cache.clear_prefix(prefix)
} else {
(0, 0)
};
tracing::debug!(
local_count = local_count,
"LazyVarBuilder: Cleared from local cache"
);
let (provider_count, provider_bytes) = self.provider.clear_prefix(prefix);
tracing::debug!(
provider_count = provider_count,
"LazyVarBuilder: Cleared from provider cache"
);
(local_count + provider_count, local_bytes + provider_bytes)
}
pub fn clear_all(&self) -> (usize, u64) {
if let Ok(mut cache) = self.cache.write() {
let count = cache.len();
let bytes = cache.memory_used();
cache.entries.clear();
cache.total_bytes = 0;
(count, bytes)
} else {
(0, 0)
}
}
}
fn dtype_size(dtype: DType) -> u64 {
match dtype {
DType::F32 | DType::U32 => 4,
DType::F64 | DType::I64 => 8,
DType::F16 | DType::BF16 => 2,
DType::U8 => 1,
_ => 4,
}
}
#[allow(dead_code)]
pub struct DirectoryTensorProvider {
directory: PathBuf,
tensor_files: HashMap<String, PathBuf>,
}
impl DirectoryTensorProvider {
pub fn new(directory: impl AsRef<Path>) -> Result<Self, HctError> {
let directory = directory.as_ref().to_path_buf();
let mut tensor_files = HashMap::new();
for entry in std::fs::read_dir(&directory).map_err(|e| HctError::Io {
path: directory.clone(),
message: e.to_string(),
})? {
let entry = entry.map_err(|e| HctError::Io {
path: directory.clone(),
message: e.to_string(),
})?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "hct") {
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
let tensor_name = crate::hct::filename_to_tensor_name(name);
tensor_files.insert(tensor_name, path);
}
}
}
tracing::info!(
count = tensor_files.len(),
directory = %directory.display(),
"Created directory tensor provider"
);
Ok(Self {
directory,
tensor_files,
})
}
pub fn tensor_count(&self) -> usize {
self.tensor_files.len()
}
}
impl TensorProvider for DirectoryTensorProvider {
fn get(&self, name: &str, device: &Device, dtype: DType) -> Result<Tensor, HctError> {
let path = self
.tensor_files
.get(name)
.ok_or_else(|| HctError::Tensor {
message: format!("Tensor not found: {}", name),
})?;
let loader = HctLoader::from_file(path)?;
loader.to_tensor(device, Some(dtype))
}
fn contains(&self, name: &str) -> bool {
self.tensor_files.contains_key(name)
}
fn tensor_names(&self) -> Vec<String> {
self.tensor_files.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::TempDir;
struct MockTensorProvider {
tensors: HashMap<String, (Vec<usize>, Vec<f32>)>,
}
impl MockTensorProvider {
fn new() -> Self {
Self {
tensors: HashMap::new(),
}
}
fn add_tensor(&mut self, name: &str, shape: Vec<usize>, data: Vec<f32>) {
self.tensors.insert(name.to_string(), (shape, data));
}
}
impl TensorProvider for MockTensorProvider {
fn get(&self, name: &str, device: &Device, _dtype: DType) -> Result<Tensor, HctError> {
let (shape, data) = self.tensors.get(name).ok_or_else(|| HctError::Tensor {
message: format!("Tensor not found: {}", name),
})?;
Tensor::from_vec(data.clone(), shape.as_slice(), device).map_err(|e| HctError::Tensor {
message: format!("Failed to create tensor: {}", e),
})
}
fn contains(&self, name: &str) -> bool {
self.tensors.contains_key(name)
}
fn tensor_names(&self) -> Vec<String> {
self.tensors.keys().cloned().collect()
}
}
#[test]
fn test_lazy_varbuilder_defers_loading() {
let mut provider = MockTensorProvider::new();
provider.add_tensor("weight", vec![4], vec![1.0, 2.0, 3.0, 4.0]);
let vb = LazyVarBuilder::new(Arc::new(provider), Device::Cpu, DType::F32);
let (cached_count, _) = vb.cache_stats();
assert_eq!(cached_count, 0, "No tensors should be cached initially");
let tensor = vb.get("weight").expect("load tensor");
assert_eq!(tensor.dims(), &[4]);
let (cached_count, _) = vb.cache_stats();
assert_eq!(cached_count, 1, "One tensor should be cached after access");
}
#[test]
fn test_lazy_varbuilder_caches_accessed_tensors() {
let mut provider = MockTensorProvider::new();
provider.add_tensor("tensor_a", vec![2], vec![1.0, 2.0]);
provider.add_tensor("tensor_b", vec![2], vec![3.0, 4.0]);
let vb = LazyVarBuilder::new(Arc::new(provider), Device::Cpu, DType::F32);
let _a = vb.get("tensor_a").expect("load tensor_a");
let (count, _) = vb.cache_stats();
assert_eq!(count, 1);
let a2 = vb.get("tensor_a").expect("load tensor_a again");
let (count, _) = vb.cache_stats();
assert_eq!(count, 1, "Should still be 1, tensor was cached");
let _b = vb.get("tensor_b").expect("load tensor_b");
let (count, _) = vb.cache_stats();
assert_eq!(count, 2, "Should be 2 after loading tensor_b");
}
#[test]
fn test_lazy_varbuilder_evicts_lru_when_full() {
let mut provider = MockTensorProvider::new();
provider.add_tensor("tensor_1", vec![4], vec![1.0, 2.0, 3.0, 4.0]);
provider.add_tensor("tensor_2", vec![4], vec![5.0, 6.0, 7.0, 8.0]);
provider.add_tensor("tensor_3", vec![4], vec![9.0, 10.0, 11.0, 12.0]);
let config = CacheConfig {
max_memory_bytes: 32,
max_entries: 2,
};
let vb =
LazyVarBuilder::with_cache_config(Arc::new(provider), Device::Cpu, DType::F32, config);
let _t1 = vb.get("tensor_1").expect("load tensor_1");
let _t2 = vb.get("tensor_2").expect("load tensor_2");
let (count, _) = vb.cache_stats();
assert_eq!(count, 2);
let _t3 = vb.get("tensor_3").expect("load tensor_3");
let (count, _) = vb.cache_stats();
assert_eq!(count, 2, "Should still be 2 after eviction");
}
#[test]
fn test_lazy_varbuilder_pp_get_works_for_model_layers() {
let mut provider = MockTensorProvider::new();
provider.add_tensor(
"model.layers.0.self_attn.q_proj.weight",
vec![4],
vec![1.0, 2.0, 3.0, 4.0],
);
provider.add_tensor(
"model.layers.0.self_attn.k_proj.weight",
vec![4],
vec![5.0, 6.0, 7.0, 8.0],
);
provider.add_tensor(
"model.layers.0.mlp.gate_proj.weight",
vec![4],
vec![9.0, 10.0, 11.0, 12.0],
);
let vb = LazyVarBuilder::new(Arc::new(provider), Device::Cpu, DType::F32);
let layer_vb = vb.pp("model").pp("layers").pp("0");
let attn_vb = layer_vb.pp("self_attn");
let q = attn_vb.get("q_proj.weight").expect("load q_proj");
let k = attn_vb.get("k_proj.weight").expect("load k_proj");
assert_eq!(q.dims(), &[4]);
assert_eq!(k.dims(), &[4]);
let mlp_vb = layer_vb.pp("mlp");
let gate = mlp_vb.get("gate_proj.weight").expect("load gate_proj");
assert_eq!(gate.dims(), &[4]);
let (count, _) = vb.cache_stats();
assert_eq!(count, 3);
}
#[test]
fn test_lazy_varbuilder_contains() {
let mut provider = MockTensorProvider::new();
provider.add_tensor("existing", vec![2], vec![1.0, 2.0]);
let vb = LazyVarBuilder::new(Arc::new(provider), Device::Cpu, DType::F32);
assert!(vb.contains("existing"));
assert!(!vb.contains("nonexistent"));
}
#[test]
fn test_lru_cache_eviction_order() {
let config = CacheConfig {
max_memory_bytes: 100,
max_entries: 3,
};
let mut cache = LruCache::new(&config);
let t1 = Tensor::zeros(&[2], DType::F32, &Device::Cpu).expect("create t1");
let t2 = Tensor::zeros(&[2], DType::F32, &Device::Cpu).expect("create t2");
let t3 = Tensor::zeros(&[2], DType::F32, &Device::Cpu).expect("create t3");
cache.insert("t1".to_string(), t1, 8);
std::thread::sleep(std::time::Duration::from_millis(10));
cache.insert("t2".to_string(), t2, 8);
std::thread::sleep(std::time::Duration::from_millis(10));
cache.insert("t3".to_string(), t3, 8);
assert_eq!(cache.len(), 3);
let _ = cache.get("t1");
let t4 = Tensor::zeros(&[2], DType::F32, &Device::Cpu).expect("create t4");
cache.insert("t4".to_string(), t4, 8);
assert_eq!(cache.len(), 3);
assert!(cache.get("t1").is_some(), "t1 should still be in cache");
assert!(cache.get("t2").is_none(), "t2 should have been evicted");
assert!(cache.get("t3").is_some(), "t3 should still be in cache");
assert!(cache.get("t4").is_some(), "t4 should be in cache");
}
}