use std::collections::HashMap;
use candle_core::{DType, Device, Module, Result as CandleResult, Tensor, D};
use candle_nn::{Embedding, Linear};
use crate::attention_cache::{
attention_with_cache, CacheType, KvCache, KvCacheConfig, StandardCache,
};
use crate::hct::HctError;
use crate::lazy_varbuilder::LazyVarBuilder;
use super::llama::LlamaConfig;
#[derive(Default)]
struct ExternalKvStore {
caches: HashMap<usize, (Tensor, Tensor)>,
}
impl ExternalKvStore {
fn new() -> Self {
Self {
caches: HashMap::new(),
}
}
fn save(&mut self, layer_idx: usize, k: Tensor, v: Tensor) -> CandleResult<()> {
let k_cpu = if k.device().is_cuda() {
k.to_device(&Device::Cpu)?
} else {
k
};
let v_cpu = if v.device().is_cuda() {
v.to_device(&Device::Cpu)?
} else {
v
};
self.caches.insert(layer_idx, (k_cpu, v_cpu));
Ok(())
}
fn restore(
&mut self,
layer_idx: usize,
device: &Device,
) -> CandleResult<Option<(Tensor, Tensor)>> {
if let Some((k_cpu, v_cpu)) = self.caches.get(&layer_idx) {
let k = k_cpu.to_device(device)?;
let v = v_cpu.to_device(device)?;
Ok(Some((k, v)))
} else {
Ok(None)
}
}
#[allow(dead_code)]
fn clear(&mut self) {
self.caches.clear();
}
fn memory_bytes(&self) -> usize {
self.caches
.values()
.map(|(k, v)| {
k.elem_count() * k.dtype().size_in_bytes()
+ v.elem_count() * v.dtype().size_in_bytes()
})
.sum()
}
fn len(&self) -> usize {
self.caches.len()
}
}
pub struct LazyLlama {
embed_tokens: Embedding,
norm: RmsNorm,
lm_head: Linear,
rotary: RotaryEmbedding,
config: LlamaConfig,
lazy_vb: LazyVarBuilder,
loaded_layers: HashMap<usize, DecoderLayer>,
lru_order: Vec<usize>,
max_loaded_layers: usize,
cache_type: CacheType,
device: Device,
dtype: DType,
layer_loads: usize,
layer_evictions: usize,
prefetch_depth: usize,
kv_store: ExternalKvStore,
}
impl LazyLlama {
pub fn load(
config: LlamaConfig,
lazy_vb: LazyVarBuilder,
max_loaded_layers: usize,
) -> Result<Self, LazyLoadError> {
let device = lazy_vb.device().clone();
let dtype = lazy_vb.dtype();
tracing::info!(
num_layers = config.num_hidden_layers,
max_loaded_layers = max_loaded_layers,
"Loading LazyLlama (layers loaded on-demand)"
);
let embed_tokens = Self::load_embedding(&config, &lazy_vb)?;
let norm = Self::load_norm(
config.hidden_size,
config.rms_norm_eps,
&lazy_vb,
"model.norm",
)?;
let lm_head = if config.tie_word_embeddings {
Linear::new(embed_tokens.embeddings().clone(), None)
} else {
Self::load_linear(
&lazy_vb,
"lm_head",
config.hidden_size,
config.vocab_size,
false,
)?
};
let rotary = RotaryEmbedding::new(&config, dtype, &device)
.map_err(|e| LazyLoadError::Candle(e.to_string()))?;
let prefetch_depth = 2.min(max_loaded_layers.saturating_sub(1));
tracing::info!(
prefetch_depth = prefetch_depth,
"LazyLlama base loaded (embedding, norm, lm_head). Layers will load on-demand."
);
Ok(Self {
embed_tokens,
norm,
lm_head,
rotary,
config,
lazy_vb,
loaded_layers: HashMap::new(),
lru_order: Vec::new(),
max_loaded_layers,
cache_type: CacheType::Standard,
device,
dtype,
layer_loads: 0,
layer_evictions: 0,
prefetch_depth,
kv_store: ExternalKvStore::new(),
})
}
pub fn set_prefetch_depth(&mut self, depth: usize) {
self.prefetch_depth = depth.min(self.max_loaded_layers.saturating_sub(1));
}
pub fn set_cache_type(&mut self, cache_type: CacheType) {
self.cache_type = cache_type;
}
pub fn forward(
&mut self,
input_ids: &Tensor,
start_pos: usize,
) -> Result<Tensor, LazyLoadError> {
let (_batch_size, seq_len) = input_ids
.dims2()
.map_err(|e| LazyLoadError::Candle(e.to_string()))?;
let mut hidden_states = self
.embed_tokens
.forward(input_ids)
.map_err(|e| LazyLoadError::Candle(e.to_string()))?;
let stored_cache_len = self
.kv_store
.caches
.get(&0)
.map(|(k, _)| k.dims().get(2).copied().unwrap_or(0))
.unwrap_or(0);
let kv_len = stored_cache_len + seq_len;
let mask = if seq_len > 1 {
Some(
Self::create_causal_mask_with_kv_len(seq_len, kv_len, &self.device, self.dtype)
.map_err(|e| LazyLoadError::Candle(e.to_string()))?,
)
} else {
None
};
for layer_idx in 0..self.config.num_hidden_layers {
self.ensure_layer_loaded(layer_idx)?;
let layer = self
.loaded_layers
.get_mut(&layer_idx)
.expect("Layer should be loaded");
hidden_states = layer
.forward(&hidden_states, &self.rotary, mask.as_ref(), start_pos)
.map_err(|e| LazyLoadError::Candle(e.to_string()))?;
self.touch_layer(layer_idx);
for prefetch_offset in 1..=self.prefetch_depth {
let prefetch_idx = layer_idx + prefetch_offset;
if prefetch_idx < self.config.num_hidden_layers {
let _ = self.ensure_layer_loaded(prefetch_idx);
}
}
}
let hidden_states = self
.norm
.forward(&hidden_states)
.map_err(|e| LazyLoadError::Candle(e.to_string()))?;
self.lm_head
.forward(&hidden_states)
.map_err(|e| LazyLoadError::Candle(e.to_string()))
}
fn ensure_layer_loaded(&mut self, layer_idx: usize) -> Result<(), LazyLoadError> {
if self.loaded_layers.contains_key(&layer_idx) {
return Ok(());
}
while self.loaded_layers.len() >= self.max_loaded_layers {
self.evict_lru_layer();
}
let max_retries = 3;
let mut total_evictions = 0;
for attempt in 0..max_retries {
if attempt > 0 {
self.force_memory_cleanup();
}
tracing::debug!(
layer = layer_idx,
attempt = attempt,
"Loading decoder layer"
);
match self.load_decoder_layer(layer_idx) {
Ok(mut layer) => {
if let Ok(Some((k, v))) = self.kv_store.restore(layer_idx, &self.device) {
layer.restore_kv(k, v);
tracing::debug!(
layer = layer_idx,
cache_seq_len = layer.self_attn.cache_len(),
"Restored KV cache from CPU store"
);
}
self.loaded_layers.insert(layer_idx, layer);
self.lru_order.push(layer_idx);
self.layer_loads += 1;
return Ok(());
},
Err(e) if Self::is_oom_error(&e) => {
let (cache_count, cache_bytes) = self.lazy_vb.cache_stats();
tracing::warn!(
layer = layer_idx,
attempt = attempt,
loaded_layers = self.loaded_layers.len(),
cache_tensors = cache_count,
cache_mb = cache_bytes / (1024 * 1024),
"OOM during layer load, evicting more layers and clearing tensor cache"
);
let evict_count = (self.loaded_layers.len() / 2).max(1);
for _ in 0..evict_count {
if self.loaded_layers.is_empty() {
break;
}
self.evict_lru_layer();
total_evictions += 1;
}
for layer in self.loaded_layers.values_mut() {
layer.clear_cache();
}
if self.loaded_layers.is_empty() {
let (cleared_count, cleared_bytes) = self.lazy_vb.clear_all();
tracing::warn!(
cleared_tensors = cleared_count,
cleared_mb = cleared_bytes / (1024 * 1024),
"Cleared entire tensor cache due to OOM"
);
}
if self.loaded_layers.is_empty() && attempt >= max_retries - 1 {
return Err(LazyLoadError::OutOfMemory {
layer: layer_idx,
attempts: attempt + 1,
evictions: total_evictions,
});
}
},
Err(e) => {
return Err(e);
},
}
}
Err(LazyLoadError::OutOfMemory {
layer: layer_idx,
attempts: max_retries,
evictions: total_evictions,
})
}
fn is_oom_error(e: &LazyLoadError) -> bool {
let msg = e.to_string().to_uppercase();
msg.contains("OUT_OF_MEMORY")
|| msg.contains("OOM")
|| msg.contains("CUDA_ERROR_OUT_OF_MEMORY")
|| msg.contains("INSUFFICIENT MEMORY")
|| msg.contains("ALLOC")
}
fn force_memory_cleanup(&mut self) {
for layer in self.loaded_layers.values_mut() {
layer.clear_cache();
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
fn evict_lru_layer(&mut self) {
if let Some(layer_idx) = self.lru_order.first().copied() {
if let Some(layer) = self.loaded_layers.get(&layer_idx) {
if let Ok(Some((k, v))) = layer.extract_kv() {
if let Err(e) = self.kv_store.save(layer_idx, k, v) {
tracing::warn!(
layer = layer_idx,
error = %e,
"Failed to save KV cache before eviction"
);
} else {
tracing::debug!(
layer = layer_idx,
kv_store_layers = self.kv_store.len(),
kv_store_mb = self.kv_store.memory_bytes() / (1024 * 1024),
"Saved KV cache to CPU before eviction"
);
}
}
}
tracing::debug!(
layer = layer_idx,
"Evicting decoder layer (GPU freed, KV cache preserved on CPU)"
);
self.loaded_layers.remove(&layer_idx);
self.lru_order.remove(0);
self.layer_evictions += 1;
}
}
fn touch_layer(&mut self, layer_idx: usize) {
if let Some(pos) = self.lru_order.iter().position(|&x| x == layer_idx) {
self.lru_order.remove(pos);
self.lru_order.push(layer_idx);
}
}
fn load_decoder_layer(&self, layer_idx: usize) -> Result<DecoderLayer, LazyLoadError> {
let prefix = format!("model.layers.{}", layer_idx);
let vb = self.lazy_vb.pp(&prefix);
DecoderLayer::load(&self.config, vb, self.cache_type.clone())
}
pub fn clear_cache(&mut self) {
for layer in self.loaded_layers.values_mut() {
layer.clear_cache();
}
}
pub fn warmup(&mut self) -> usize {
let layers_to_load = self.max_loaded_layers.min(self.config.num_hidden_layers);
tracing::info!(
layers = layers_to_load,
"Warming up LazyLlama by prefetching initial layers"
);
let mut loaded = 0;
for layer_idx in 0..layers_to_load {
match self.ensure_layer_loaded(layer_idx) {
Ok(()) => loaded += 1,
Err(e) => {
tracing::warn!(layer = layer_idx, error = %e, "Warmup prefetch failed");
break;
},
}
}
tracing::info!(layers_loaded = loaded, "Warmup complete");
loaded
}
pub fn prefetch_layers(&mut self, start_layer: usize, count: usize) -> usize {
let end_layer = (start_layer + count).min(self.config.num_hidden_layers);
let mut loaded = 0;
for layer_idx in start_layer..end_layer {
if let Ok(()) = self.ensure_layer_loaded(layer_idx) {
loaded += 1;
}
}
loaded
}
pub fn stats(&self) -> LazyStats {
LazyStats {
total_layers: self.config.num_hidden_layers,
loaded_layers: self.loaded_layers.len(),
max_loaded_layers: self.max_loaded_layers,
layer_loads: self.layer_loads,
layer_evictions: self.layer_evictions,
prefetch_depth: self.prefetch_depth,
}
}
pub fn cache_seq_len(&self) -> usize {
self.loaded_layers
.values()
.next()
.map_or(0, |l| l.self_attn.cache_len())
}
pub fn cache_memory_bytes(&self) -> usize {
self.loaded_layers
.values()
.map(|l| l.self_attn.cache_memory_bytes())
.sum()
}
fn load_embedding(
config: &LlamaConfig,
vb: &LazyVarBuilder,
) -> Result<Embedding, LazyLoadError> {
let weight = vb.get("model.embed_tokens.weight")?;
Ok(Embedding::new(weight, config.hidden_size))
}
fn load_norm(
_size: usize,
eps: f64,
vb: &LazyVarBuilder,
prefix: &str,
) -> Result<RmsNorm, LazyLoadError> {
let weight = vb.get(&format!("{}.weight", prefix))?;
Ok(RmsNorm { weight, eps })
}
fn load_linear(
vb: &LazyVarBuilder,
name: &str,
_in_dim: usize,
_out_dim: usize,
bias: bool,
) -> Result<Linear, LazyLoadError> {
let weight = vb.get(&format!("{}.weight", name))?;
let bias_tensor = if bias {
Some(vb.get(&format!("{}.bias", name))?)
} else {
None
};
Ok(Linear::new(weight, bias_tensor))
}
#[allow(dead_code)]
fn create_causal_mask(
seq_len: usize,
start_pos: usize,
device: &Device,
dtype: DType,
) -> CandleResult<Tensor> {
Self::create_causal_mask_with_kv_len(seq_len, seq_len + start_pos, device, dtype)
}
fn create_causal_mask_with_kv_len(
seq_len: usize,
kv_len: usize,
device: &Device,
dtype: DType,
) -> CandleResult<Tensor> {
let cache_len = kv_len.saturating_sub(seq_len);
let mask: Vec<f32> = (0..seq_len)
.flat_map(|i| {
(0..kv_len).map(move |j| {
if j > cache_len + i {
f32::NEG_INFINITY
} else {
0.0
}
})
})
.collect();
Tensor::from_vec(mask, (seq_len, kv_len), device)?.to_dtype(dtype)
}
}
#[derive(Debug, Clone)]
pub struct LazyStats {
pub total_layers: usize,
pub loaded_layers: usize,
pub max_loaded_layers: usize,
pub layer_loads: usize,
pub layer_evictions: usize,
pub prefetch_depth: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum LazyLoadError {
#[error("HCT loading error: {0}")]
Hct(#[from] HctError),
#[error("Candle error: {0}")]
Candle(String),
#[error("Layer not found: {0}")]
LayerNotFound(usize),
#[error(
"Out of memory: layer {layer} failed after {attempts} retries with {evictions} evictions"
)]
OutOfMemory {
layer: usize,
attempts: usize,
evictions: usize,
},
}
impl From<candle_core::Error> for LazyLoadError {
fn from(e: candle_core::Error) -> Self {
LazyLoadError::Candle(e.to_string())
}
}
struct RmsNorm {
weight: Tensor,
eps: f64,
}
impl Module for RmsNorm {
fn forward(&self, x: &Tensor) -> CandleResult<Tensor> {
let dtype = x.dtype();
let x = x.to_dtype(DType::F32)?;
let variance = x.sqr()?.mean_keepdim(D::Minus1)?;
let x_normed = x.broadcast_div(&(variance + self.eps)?.sqrt()?)?;
x_normed.to_dtype(dtype)?.broadcast_mul(&self.weight)
}
}
struct RotaryEmbedding {
cos: Tensor,
sin: Tensor,
}
impl RotaryEmbedding {
fn new(config: &LlamaConfig, dtype: DType, device: &Device) -> CandleResult<Self> {
let head_dim = config.head_dim();
let max_seq_len = config.max_position_embeddings;
let theta = config.rope_theta;
let inv_freq: Vec<f32> = (0..head_dim)
.step_by(2)
.map(|i| 1.0 / theta.powf(i as f64 / head_dim as f64) as f32)
.collect();
let inv_freq = if let Some(ref scaling) = config.rope_scaling {
if scaling.rope_type.as_deref() == Some("llama3") {
Self::apply_llama3_scaling(&inv_freq, scaling)
} else if let Some(factor) = scaling.factor {
inv_freq.iter().map(|f| f / factor as f32).collect()
} else {
inv_freq
}
} else {
inv_freq
};
let inv_freq = Tensor::new(inv_freq.as_slice(), device)?;
let positions: Vec<f32> = (0..max_seq_len).map(|p| p as f32).collect();
let positions = Tensor::new(positions.as_slice(), device)?.unsqueeze(1)?;
let freqs = positions.matmul(&inv_freq.unsqueeze(0)?)?;
let emb = Tensor::cat(&[&freqs, &freqs], D::Minus1)?;
let cos = emb.cos()?.to_dtype(dtype)?;
let sin = emb.sin()?.to_dtype(dtype)?;
Ok(Self { cos, sin })
}
fn apply_llama3_scaling(
inv_freq: &[f32],
scaling: &super::llama::RopeScalingConfig,
) -> Vec<f32> {
let factor = scaling.factor.unwrap_or(1.0) as f32;
let low_freq_factor = scaling.low_freq_factor.unwrap_or(1.0) as f32;
let high_freq_factor = scaling.high_freq_factor.unwrap_or(4.0) as f32;
let orig_max_pos = scaling.original_max_position_embeddings.unwrap_or(8192) as f32;
let low_freq_wavelen = orig_max_pos / low_freq_factor;
let high_freq_wavelen = orig_max_pos / high_freq_factor;
inv_freq
.iter()
.map(|&freq| {
let wavelen = 2.0 * std::f32::consts::PI / freq;
if wavelen > low_freq_wavelen {
freq / factor
} else if wavelen < high_freq_wavelen {
freq
} else {
let smooth = (orig_max_pos / wavelen - low_freq_factor)
/ (high_freq_factor - low_freq_factor);
(1.0 - smooth) * freq / factor + smooth * freq
}
})
.collect()
}
fn apply(&self, q: &Tensor, k: &Tensor, start_pos: usize) -> CandleResult<(Tensor, Tensor)> {
let seq_len = q.dim(2)?;
let cos = self.cos.narrow(0, start_pos, seq_len)?;
let sin = self.sin.narrow(0, start_pos, seq_len)?;
let q_embed = Self::rotate(q, &cos, &sin)?;
let k_embed = Self::rotate(k, &cos, &sin)?;
Ok((q_embed, k_embed))
}
fn rotate(x: &Tensor, cos: &Tensor, sin: &Tensor) -> CandleResult<Tensor> {
let x1 = x.narrow(D::Minus1, 0, x.dim(D::Minus1)? / 2)?;
let x2 = x.narrow(D::Minus1, x.dim(D::Minus1)? / 2, x.dim(D::Minus1)? / 2)?;
let rotate_x = Tensor::cat(&[&x2.neg()?, &x1], D::Minus1)?;
let cos = cos.unsqueeze(0)?.unsqueeze(0)?;
let sin = sin.unsqueeze(0)?.unsqueeze(0)?;
x.broadcast_mul(&cos)? + rotate_x.broadcast_mul(&sin)?
}
}
struct Mlp {
gate_proj: Linear,
up_proj: Linear,
down_proj: Linear,
}
impl Mlp {
fn load(
config: &LlamaConfig,
vb: &LazyVarBuilder,
prefix: &str,
) -> Result<Self, LazyLoadError> {
let gate_proj = LazyLlama::load_linear(
vb,
&format!("{}.gate_proj", prefix),
config.hidden_size,
config.intermediate_size,
false,
)?;
let up_proj = LazyLlama::load_linear(
vb,
&format!("{}.up_proj", prefix),
config.hidden_size,
config.intermediate_size,
false,
)?;
let down_proj = LazyLlama::load_linear(
vb,
&format!("{}.down_proj", prefix),
config.intermediate_size,
config.hidden_size,
false,
)?;
Ok(Self {
gate_proj,
up_proj,
down_proj,
})
}
}
impl Module for Mlp {
fn forward(&self, x: &Tensor) -> CandleResult<Tensor> {
let gate = self.gate_proj.forward(x)?;
let gate = candle_nn::ops::silu(&gate)?;
let up = self.up_proj.forward(x)?;
let x = (gate * up)?;
self.down_proj.forward(&x)
}
}
struct Attention {
q_proj: Linear,
k_proj: Linear,
v_proj: Linear,
o_proj: Linear,
num_heads: usize,
num_kv_heads: usize,
head_dim: usize,
cache: Box<dyn KvCache>,
}
impl Attention {
fn load(
config: &LlamaConfig,
vb: &LazyVarBuilder,
prefix: &str,
cache_type: CacheType,
) -> Result<Self, LazyLoadError> {
let num_heads = config.num_attention_heads;
let num_kv_heads = config.num_kv_heads();
let head_dim = config.head_dim();
let q_proj = LazyLlama::load_linear(
vb,
&format!("{}.q_proj", prefix),
config.hidden_size,
num_heads * head_dim,
false,
)?;
let k_proj = LazyLlama::load_linear(
vb,
&format!("{}.k_proj", prefix),
config.hidden_size,
num_kv_heads * head_dim,
false,
)?;
let v_proj = LazyLlama::load_linear(
vb,
&format!("{}.v_proj", prefix),
config.hidden_size,
num_kv_heads * head_dim,
false,
)?;
let o_proj = LazyLlama::load_linear(
vb,
&format!("{}.o_proj", prefix),
num_heads * head_dim,
config.hidden_size,
false,
)?;
let cache_config = KvCacheConfig {
num_kv_heads,
head_dim,
dtype: vb.dtype(),
device: vb.device().clone(),
};
let cache = cache_type
.create(&cache_config)
.map_err(|e| LazyLoadError::Candle(e.to_string()))?;
Ok(Self {
q_proj,
k_proj,
v_proj,
o_proj,
num_heads,
num_kv_heads,
head_dim,
cache,
})
}
fn forward(
&mut self,
x: &Tensor,
rotary: &RotaryEmbedding,
mask: Option<&Tensor>,
start_pos: usize,
) -> CandleResult<Tensor> {
let (batch_size, seq_len, _) = x.dims3()?;
let q = self.q_proj.forward(x)?;
let k = self.k_proj.forward(x)?;
let v = self.v_proj.forward(x)?;
let q = q
.reshape((batch_size, seq_len, self.num_heads, self.head_dim))?
.transpose(1, 2)?;
let k = k
.reshape((batch_size, seq_len, self.num_kv_heads, self.head_dim))?
.transpose(1, 2)?;
let v = v
.reshape((batch_size, seq_len, self.num_kv_heads, self.head_dim))?
.transpose(1, 2)?;
let (q, k) = rotary.apply(&q, &k, start_pos)?;
let attn_output = attention_with_cache(
&q,
&k,
&v,
self.cache.as_mut(),
self.num_heads,
self.num_kv_heads,
mask,
)?;
let attn_output = attn_output.transpose(1, 2)?.reshape((
batch_size,
seq_len,
self.num_heads * self.head_dim,
))?;
self.o_proj.forward(&attn_output)
}
fn clear_cache(&mut self) {
self.cache.clear();
}
fn cache_len(&self) -> usize {
self.cache.seq_len()
}
fn cache_memory_bytes(&self) -> usize {
self.cache.memory_bytes()
}
fn extract_kv(&self) -> CandleResult<Option<(Tensor, Tensor)>> {
self.cache.get_kv()
}
fn restore_kv(&mut self, k: Tensor, v: Tensor) {
self.cache = Box::new(StandardCache::with_kv(k, v));
}
}
struct DecoderLayer {
self_attn: Attention,
mlp: Mlp,
input_layernorm: RmsNorm,
post_attention_layernorm: RmsNorm,
}
impl DecoderLayer {
fn load(
config: &LlamaConfig,
vb: LazyVarBuilder,
cache_type: CacheType,
) -> Result<Self, LazyLoadError> {
let self_attn = Attention::load(config, &vb, "self_attn", cache_type)?;
let mlp = Mlp::load(config, &vb, "mlp")?;
let input_layernorm = LazyLlama::load_norm(
config.hidden_size,
config.rms_norm_eps,
&vb,
"input_layernorm",
)?;
let post_attention_layernorm = LazyLlama::load_norm(
config.hidden_size,
config.rms_norm_eps,
&vb,
"post_attention_layernorm",
)?;
Ok(Self {
self_attn,
mlp,
input_layernorm,
post_attention_layernorm,
})
}
fn forward(
&mut self,
x: &Tensor,
rotary: &RotaryEmbedding,
mask: Option<&Tensor>,
start_pos: usize,
) -> CandleResult<Tensor> {
let residual = x.clone();
let x = self.input_layernorm.forward(x)?;
let x = self.self_attn.forward(&x, rotary, mask, start_pos)?;
let x = (residual + x)?;
let residual = x.clone();
let x = self.post_attention_layernorm.forward(&x)?;
let x = self.mlp.forward(&x)?;
residual + x
}
fn clear_cache(&mut self) {
self.self_attn.clear_cache();
}
fn extract_kv(&self) -> CandleResult<Option<(Tensor, Tensor)>> {
self.self_attn.extract_kv()
}
fn restore_kv(&mut self, k: Tensor, v: Tensor) {
self.self_attn.restore_kv(k, v);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
struct MockTensorProvider {
tensors: std::sync::RwLock<HashMap<String, Tensor>>,
}
impl MockTensorProvider {
fn new() -> Self {
Self {
tensors: std::sync::RwLock::new(HashMap::new()),
}
}
fn add(&self, name: &str, shape: &[usize], device: &Device) {
let data: Vec<f32> = (0..shape.iter().product::<usize>())
.map(|i| (i as f32) * 0.001)
.collect();
let tensor = Tensor::from_vec(data, shape, device).unwrap();
self.tensors
.write()
.unwrap()
.insert(name.to_string(), tensor);
}
}
impl crate::lazy_varbuilder::TensorProvider for MockTensorProvider {
fn get(&self, name: &str, _device: &Device, _dtype: DType) -> Result<Tensor, HctError> {
self.tensors
.read()
.unwrap()
.get(name)
.cloned()
.ok_or_else(|| HctError::Tensor {
message: format!("Tensor not found: {}", name),
})
}
fn contains(&self, name: &str) -> bool {
self.tensors.read().unwrap().contains_key(name)
}
fn tensor_names(&self) -> Vec<String> {
self.tensors.read().unwrap().keys().cloned().collect()
}
}
#[test]
fn test_lazy_stats() {
let stats = LazyStats {
total_layers: 126,
loaded_layers: 12,
max_loaded_layers: 12,
layer_loads: 50,
layer_evictions: 38,
prefetch_depth: 2,
};
assert_eq!(stats.total_layers, 126);
assert_eq!(stats.loaded_layers, 12);
}
}