Skip to main content

kvbm_config/
cache.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Cache tier configuration for KVBM.
5//!
6//! Defines configuration for G2 (host/pinned memory) and G3 (disk) cache tiers,
7//! as well as the parallelism mode for distributed workers.
8//!
9//! The leader uses this configuration to coordinate cache tier creation on workers.
10
11use std::path::PathBuf;
12
13use serde::{Deserialize, Serialize};
14use validator::Validate;
15
16/// Parallelism strategy for KV cache across workers.
17///
18/// This determines how KV blocks are distributed and transferred across
19/// multiple workers in a distributed inference setup.
20#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(rename_all = "snake_case")]
22pub enum ParallelismMode {
23    /// Tensor parallel: each worker has a shard of each KV block.
24    ///
25    /// This is the standard approach for tensor-parallel inference where
26    /// attention heads are split across workers. Each worker stores and
27    /// transfers only its portion of each KV block.
28    ///
29    /// All workers have G1, G2, and G3 tiers. Operations execute on all
30    /// workers simultaneously (SPMD).
31    #[default]
32    TensorParallel,
33
34    /// Replicated data: all workers have full KV blocks (MLA scenario).
35    ///
36    /// In MLA (Multi-head Latent Attention) architectures, KV blocks are
37    /// replicated rather than sharded. Only rank 0 has G2/G3 storage;
38    /// data is broadcast to other ranks after loading to G1.
39    ///
40    /// This reduces storage requirements on non-rank-0 workers and is
41    /// suitable when the model's KV representation is the same across
42    /// all attention heads.
43    ReplicatedData,
44}
45
46/// Host cache configuration (G2 tier - pinned CPU memory).
47///
48/// The host cache provides a staging area for KV blocks between GPU and disk.
49/// Memory is allocated as pinned (page-locked) for efficient DMA transfers.
50#[derive(Debug, Clone, Serialize, Deserialize, Validate, Default)]
51pub struct HostCacheConfig {
52    /// Cache size in gigabytes.
53    /// Used to compute num_blocks if not explicitly set.
54    pub cache_size_gb: Option<f64>,
55
56    /// Explicit number of blocks for the host cache.
57    /// Takes priority over cache_size_gb if set.
58    pub num_blocks: Option<usize>,
59}
60
61impl HostCacheConfig {
62    /// Compute the number of blocks based on configuration and block size.
63    ///
64    /// Priority: explicit num_blocks > computed from cache_size_gb
65    ///
66    /// # Arguments
67    /// * `bytes_per_block` - Size of each block in bytes
68    ///
69    /// # Returns
70    /// Number of blocks, or None if neither num_blocks nor cache_size_gb is set,
71    /// or if bytes_per_block is zero.
72    pub fn compute_num_blocks(&self, bytes_per_block: usize) -> Option<usize> {
73        if bytes_per_block == 0 {
74            return None;
75        }
76        self.num_blocks.or_else(|| {
77            self.cache_size_gb.map(|gb| {
78                // Convert GB to bytes and divide by block size
79                ((gb * 1_000_000_000.0) / bytes_per_block as f64) as usize
80            })
81        })
82    }
83
84    /// Check if host cache is enabled (has any configuration).
85    pub fn is_enabled(&self) -> bool {
86        self.num_blocks.is_some() || self.cache_size_gb.is_some()
87    }
88}
89
90/// Disk cache configuration (G3 tier - persistent storage).
91///
92/// The disk cache provides extended capacity for KV blocks beyond GPU and host memory.
93/// Can use either GPU Direct Storage (GDS) for direct GPU-disk transfers or POSIX
94/// for regular file I/O.
95#[derive(Debug, Clone, Serialize, Deserialize, Validate, Default)]
96pub struct DiskCacheConfig {
97    /// Cache size in gigabytes.
98    /// Used to compute num_blocks if not explicitly set.
99    pub cache_size_gb: Option<f64>,
100
101    /// Explicit number of blocks for the disk cache.
102    /// Takes priority over cache_size_gb if set.
103    pub num_blocks: Option<usize>,
104
105    /// Use GPU Direct Storage (GDS) if available.
106    /// When true, enables GDS_MT backend for direct GPU-disk transfers.
107    /// When false or GDS unavailable, falls back to POSIX backend.
108    #[serde(default)]
109    pub use_gds: bool,
110
111    /// Storage path for disk cache files.
112    /// If None, a default path will be used.
113    pub storage_path: Option<PathBuf>,
114}
115
116impl DiskCacheConfig {
117    /// Compute the number of blocks based on configuration and block size.
118    ///
119    /// Priority: explicit num_blocks > computed from cache_size_gb
120    ///
121    /// # Arguments
122    /// * `bytes_per_block` - Size of each block in bytes
123    ///
124    /// # Returns
125    /// Number of blocks, or None if neither num_blocks nor cache_size_gb is set,
126    /// or if bytes_per_block is zero.
127    pub fn compute_num_blocks(&self, bytes_per_block: usize) -> Option<usize> {
128        if bytes_per_block == 0 {
129            return None;
130        }
131        self.num_blocks.or_else(|| {
132            self.cache_size_gb.map(|gb| {
133                // Convert GB to bytes and divide by block size
134                ((gb * 1_000_000_000.0) / bytes_per_block as f64) as usize
135            })
136        })
137    }
138
139    /// Check if disk cache is enabled (has any configuration).
140    pub fn is_enabled(&self) -> bool {
141        self.num_blocks.is_some() || self.cache_size_gb.is_some()
142    }
143}
144
145/// Top-level cache configuration.
146///
147/// Groups host (G2) and disk (G3) cache configurations together,
148/// plus the parallelism mode for distributed workers.
149///
150/// Use Figment profiles to configure different cache settings for leader vs worker.
151#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
152pub struct CacheConfig {
153    /// Host cache (G2 tier) - pinned CPU memory.
154    #[serde(default)]
155    #[validate(nested)]
156    pub host: HostCacheConfig,
157
158    /// Disk cache (G3 tier) - persistent storage.
159    /// Optional - only configure if disk caching is needed.
160    #[validate(nested)]
161    pub disk: Option<DiskCacheConfig>,
162
163    /// Parallelism mode for distributed workers.
164    ///
165    /// - `TensorParallel` (default): Each worker has a shard of each KV block
166    /// - `ReplicatedData`: Only rank 0 has G2/G3; data is broadcast on load
167    ///
168    /// Can be set via env var: `KVBM_CACHE_PARALLELISM=tensor_parallel|replicated_data`
169    #[serde(default)]
170    pub parallelism: ParallelismMode,
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_host_cache_default() {
179        let config = HostCacheConfig::default();
180        assert!(config.cache_size_gb.is_none());
181        assert!(config.num_blocks.is_none());
182        assert!(!config.is_enabled());
183    }
184
185    #[test]
186    fn test_host_cache_explicit_blocks() {
187        let config = HostCacheConfig {
188            num_blocks: Some(1000),
189            cache_size_gb: Some(10.0), // Should be ignored
190        };
191
192        // With 1MB blocks, explicit num_blocks takes priority
193        let bytes_per_block = 1_000_000;
194        assert_eq!(config.compute_num_blocks(bytes_per_block), Some(1000));
195        assert!(config.is_enabled());
196    }
197
198    #[test]
199    fn test_host_cache_from_size_gb() {
200        let config = HostCacheConfig {
201            num_blocks: None,
202            cache_size_gb: Some(10.0), // 10 GB
203        };
204
205        // With 1MB blocks: 10GB / 1MB = 10,000 blocks
206        let bytes_per_block = 1_000_000;
207        assert_eq!(config.compute_num_blocks(bytes_per_block), Some(10_000));
208        assert!(config.is_enabled());
209    }
210
211    #[test]
212    fn test_disk_cache_default() {
213        let config = DiskCacheConfig::default();
214        assert!(config.cache_size_gb.is_none());
215        assert!(config.num_blocks.is_none());
216        assert!(!config.use_gds);
217        assert!(config.storage_path.is_none());
218        assert!(!config.is_enabled());
219    }
220
221    #[test]
222    fn test_disk_cache_with_gds() {
223        let config = DiskCacheConfig {
224            num_blocks: Some(5000),
225            cache_size_gb: None,
226            use_gds: true,
227            storage_path: Some(PathBuf::from("/mnt/nvme/kv_cache")),
228        };
229
230        assert!(config.use_gds);
231        assert_eq!(
232            config.storage_path,
233            Some(PathBuf::from("/mnt/nvme/kv_cache"))
234        );
235        assert!(config.is_enabled());
236    }
237
238    #[test]
239    fn test_parallelism_mode_default() {
240        let mode = ParallelismMode::default();
241        assert_eq!(mode, ParallelismMode::TensorParallel);
242    }
243
244    #[test]
245    fn test_parallelism_mode_serde() {
246        // Test serialization
247        let tp = ParallelismMode::TensorParallel;
248        let json = serde_json::to_string(&tp).unwrap();
249        assert_eq!(json, "\"tensor_parallel\"");
250
251        let rd = ParallelismMode::ReplicatedData;
252        let json = serde_json::to_string(&rd).unwrap();
253        assert_eq!(json, "\"replicated_data\"");
254
255        // Test deserialization
256        let mode: ParallelismMode = serde_json::from_str("\"tensor_parallel\"").unwrap();
257        assert_eq!(mode, ParallelismMode::TensorParallel);
258
259        let mode: ParallelismMode = serde_json::from_str("\"replicated_data\"").unwrap();
260        assert_eq!(mode, ParallelismMode::ReplicatedData);
261    }
262
263    #[test]
264    fn test_cache_config_with_parallelism() {
265        let config = CacheConfig {
266            host: HostCacheConfig::default(),
267            disk: None,
268            parallelism: ParallelismMode::ReplicatedData,
269        };
270
271        assert_eq!(config.parallelism, ParallelismMode::ReplicatedData);
272    }
273
274    #[test]
275    fn test_cache_config_default_parallelism() {
276        let config = CacheConfig::default();
277        assert_eq!(config.parallelism, ParallelismMode::TensorParallel);
278    }
279}