1use 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#[derive(Debug, Clone)]
29pub struct PagedKvCacheConfig {
30 pub block_size: usize,
32 pub max_gpu_blocks: usize,
34 pub max_cpu_blocks: usize,
36 pub enable_cow: bool,
38 pub enable_swapping: bool,
40 pub low_watermark: f32,
42 pub high_watermark: f32,
44 pub num_layers: usize,
46 pub num_heads: usize,
48 pub head_dim: usize,
50 pub enable_prefix_cache: bool,
52 pub max_prefixes: usize,
54 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#[derive(Debug)]
80pub struct PagedKvCacheHandle {
81 request_id: RequestId,
83 device: Device,
85 block_table: RwLock<BlockTable>,
87 num_tokens: RwLock<usize>,
89 num_layers: usize,
91 num_heads: usize,
93 head_dim: usize,
95 block_size: usize,
97 last_access: RwLock<Instant>,
99 has_cow_refs: RwLock<bool>,
101 ref_count: AtomicU64,
103}
104
105impl PagedKvCacheHandle {
106 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 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 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 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 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 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 pub fn required_blocks(&self, num_tokens: usize) -> usize {
214 num_tokens.div_ceil(self.block_size)
215 }
216
217 pub fn add_ref(&self) {
219 self.ref_count.fetch_add(1, Ordering::Relaxed);
220 *self.has_cow_refs.write() = true;
221 }
222
223 pub fn remove_ref(&self) -> u64 {
225 self.ref_count.fetch_sub(1, Ordering::Relaxed)
226 }
227
228 pub fn ref_count(&self) -> u64 {
230 self.ref_count.load(Ordering::Relaxed)
231 }
232
233 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 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 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 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; 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
333pub struct PagedKvCacheManager {
335 config: PagedKvCacheConfig,
337 gpu_pool: BlockPool,
339 cpu_pool: Option<BlockPool>,
341 active_handles: RwLock<HashMap<RequestId, Arc<PagedKvCacheHandle>>>,
343 block_to_request: RwLock<HashMap<PhysicalBlockId, RequestId>>,
345 swapped_blocks: RwLock<HashMap<PhysicalBlockId, PhysicalBlockId>>,
347 prefix_cache: Option<PrefixCache>,
349 stats: Mutex<CacheManagerStats>,
351 #[allow(clippy::type_complexity)]
353 pressure_callback: Mutex<Option<Box<dyn Fn(MemoryPressure) + Send + Sync>>>,
354}
355
356impl PagedKvCacheManager {
357 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 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 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 let logical_id = (current_blocks + i) as u32;
459 handle.add_block(logical_id, physical_id.0);
460
461 self.block_to_request
463 .write()
464 .insert(physical_id, handle.request_id.clone());
465
466 allocated.push(physical_id);
467 }
468
469 {
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 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 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 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 pub fn gpu_pool(&self) -> &BlockPool {
565 &self.gpu_pool
566 }
567
568 pub fn prefix_cache(&self) -> Option<&PrefixCache> {
570 self.prefix_cache.as_ref()
571 }
572
573 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 target.add_block(i as u32, phys_id);
592 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 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 let cpu_allocation = cpu_pool.allocate()?;
621 let cpu_block = cpu_allocation.physical_id;
622
623 swap_map.insert(gpu_block, cpu_block);
627 swapped.push(cpu_block);
628
629 self.gpu_pool.deallocate(gpu_block)?;
631 }
632
633 debug!("Swapped out {} blocks to CPU", swapped.len());
634 Ok(swapped)
635 }
636
637 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 let gpu_allocation = self.gpu_pool.allocate()?;
650 let gpu_block = gpu_allocation.physical_id;
651
652 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 cpu_pool.deallocate(cpu_block)?;
668 }
669
670 debug!("Swapped in {} blocks from CPU", swapped.len());
671 Ok(swapped)
672 }
673
674 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 fn notify_pressure(&self, pressure: MemoryPressure) {
690 if let Some(ref callback) = *self.pressure_callback.lock() {
691 callback(pressure);
692 }
693 }
694
695 pub fn free_block_count(&self) -> usize {
697 self.gpu_pool.stats().free_blocks
698 }
699
700 pub fn total_blocks(&self) -> usize {
702 self.gpu_pool.stats().total_blocks
703 }
704
705 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 let allocation = self.gpu_pool.allocate()?;
716 let new_physical = allocation.physical_id;
717
718 new_blocks.push(new_physical.0);
722
723 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 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 {
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 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 pub fn prefix_cache_stats(&self) -> Option<PrefixCacheStats> {
785 self.prefix_cache.as_ref().map(|pc| pc.stats())
786 }
787
788 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 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 let pressure = self.check_pressure();
820 if matches!(pressure, MemoryPressure::Critical) {
821 self.notify_pressure(pressure);
822 let _ = self.gc().await;
824 }
825
826 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 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 self.active_handles
846 .write()
847 .insert(request.request_id.clone(), handle.clone());
848
849 {
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 if paged_handle.is_cow() && paged_handle.ref_count() > 1 {
875 let existing = paged_handle.get_physical_blocks();
877 let _new_physical = self.cow_copy(paged_handle, &existing)?;
878 }
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 if handle.ref_count() > 1 {
903 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 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 {
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 let bytes_per_block = self.config.block_size
954 * 2 * self.config.num_layers
956 * self.config.num_heads
957 * self.config.head_dim
958 * 2; 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 let evicted = self.gpu_pool.evict_blocks(10)?;
971
972 {
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, 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#[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 let stats = handle.stats();
1057 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 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 for _ in 0..8 {
1159 let req = create_test_request();
1160 let _ = manager.allocate(&req).await;
1161 }
1162
1163 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 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 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 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 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, 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; 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 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 assert_eq!(keys[0], 0.0);
1251 assert_eq!(keys[kv_size - 1], 7.0);
1252
1253 assert_eq!(keys[4 * kv_size], 400.0);
1255
1256 assert_eq!(vals[5 * kv_size], 550.0);
1258
1259 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, 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}