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}