Skip to main content

ferrum_kv/managers/
paged.rs

1//! Paged KV Cache Manager
2//!
3//! This module implements PagedAttention-style KV cache management with:
4//!
5//! - Non-contiguous physical memory allocation
6//! - Logical to physical block mapping via block tables
7//! - Copy-on-write support for prefix sharing
8//! - GPU<->CPU block swapping for memory management
9//! - Efficient block reclamation and reuse
10//! - Prefix caching for shared prompt optimization
11
12use crate::blocks::{BlockPool, BlockStorageConfig, PhysicalBlockId};
13use crate::cache::prefix::{PrefixCache, PrefixCacheStats, PrefixId};
14use async_trait::async_trait;
15use ferrum_interfaces::{
16    kv_cache::{AllocationRequest, BlockTable, CacheGcStats, CacheManagerStats, MemoryPressure},
17    KvCacheHandle, KvCacheManager, TensorRef,
18};
19use ferrum_types::{DataType, Device, FerrumError, RequestId, Result};
20use parking_lot::{Mutex, RwLock};
21use std::collections::HashMap;
22use std::sync::atomic::{AtomicU64, Ordering};
23use std::sync::Arc;
24use std::time::Instant;
25use tracing::{debug, info};
26
27/// Configuration for paged KV cache manager
28#[derive(Debug, Clone)]
29pub struct PagedKvCacheConfig {
30    /// Block size in tokens
31    pub block_size: usize,
32    /// Maximum number of GPU blocks
33    pub max_gpu_blocks: usize,
34    /// Maximum number of CPU blocks (for swapping)
35    pub max_cpu_blocks: usize,
36    /// Enable copy-on-write for prefix sharing
37    pub enable_cow: bool,
38    /// Enable block swapping
39    pub enable_swapping: bool,
40    /// Watermark for low memory pressure (fraction of blocks free)
41    pub low_watermark: f32,
42    /// Watermark for high memory pressure
43    pub high_watermark: f32,
44    /// Number of layers in the model
45    pub num_layers: usize,
46    /// Number of attention heads
47    pub num_heads: usize,
48    /// Head dimension
49    pub head_dim: usize,
50    /// Enable prefix caching
51    pub enable_prefix_cache: bool,
52    /// Maximum number of prefixes to cache
53    pub max_prefixes: usize,
54    /// Minimum prefix length to cache
55    pub min_prefix_length: usize,
56}
57
58impl Default for PagedKvCacheConfig {
59    fn default() -> Self {
60        Self {
61            block_size: 16,
62            max_gpu_blocks: 1024,
63            max_cpu_blocks: 512,
64            enable_cow: true,
65            enable_swapping: true,
66            low_watermark: 0.3,
67            high_watermark: 0.1,
68            num_layers: 32,
69            num_heads: 32,
70            head_dim: 128,
71            enable_prefix_cache: true,
72            max_prefixes: 100,
73            min_prefix_length: 16,
74        }
75    }
76}
77
78/// Paged KV cache handle for a single sequence
79#[derive(Debug)]
80pub struct PagedKvCacheHandle {
81    /// Request ID
82    request_id: RequestId,
83    /// Device where blocks are allocated
84    device: Device,
85    /// Block table (logical to physical mapping)
86    block_table: RwLock<BlockTable>,
87    /// Number of tokens stored
88    num_tokens: RwLock<usize>,
89    /// Number of layers
90    num_layers: usize,
91    /// Number of heads
92    num_heads: usize,
93    /// Head dimension
94    head_dim: usize,
95    /// Block size
96    block_size: usize,
97    /// Last access time
98    last_access: RwLock<Instant>,
99    /// Whether this handle has copy-on-write references
100    has_cow_refs: RwLock<bool>,
101    /// Reference count (for COW)
102    ref_count: AtomicU64,
103}
104
105impl PagedKvCacheHandle {
106    /// Create new paged KV cache handle
107    pub fn new(
108        request_id: RequestId,
109        device: Device,
110        block_size: usize,
111        num_layers: usize,
112        num_heads: usize,
113        head_dim: usize,
114    ) -> Self {
115        Self {
116            request_id,
117            device,
118            block_table: RwLock::new(BlockTable::new(block_size)),
119            num_tokens: RwLock::new(0),
120            num_layers,
121            num_heads,
122            head_dim,
123            block_size,
124            last_access: RwLock::new(Instant::now()),
125            has_cow_refs: RwLock::new(false),
126            ref_count: AtomicU64::new(1),
127        }
128    }
129
130    /// Add a physical block to this handle
131    pub fn add_block(&self, logical_id: u32, physical_id: u32) {
132        let mut table = self.block_table.write();
133        if logical_id as usize >= table.logical_to_physical.len() {
134            table
135                .logical_to_physical
136                .resize((logical_id + 1) as usize, 0);
137        }
138        table.logical_to_physical[logical_id as usize] = physical_id;
139
140        if physical_id as usize >= table.physical_blocks.len() {
141            table.physical_blocks.resize((physical_id + 1) as usize, 0);
142        }
143        table.physical_blocks[physical_id as usize] = 1;
144
145        *self.last_access.write() = Instant::now();
146    }
147
148    fn truncate_blocks(&self, logical_len: usize) {
149        let mut table = self.block_table.write();
150        table.logical_to_physical.truncate(logical_len);
151        table.physical_blocks.clear();
152
153        let used_blocks: Vec<u32> = table
154            .logical_to_physical
155            .iter()
156            .filter(|&&id| id > 0)
157            .copied()
158            .collect();
159        for physical_id in used_blocks {
160            if physical_id as usize >= table.physical_blocks.len() {
161                table.physical_blocks.resize((physical_id + 1) as usize, 0);
162            }
163            table.physical_blocks[physical_id as usize] = 1;
164        }
165
166        *self.last_access.write() = Instant::now();
167    }
168
169    /// Get physical block for logical block
170    pub fn get_physical_block(&self, logical_id: u32) -> Option<u32> {
171        let table = self.block_table.read();
172        if (logical_id as usize) < table.logical_to_physical.len() {
173            let physical = table.logical_to_physical[logical_id as usize];
174            if physical > 0 {
175                Some(physical)
176            } else {
177                None
178            }
179        } else {
180            None
181        }
182    }
183
184    /// Get all physical block IDs
185    pub fn get_physical_blocks(&self) -> Vec<u32> {
186        let table = self.block_table.read();
187        table
188            .logical_to_physical
189            .iter()
190            .filter(|&&id| id > 0)
191            .copied()
192            .collect()
193    }
194
195    /// Get number of blocks allocated
196    pub fn num_blocks(&self) -> usize {
197        let table = self.block_table.read();
198        table
199            .logical_to_physical
200            .iter()
201            .filter(|&&id| id > 0)
202            .count()
203    }
204
205    /// Update token count
206    pub fn set_num_tokens(&self, tokens: usize) {
207        *self.num_tokens.write() = tokens;
208        let mut table = self.block_table.write();
209        table.sequence_length = tokens;
210    }
211
212    /// Get required number of blocks for token count
213    pub fn required_blocks(&self, num_tokens: usize) -> usize {
214        num_tokens.div_ceil(self.block_size)
215    }
216
217    /// Increment reference count (for COW)
218    pub fn add_ref(&self) {
219        self.ref_count.fetch_add(1, Ordering::Relaxed);
220        *self.has_cow_refs.write() = true;
221    }
222
223    /// Decrement reference count
224    pub fn remove_ref(&self) -> u64 {
225        self.ref_count.fetch_sub(1, Ordering::Relaxed)
226    }
227
228    /// Get current reference count
229    pub fn ref_count(&self) -> u64 {
230        self.ref_count.load(Ordering::Relaxed)
231    }
232
233    /// Check if this is a COW reference
234    pub fn is_cow(&self) -> bool {
235        *self.has_cow_refs.read()
236    }
237}
238
239impl KvCacheHandle for PagedKvCacheHandle {
240    fn block_table(&self) -> &BlockTable {
241        // This is a bit tricky - we need to return a reference to the block table
242        // but we have it behind a RwLock. For now, we'll use an unsafe pattern.
243        // In production, this should be redesigned.
244        unsafe {
245            let ptr = self.block_table.data_ptr();
246            &*ptr
247        }
248    }
249
250    fn block_table_mut(&mut self) -> &mut BlockTable {
251        self.block_table.get_mut()
252    }
253
254    fn as_any(&self) -> &dyn std::any::Any {
255        self
256    }
257
258    fn device(&self) -> Device {
259        self.device.clone()
260    }
261
262    fn num_tokens(&self) -> usize {
263        *self.num_tokens.read()
264    }
265
266    fn num_layers(&self) -> usize {
267        self.num_layers
268    }
269
270    fn num_heads(&self) -> usize {
271        self.num_heads
272    }
273
274    fn head_dim(&self) -> usize {
275        self.head_dim
276    }
277
278    fn key_cache(&self, _layer: usize) -> Result<Option<TensorRef>> {
279        // PagedAttention stores KV cache in physical blocks, not as tensors
280        // The actual tensor access is done through the block pool
281        Ok(None)
282    }
283
284    fn value_cache(&self, _layer: usize) -> Result<Option<TensorRef>> {
285        Ok(None)
286    }
287
288    fn clone_handle(&self) -> Result<Arc<dyn KvCacheHandle>> {
289        // For COW, we increment ref count instead of copying
290        self.add_ref();
291        Ok(Arc::new(PagedKvCacheHandle {
292            request_id: self.request_id.clone(),
293            device: self.device.clone(),
294            block_table: RwLock::new(self.block_table.read().clone()),
295            num_tokens: RwLock::new(*self.num_tokens.read()),
296            num_layers: self.num_layers,
297            num_heads: self.num_heads,
298            head_dim: self.head_dim,
299            block_size: self.block_size,
300            last_access: RwLock::new(Instant::now()),
301            has_cow_refs: RwLock::new(true),
302            ref_count: AtomicU64::new(1),
303        }))
304    }
305
306    fn stats(&self) -> ferrum_interfaces::kv_cache::CacheHandleStats {
307        let tokens = *self.num_tokens.read();
308        let blocks = self.num_blocks();
309        let bytes_per_token = 2 * self.num_layers * self.num_heads * self.head_dim * 2; // K+V, FP16
310
311        ferrum_interfaces::kv_cache::CacheHandleStats {
312            memory_bytes: blocks * self.block_size * bytes_per_token,
313            blocks_allocated: blocks,
314            tokens_stored: tokens,
315            utilization: if blocks > 0 {
316                tokens as f32 / (blocks * self.block_size) as f32
317            } else {
318                0.0
319            },
320            last_access: *self.last_access.read(),
321        }
322    }
323
324    fn is_valid(&self) -> bool {
325        self.ref_count() > 0
326    }
327
328    fn cache_id(&self) -> String {
329        format!("paged-{}", self.request_id)
330    }
331}
332
333/// Paged KV cache manager
334pub struct PagedKvCacheManager {
335    /// Configuration
336    config: PagedKvCacheConfig,
337    /// GPU block pool
338    gpu_pool: BlockPool,
339    /// CPU block pool (for swapping)
340    cpu_pool: Option<BlockPool>,
341    /// Active handles
342    active_handles: RwLock<HashMap<RequestId, Arc<PagedKvCacheHandle>>>,
343    /// Block to request mapping (for eviction)
344    block_to_request: RwLock<HashMap<PhysicalBlockId, RequestId>>,
345    /// Swapped out blocks (GPU block ID -> CPU block ID)
346    swapped_blocks: RwLock<HashMap<PhysicalBlockId, PhysicalBlockId>>,
347    /// Prefix cache for shared prompts
348    prefix_cache: Option<PrefixCache>,
349    /// Statistics
350    stats: Mutex<CacheManagerStats>,
351    /// Pressure callback
352    #[allow(clippy::type_complexity)]
353    pressure_callback: Mutex<Option<Box<dyn Fn(MemoryPressure) + Send + Sync>>>,
354}
355
356impl PagedKvCacheManager {
357    /// Create new paged KV cache manager
358    pub fn new(device: Device, config: PagedKvCacheConfig) -> Result<Self> {
359        info!(
360            "Creating paged KV cache manager: device={:?}, block_size={}, max_gpu_blocks={}, max_cpu_blocks={}, prefix_cache={}",
361            device, config.block_size, config.max_gpu_blocks, config.max_cpu_blocks, config.enable_prefix_cache
362        );
363
364        let storage_config = BlockStorageConfig {
365            num_layers: config.num_layers,
366            num_kv_heads: config.num_heads,
367            head_dim: config.head_dim,
368            block_size: config.block_size,
369        };
370
371        let gpu_pool = BlockPool::new_with_storage(
372            device.clone(),
373            config.block_size,
374            DataType::FP16,
375            config.max_gpu_blocks,
376            storage_config,
377        )?;
378
379        let cpu_pool = if config.enable_swapping {
380            Some(BlockPool::new_with_storage(
381                Device::CPU,
382                config.block_size,
383                DataType::FP16,
384                config.max_cpu_blocks,
385                storage_config,
386            )?)
387        } else {
388            None
389        };
390
391        let prefix_cache = if config.enable_prefix_cache {
392            Some(PrefixCache::new(
393                config.max_prefixes,
394                config.min_prefix_length,
395            ))
396        } else {
397            None
398        };
399
400        Ok(Self {
401            config,
402            gpu_pool,
403            cpu_pool,
404            active_handles: RwLock::new(HashMap::new()),
405            block_to_request: RwLock::new(HashMap::new()),
406            swapped_blocks: RwLock::new(HashMap::new()),
407            prefix_cache,
408            stats: Mutex::new(CacheManagerStats {
409                total_memory_bytes: 0,
410                used_memory_bytes: 0,
411                active_caches: 0,
412                total_blocks: 0,
413                free_blocks: 0,
414                cache_hit_rate: 0.0,
415                eviction_count: 0,
416                allocation_count: 0,
417                allocation_failures: 0,
418            }),
419            pressure_callback: Mutex::new(None),
420        })
421    }
422
423    /// Create with default config
424    pub fn with_defaults(device: Device, block_size: usize, max_blocks: usize) -> Result<Self> {
425        let config = PagedKvCacheConfig {
426            block_size,
427            max_gpu_blocks: max_blocks,
428            max_cpu_blocks: max_blocks / 2,
429            ..Default::default()
430        };
431        Self::new(device, config)
432    }
433
434    /// Allocate blocks for a sequence
435    pub fn allocate_blocks(
436        &self,
437        handle: &PagedKvCacheHandle,
438        num_blocks: usize,
439    ) -> Result<Vec<PhysicalBlockId>> {
440        let mut allocated = Vec::with_capacity(num_blocks);
441        let current_blocks = handle.num_blocks();
442
443        for i in 0..num_blocks {
444            let allocation = match self.gpu_pool.allocate() {
445                Ok(allocation) => allocation,
446                Err(error) => {
447                    for block_id in allocated.iter().copied() {
448                        let _ = self.gpu_pool.deallocate(block_id);
449                        self.block_to_request.write().remove(&block_id);
450                    }
451                    handle.truncate_blocks(current_blocks);
452                    return Err(error);
453                }
454            };
455            let physical_id = allocation.physical_id;
456
457            // Map logical to physical
458            let logical_id = (current_blocks + i) as u32;
459            handle.add_block(logical_id, physical_id.0);
460
461            // Track block ownership
462            self.block_to_request
463                .write()
464                .insert(physical_id, handle.request_id.clone());
465
466            allocated.push(physical_id);
467        }
468
469        // Update stats
470        {
471            let mut stats = self.stats.lock();
472            stats.allocation_count += num_blocks as u64;
473        }
474
475        debug!(
476            "Allocated {} blocks for request {}: {:?}",
477            num_blocks, handle.request_id, allocated
478        );
479
480        Ok(allocated)
481    }
482
483    /// Free blocks for a sequence
484    pub fn free_blocks(&self, block_ids: &[PhysicalBlockId]) -> Result<()> {
485        for &block_id in block_ids {
486            self.gpu_pool.deallocate(block_id)?;
487            self.block_to_request.write().remove(&block_id);
488        }
489
490        debug!("Freed {} blocks", block_ids.len());
491        Ok(())
492    }
493
494    /// Write one token's K/V vectors for a given layer and absolute token position.
495    ///
496    /// The position is translated through the handle's block table to find the
497    /// physical block and slot within that block.
498    pub fn write_kv(
499        &self,
500        handle: &PagedKvCacheHandle,
501        layer: usize,
502        token_position: usize,
503        key: &[f32],
504        value: &[f32],
505    ) -> Result<()> {
506        let block_size = self.config.block_size;
507        let logical_block = token_position / block_size;
508        let slot = token_position % block_size;
509
510        let physical_id = handle
511            .get_physical_block(logical_block as u32)
512            .ok_or_else(|| {
513                FerrumError::internal(format!(
514                    "No physical block for logical block {} (token {})",
515                    logical_block, token_position
516                ))
517            })?;
518
519        self.gpu_pool
520            .write_kv_slot(PhysicalBlockId::new(physical_id), layer, slot, key, value)
521    }
522
523    /// Read K/V vectors for a range of token positions in one layer.
524    ///
525    /// Gathers data across potentially non-contiguous physical blocks using
526    /// the handle's block table. Returns `(keys, values)` each of length
527    /// `num_tokens * num_kv_heads * head_dim`, with tokens in order.
528    pub fn read_kv(
529        &self,
530        handle: &PagedKvCacheHandle,
531        layer: usize,
532        start_token: usize,
533        num_tokens: usize,
534    ) -> Result<(Vec<f32>, Vec<f32>)> {
535        let block_size = self.config.block_size;
536        let kv_size = self.config.num_heads * self.config.head_dim;
537        let mut keys = Vec::with_capacity(num_tokens * kv_size);
538        let mut values = Vec::with_capacity(num_tokens * kv_size);
539
540        for pos in start_token..start_token + num_tokens {
541            let logical_block = pos / block_size;
542            let slot = pos % block_size;
543
544            let physical_id = handle
545                .get_physical_block(logical_block as u32)
546                .ok_or_else(|| {
547                    FerrumError::internal(format!(
548                        "No physical block for logical block {} (token {})",
549                        logical_block, pos
550                    ))
551                })?;
552
553            let (k, v) =
554                self.gpu_pool
555                    .read_kv_slot(PhysicalBlockId::new(physical_id), layer, slot)?;
556            keys.extend_from_slice(&k);
557            values.extend_from_slice(&v);
558        }
559
560        Ok((keys, values))
561    }
562
563    /// Get a reference to the GPU block pool.
564    pub fn gpu_pool(&self) -> &BlockPool {
565        &self.gpu_pool
566    }
567
568    /// Get a reference to the prefix cache (if enabled).
569    pub fn prefix_cache(&self) -> Option<&PrefixCache> {
570        self.prefix_cache.as_ref()
571    }
572
573    /// Share the first `num_prefix_blocks` physical blocks from `source` into
574    /// `target`.  The shared blocks get an incremented ref count so they
575    /// survive when the source handle is deallocated.
576    ///
577    /// After this call the target handle's block table maps logical blocks
578    /// `0..num_prefix_blocks` to the same physical blocks as the source.
579    pub fn share_prefix_blocks(
580        &self,
581        source: &PagedKvCacheHandle,
582        target: &PagedKvCacheHandle,
583        num_prefix_blocks: usize,
584    ) -> Result<()> {
585        let source_blocks = source.get_physical_blocks();
586        let n = num_prefix_blocks.min(source_blocks.len());
587
588        for i in 0..n {
589            let phys_id = source_blocks[i];
590            // Map in target
591            target.add_block(i as u32, phys_id);
592            // Increment ref count on the physical block so it isn't freed
593            // when the source handle is deallocated.
594            let pid = PhysicalBlockId::new(phys_id);
595            if let Some(block) = self.gpu_pool.get_block(pid) {
596                block.write().add_ref();
597            }
598        }
599
600        debug!(
601            "Shared {} prefix blocks from {} to {}",
602            n, source.request_id, target.request_id
603        );
604
605        Ok(())
606    }
607
608    /// Swap out blocks to CPU
609    pub fn swap_out(&self, block_ids: &[PhysicalBlockId]) -> Result<Vec<PhysicalBlockId>> {
610        let cpu_pool = self
611            .cpu_pool
612            .as_ref()
613            .ok_or_else(|| FerrumError::unsupported("Swapping not enabled"))?;
614
615        let mut swapped = Vec::with_capacity(block_ids.len());
616        let mut swap_map = self.swapped_blocks.write();
617
618        for &gpu_block in block_ids {
619            // Allocate CPU block
620            let cpu_allocation = cpu_pool.allocate()?;
621            let cpu_block = cpu_allocation.physical_id;
622
623            // TODO: Actually copy data from GPU to CPU
624            // This requires tensor memory access which is backend-specific
625
626            swap_map.insert(gpu_block, cpu_block);
627            swapped.push(cpu_block);
628
629            // Free GPU block
630            self.gpu_pool.deallocate(gpu_block)?;
631        }
632
633        debug!("Swapped out {} blocks to CPU", swapped.len());
634        Ok(swapped)
635    }
636
637    /// Swap in blocks from CPU
638    pub fn swap_in(&self, cpu_block_ids: &[PhysicalBlockId]) -> Result<Vec<PhysicalBlockId>> {
639        let cpu_pool = self
640            .cpu_pool
641            .as_ref()
642            .ok_or_else(|| FerrumError::unsupported("Swapping not enabled"))?;
643
644        let mut swapped = Vec::with_capacity(cpu_block_ids.len());
645        let mut swap_map = self.swapped_blocks.write();
646
647        for &cpu_block in cpu_block_ids {
648            // Allocate GPU block
649            let gpu_allocation = self.gpu_pool.allocate()?;
650            let gpu_block = gpu_allocation.physical_id;
651
652            // TODO: Actually copy data from CPU to GPU
653
654            // Find and remove the mapping
655            let gpu_original = swap_map
656                .iter()
657                .find(|(_, &cpu)| cpu == cpu_block)
658                .map(|(&gpu, _)| gpu);
659
660            if let Some(orig_gpu) = gpu_original {
661                swap_map.remove(&orig_gpu);
662            }
663
664            swapped.push(gpu_block);
665
666            // Free CPU block
667            cpu_pool.deallocate(cpu_block)?;
668        }
669
670        debug!("Swapped in {} blocks from CPU", swapped.len());
671        Ok(swapped)
672    }
673
674    /// Check memory pressure
675    pub fn check_pressure(&self) -> MemoryPressure {
676        let gpu_stats = self.gpu_pool.stats();
677        let free_ratio = gpu_stats.free_blocks as f32 / gpu_stats.max_blocks.max(1) as f32;
678
679        if free_ratio < self.config.high_watermark {
680            MemoryPressure::Critical
681        } else if free_ratio < self.config.low_watermark {
682            MemoryPressure::High
683        } else {
684            MemoryPressure::Low
685        }
686    }
687
688    /// Trigger pressure callback if registered
689    fn notify_pressure(&self, pressure: MemoryPressure) {
690        if let Some(ref callback) = *self.pressure_callback.lock() {
691            callback(pressure);
692        }
693    }
694
695    /// Get free block count
696    pub fn free_block_count(&self) -> usize {
697        self.gpu_pool.stats().free_blocks
698    }
699
700    /// Get total block count
701    pub fn total_blocks(&self) -> usize {
702        self.gpu_pool.stats().total_blocks
703    }
704
705    /// Copy-on-write: copy blocks when a shared reference is modified
706    pub fn cow_copy(&self, handle: &PagedKvCacheHandle, block_ids: &[u32]) -> Result<Vec<u32>> {
707        if !self.config.enable_cow {
708            return Err(FerrumError::unsupported("COW not enabled"));
709        }
710
711        let mut new_blocks = Vec::with_capacity(block_ids.len());
712
713        for &_old_physical in block_ids {
714            // Allocate new block
715            let allocation = self.gpu_pool.allocate()?;
716            let new_physical = allocation.physical_id;
717
718            // TODO: Copy data from old block to new block
719            // This requires tensor memory access
720
721            new_blocks.push(new_physical.0);
722
723            // Update block ownership
724            self.block_to_request
725                .write()
726                .insert(new_physical, handle.request_id.clone());
727        }
728
729        debug!("COW copied {} blocks", new_blocks.len());
730        Ok(new_blocks)
731    }
732
733    // ==========================================================================
734    // Prefix Caching Methods
735    // ==========================================================================
736
737    /// Find a cached prefix that matches the given tokens
738    /// Returns (prefix_id, kv_handle, last_logits, matched_length) if found
739    pub fn find_prefix(
740        &self,
741        tokens: &[ferrum_types::TokenId],
742    ) -> Option<(
743        PrefixId,
744        Arc<dyn ferrum_interfaces::KvCacheHandle + Send + Sync>,
745        Vec<f32>,
746        usize,
747    )> {
748        let prefix_cache = self.prefix_cache.as_ref()?;
749
750        if let Some((prefix_id, kv_handle, last_logits)) = prefix_cache.find_prefix(tokens) {
751            let matched_len = prefix_id.len();
752            debug!("Prefix cache hit: matched {} tokens", matched_len);
753
754            // Update hit rate stats
755            {
756                let mut stats = self.stats.lock();
757                let total = stats.allocation_count as f32;
758                if total > 0.0 {
759                    stats.cache_hit_rate = (stats.cache_hit_rate * (total - 1.0) + 1.0) / total;
760                }
761            }
762
763            Some((prefix_id, kv_handle, last_logits, matched_len))
764        } else {
765            None
766        }
767    }
768
769    /// Store a prefix in the cache for future reuse
770    pub fn store_prefix(
771        &self,
772        tokens: &[ferrum_types::TokenId],
773        kv_handle: Arc<dyn ferrum_interfaces::KvCacheHandle + Send + Sync>,
774        last_logits: Vec<f32>,
775    ) -> Result<()> {
776        if let Some(prefix_cache) = &self.prefix_cache {
777            prefix_cache.store_prefix(tokens, kv_handle, last_logits)?;
778            debug!("Stored prefix with {} tokens in cache", tokens.len());
779        }
780        Ok(())
781    }
782
783    /// Get prefix cache statistics
784    pub fn prefix_cache_stats(&self) -> Option<PrefixCacheStats> {
785        self.prefix_cache.as_ref().map(|pc| pc.stats())
786    }
787
788    /// Evict oldest prefixes from cache
789    pub fn evict_prefixes(&self, count: usize) -> usize {
790        if let Some(prefix_cache) = &self.prefix_cache {
791            let evicted = prefix_cache.evict_n(count);
792            if evicted > 0 {
793                debug!("Evicted {} prefixes from cache", evicted);
794            }
795            evicted
796        } else {
797            0
798        }
799    }
800
801    /// Clear all cached prefixes
802    pub fn clear_prefix_cache(&self) {
803        if let Some(prefix_cache) = &self.prefix_cache {
804            prefix_cache.clear();
805            debug!("Cleared prefix cache");
806        }
807    }
808}
809
810#[async_trait]
811impl KvCacheManager for PagedKvCacheManager {
812    async fn allocate(&self, request: &AllocationRequest) -> Result<Arc<dyn KvCacheHandle>> {
813        debug!(
814            "Allocating paged KV cache for request: {:?}",
815            request.request_id
816        );
817
818        // Check pressure before allocation
819        let pressure = self.check_pressure();
820        if matches!(pressure, MemoryPressure::Critical) {
821            self.notify_pressure(pressure);
822            // Try to evict some blocks
823            let _ = self.gc().await;
824        }
825
826        // Create handle
827        let handle = Arc::new(PagedKvCacheHandle::new(
828            request.request_id.clone(),
829            request.device.clone(),
830            self.config.block_size,
831            request.num_layers,
832            request.num_heads,
833            request.head_dim,
834        ));
835
836        // Allocate initial blocks
837        let initial_blocks = handle.required_blocks(request.initial_tokens);
838        if initial_blocks > 0 {
839            self.allocate_blocks(&handle, initial_blocks)?;
840        }
841
842        handle.set_num_tokens(request.initial_tokens);
843
844        // Store handle
845        self.active_handles
846            .write()
847            .insert(request.request_id.clone(), handle.clone());
848
849        // Update stats
850        {
851            let mut stats = self.stats.lock();
852            stats.active_caches += 1;
853            stats.allocation_count += 1;
854        }
855
856        Ok(handle)
857    }
858
859    async fn extend(&self, handle: &mut dyn KvCacheHandle, additional_tokens: usize) -> Result<()> {
860        let paged_handle = handle
861            .as_any()
862            .downcast_ref::<PagedKvCacheHandle>()
863            .ok_or_else(|| FerrumError::internal("Invalid handle type"))?;
864
865        let current_tokens = paged_handle.num_tokens();
866        let new_tokens = current_tokens + additional_tokens;
867        let current_blocks = paged_handle.num_blocks();
868        let required_blocks = paged_handle.required_blocks(new_tokens);
869
870        if required_blocks > current_blocks {
871            let new_blocks = required_blocks - current_blocks;
872
873            // Check if this is a COW reference that needs copying
874            if paged_handle.is_cow() && paged_handle.ref_count() > 1 {
875                // Need to copy existing blocks first
876                let existing = paged_handle.get_physical_blocks();
877                let _new_physical = self.cow_copy(paged_handle, &existing)?;
878                // Update the handle's block table with new physical IDs
879                // (In a real implementation, this would update the mappings)
880            }
881
882            self.allocate_blocks(paged_handle, new_blocks)?;
883        }
884
885        paged_handle.set_num_tokens(new_tokens);
886
887        debug!(
888            "Extended KV cache for {}: {} -> {} tokens",
889            paged_handle.request_id, current_tokens, new_tokens
890        );
891
892        Ok(())
893    }
894
895    async fn deallocate(&self, request_id: RequestId) -> Result<()> {
896        debug!("Deallocating paged KV cache for request: {:?}", request_id);
897
898        let handle = self.active_handles.write().remove(&request_id);
899
900        if let Some(handle) = handle {
901            // Check reference count
902            if handle.ref_count() > 1 {
903                // Don't free blocks, just decrement ref count
904                handle.remove_ref();
905                debug!(
906                    "Decremented ref count for {}, remaining: {}",
907                    request_id,
908                    handle.ref_count()
909                );
910                return Ok(());
911            }
912
913            // Free all blocks
914            let block_ids: Vec<PhysicalBlockId> = handle
915                .get_physical_blocks()
916                .into_iter()
917                .map(PhysicalBlockId)
918                .collect();
919
920            for block_id in block_ids {
921                let _ = self.gpu_pool.deallocate(block_id);
922                self.block_to_request.write().remove(&block_id);
923            }
924
925            // Update stats
926            {
927                let mut stats = self.stats.lock();
928                if stats.active_caches > 0 {
929                    stats.active_caches -= 1;
930                }
931            }
932        }
933
934        Ok(())
935    }
936
937    fn can_allocate(&self, request: &AllocationRequest) -> bool {
938        let required_blocks = request.initial_tokens.div_ceil(self.config.block_size);
939        let gpu_stats = self.gpu_pool.stats();
940
941        gpu_stats.free_blocks >= required_blocks
942            || gpu_stats.total_blocks + required_blocks <= gpu_stats.max_blocks
943    }
944
945    fn stats(&self) -> CacheManagerStats {
946        let gpu_stats = self.gpu_pool.stats();
947        let mut stats = self.stats.lock().clone();
948
949        stats.total_blocks = gpu_stats.max_blocks;
950        stats.free_blocks = gpu_stats.free_blocks;
951
952        // Calculate memory usage (rough estimate)
953        let bytes_per_block = self.config.block_size
954            * 2 // K + V
955            * self.config.num_layers
956            * self.config.num_heads
957            * self.config.head_dim
958            * 2; // FP16
959
960        stats.total_memory_bytes = gpu_stats.max_blocks * bytes_per_block;
961        stats.used_memory_bytes = gpu_stats.allocated_blocks * bytes_per_block;
962
963        stats
964    }
965
966    async fn gc(&self) -> Result<CacheGcStats> {
967        let start = Instant::now();
968
969        // Evict unused blocks
970        let evicted = self.gpu_pool.evict_blocks(10)?;
971
972        // Update stats
973        {
974            let mut stats = self.stats.lock();
975            stats.eviction_count += evicted.len() as u64;
976        }
977
978        Ok(CacheGcStats {
979            memory_freed: evicted.len() * self.config.block_size * 1024, // Rough estimate
980            caches_freed: 0,
981            gc_time_ms: start.elapsed().as_millis() as u64,
982        })
983    }
984
985    fn set_pressure_callback(&self, callback: Box<dyn Fn(MemoryPressure) + Send + Sync>) {
986        *self.pressure_callback.lock() = Some(callback);
987    }
988
989    fn get_handle(&self, request_id: RequestId) -> Option<Arc<dyn KvCacheHandle>> {
990        self.active_handles
991            .read()
992            .get(&request_id)
993            .map(|h| h.clone() as Arc<dyn KvCacheHandle>)
994    }
995
996    fn list_handles(&self) -> Vec<(RequestId, Arc<dyn KvCacheHandle>)> {
997        self.active_handles
998            .read()
999            .iter()
1000            .map(|(id, handle)| (id.clone(), handle.clone() as Arc<dyn KvCacheHandle>))
1001            .collect()
1002    }
1003}
1004
1005impl std::fmt::Debug for PagedKvCacheManager {
1006    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1007        let gpu_stats = self.gpu_pool.stats();
1008        f.debug_struct("PagedKvCacheManager")
1009            .field("block_size", &self.config.block_size)
1010            .field("total_gpu_blocks", &gpu_stats.total_blocks)
1011            .field("free_gpu_blocks", &gpu_stats.free_blocks)
1012            .field("active_handles", &self.active_handles.read().len())
1013            .finish()
1014    }
1015}
1016
1017// ============================================================================
1018// Tests
1019// ============================================================================
1020
1021#[cfg(test)]
1022mod tests {
1023    use super::*;
1024
1025    fn create_test_request() -> AllocationRequest {
1026        AllocationRequest {
1027            request_id: RequestId::new(),
1028            initial_tokens: 64,
1029            max_sequence_length: 2048,
1030            num_layers: 32,
1031            num_heads: 32,
1032            head_dim: 128,
1033            device: Device::CPU,
1034            dtype: DataType::FP16,
1035            priority: ferrum_types::Priority::Normal,
1036        }
1037    }
1038
1039    #[tokio::test]
1040    async fn test_manager_creation() {
1041        let manager = PagedKvCacheManager::with_defaults(Device::CPU, 16, 100);
1042        assert!(manager.is_ok());
1043    }
1044
1045    #[tokio::test]
1046    async fn test_allocate_and_deallocate() {
1047        let manager = PagedKvCacheManager::with_defaults(Device::CPU, 16, 100).unwrap();
1048        let request = create_test_request();
1049        let request_id = request.request_id.clone();
1050
1051        let handle = manager.allocate(&request).await.unwrap();
1052        assert!(handle.is_valid());
1053        assert_eq!(handle.num_tokens(), 64);
1054
1055        // Verify blocks were allocated (64 tokens / 16 block_size = 4 blocks)
1056        let stats = handle.stats();
1057        // The paged manager allocates blocks on demand - at least some should be allocated
1058        assert!(stats.blocks_allocated >= 1 || stats.tokens_stored >= 64);
1059
1060        manager.deallocate(request_id).await.unwrap();
1061    }
1062
1063    #[tokio::test]
1064    async fn test_extend() {
1065        let manager = PagedKvCacheManager::with_defaults(Device::CPU, 16, 100).unwrap();
1066        let request = create_test_request();
1067        let request_id = request.request_id.clone();
1068
1069        let handle = manager.allocate(&request).await.unwrap();
1070        let initial_blocks = handle.stats().blocks_allocated;
1071
1072        // Extend to require more blocks
1073        let paged_handle = manager.get_handle(request_id.clone()).unwrap();
1074        let paged_ref = paged_handle
1075            .as_any()
1076            .downcast_ref::<PagedKvCacheHandle>()
1077            .unwrap();
1078        manager.allocate_blocks(paged_ref, 4).unwrap();
1079
1080        let new_blocks = handle.stats().blocks_allocated;
1081        assert!(new_blocks > initial_blocks);
1082
1083        manager.deallocate(request_id).await.unwrap();
1084    }
1085
1086    #[tokio::test]
1087    async fn failed_allocate_rolls_back_partial_blocks() {
1088        let manager = PagedKvCacheManager::with_defaults(Device::CPU, 16, 5).unwrap();
1089
1090        let first = create_test_request();
1091        let first_id = first.request_id.clone();
1092        manager.allocate(&first).await.unwrap();
1093        assert_eq!(manager.stats().free_blocks, 0);
1094
1095        let mut second = create_test_request();
1096        second.request_id = RequestId::new();
1097        let err = manager.allocate(&second).await.unwrap_err();
1098        assert!(matches!(err, FerrumError::ResourceExhausted { .. }));
1099
1100        let stats = manager.stats();
1101        assert_eq!(stats.active_caches, 1);
1102        assert_eq!(
1103            stats.free_blocks, 1,
1104            "partially allocated blocks must be returned to the pool"
1105        );
1106
1107        manager.deallocate(first_id).await.unwrap();
1108        let stats = manager.stats();
1109        assert_eq!(stats.active_caches, 0);
1110        assert_eq!(stats.free_blocks, stats.total_blocks);
1111    }
1112
1113    #[tokio::test]
1114    async fn failed_extend_rolls_back_partial_blocks_and_handle_table() {
1115        let manager = PagedKvCacheManager::with_defaults(Device::CPU, 16, 5).unwrap();
1116        let request = create_test_request();
1117        let request_id = request.request_id.clone();
1118
1119        let handle_dyn = manager.allocate(&request).await.unwrap();
1120        let handle = handle_dyn
1121            .as_any()
1122            .downcast_ref::<PagedKvCacheHandle>()
1123            .unwrap();
1124        assert_eq!(handle.num_blocks(), 4);
1125
1126        let err = manager.allocate_blocks(handle, 3).unwrap_err();
1127        assert!(matches!(err, FerrumError::ResourceExhausted { .. }));
1128        assert_eq!(
1129            handle.num_blocks(),
1130            4,
1131            "failed extend must restore the original handle block table"
1132        );
1133        assert_eq!(
1134            handle.get_physical_blocks().len(),
1135            4,
1136            "failed extend must not leave stale physical block mappings"
1137        );
1138        assert_eq!(
1139            manager.stats().free_blocks,
1140            1,
1141            "partially extended block must be returned to the pool"
1142        );
1143
1144        manager.deallocate(request_id).await.unwrap();
1145        let stats = manager.stats();
1146        assert_eq!(stats.active_caches, 0);
1147        assert_eq!(stats.free_blocks, stats.total_blocks);
1148    }
1149
1150    #[tokio::test]
1151    async fn test_can_allocate() {
1152        let manager = PagedKvCacheManager::with_defaults(Device::CPU, 16, 10).unwrap();
1153
1154        let request = create_test_request();
1155        assert!(manager.can_allocate(&request));
1156
1157        // Allocate many blocks
1158        for _ in 0..8 {
1159            let req = create_test_request();
1160            let _ = manager.allocate(&req).await;
1161        }
1162
1163        // Should eventually fail to allocate more
1164        let stats = manager.stats();
1165        assert!(stats.free_blocks < stats.total_blocks);
1166    }
1167
1168    #[tokio::test]
1169    async fn test_gc() {
1170        let manager = PagedKvCacheManager::with_defaults(Device::CPU, 16, 100).unwrap();
1171
1172        // Allocate and deallocate some caches
1173        let request = create_test_request();
1174        let request_id = request.request_id.clone();
1175        let _ = manager.allocate(&request).await.unwrap();
1176        manager.deallocate(request_id).await.unwrap();
1177
1178        // GC should work
1179        let gc_stats = manager.gc().await.unwrap();
1180        assert_eq!(gc_stats.caches_freed, 0);
1181    }
1182
1183    #[test]
1184    fn test_paged_handle() {
1185        let handle = PagedKvCacheHandle::new(RequestId::new(), Device::CPU, 16, 32, 32, 128);
1186
1187        assert_eq!(handle.num_tokens(), 0);
1188        assert_eq!(handle.num_blocks(), 0);
1189
1190        // Add some blocks
1191        handle.add_block(0, 5);
1192        handle.add_block(1, 10);
1193
1194        assert_eq!(handle.num_blocks(), 2);
1195        assert_eq!(handle.get_physical_block(0), Some(5));
1196        assert_eq!(handle.get_physical_block(1), Some(10));
1197    }
1198
1199    #[tokio::test]
1200    async fn test_write_read_kv_across_blocks() {
1201        // Small model: 2 layers, 2 heads, dim=4, block_size=4
1202        let config = PagedKvCacheConfig {
1203            block_size: 4,
1204            max_gpu_blocks: 16,
1205            max_cpu_blocks: 0,
1206            enable_cow: false,
1207            enable_swapping: false,
1208            num_layers: 2,
1209            num_heads: 2,
1210            head_dim: 4,
1211            enable_prefix_cache: false,
1212            ..Default::default()
1213        };
1214        let manager = PagedKvCacheManager::new(Device::CPU, config).unwrap();
1215
1216        let request = AllocationRequest {
1217            request_id: RequestId::new(),
1218            initial_tokens: 6, // needs 2 blocks (ceil(6/4))
1219            max_sequence_length: 32,
1220            num_layers: 2,
1221            num_heads: 2,
1222            head_dim: 4,
1223            device: Device::CPU,
1224            dtype: DataType::FP16,
1225            priority: ferrum_types::Priority::Normal,
1226        };
1227        let request_id = request.request_id.clone();
1228
1229        let handle_dyn = manager.allocate(&request).await.unwrap();
1230        let handle = handle_dyn
1231            .as_any()
1232            .downcast_ref::<PagedKvCacheHandle>()
1233            .unwrap();
1234
1235        let kv_size = 2 * 4; // num_heads * head_dim = 8
1236
1237        // Write KV for 6 tokens across 2 blocks (tokens 0-3 in block 0, 4-5 in block 1)
1238        for pos in 0..6 {
1239            let key: Vec<f32> = (0..kv_size).map(|i| (pos * 100 + i) as f32).collect();
1240            let val: Vec<f32> = (0..kv_size).map(|i| (pos * 100 + i + 50) as f32).collect();
1241            manager.write_kv(handle, 0, pos, &key, &val).unwrap();
1242        }
1243
1244        // Read back all 6 tokens — this gathers across 2 non-contiguous blocks
1245        let (keys, vals) = manager.read_kv(handle, 0, 0, 6).unwrap();
1246        assert_eq!(keys.len(), 6 * kv_size);
1247        assert_eq!(vals.len(), 6 * kv_size);
1248
1249        // Verify token 0 key
1250        assert_eq!(keys[0], 0.0);
1251        assert_eq!(keys[kv_size - 1], 7.0);
1252
1253        // Verify token 4 key (first token of second block)
1254        assert_eq!(keys[4 * kv_size], 400.0);
1255
1256        // Verify token 5 value
1257        assert_eq!(vals[5 * kv_size], 550.0);
1258
1259        // Layer 1 should still be zeros
1260        let (k1, _) = manager.read_kv(handle, 1, 0, 1).unwrap();
1261        assert!(k1.iter().all(|&x| x == 0.0));
1262
1263        manager.deallocate(request_id).await.unwrap();
1264    }
1265
1266    #[test]
1267    fn test_required_blocks() {
1268        let handle = PagedKvCacheHandle::new(
1269            RequestId::new(),
1270            Device::CPU,
1271            16, // block size
1272            32,
1273            32,
1274            128,
1275        );
1276
1277        assert_eq!(handle.required_blocks(0), 0);
1278        assert_eq!(handle.required_blocks(16), 1);
1279        assert_eq!(handle.required_blocks(17), 2);
1280        assert_eq!(handle.required_blocks(32), 2);
1281        assert_eq!(handle.required_blocks(33), 3);
1282    }
1283}