Skip to main content

kvbm_config/
lib.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! KVBM Configuration Library
5//!
6//! Provides centralized configuration for Tokio, Rayon, Messenger, and NixL runtimes.
7//! Supports role-specific configuration for leader and worker components.
8
9mod cache;
10mod discovery;
11mod events;
12mod messenger;
13mod nixl;
14mod object;
15mod offload;
16mod onboard;
17mod rayon;
18mod tokio;
19
20pub use cache::{CacheConfig, DiskCacheConfig, HostCacheConfig, ParallelismMode};
21pub use discovery::{
22    DiscoveryConfig, EtcdDiscoveryConfig, FilesystemDiscoveryConfig, P2pDiscoveryConfig,
23};
24pub use events::{BatchingConfig as EventsBatchingConfig, EventPolicyConfig, EventsConfig};
25pub use messenger::{MessengerBackendConfig, MessengerConfig};
26pub use nixl::NixlConfig;
27pub use object::{NixlObjectConfig, ObjectClientConfig, ObjectConfig, S3ObjectConfig};
28pub use offload::{
29    OffloadConfig, PolicyType, PresenceFilterConfig, PresenceLfuFilterConfig, TierOffloadConfig,
30};
31pub use onboard::{OnboardConfig, OnboardMode};
32pub use rayon::RayonConfig;
33pub use tokio::TokioConfig;
34
35use figment::{
36    Figment, Metadata, Profile, Provider,
37    providers::{Env, Format, Json, Serialized, Toml},
38    value::{Dict, Map},
39};
40use serde::{Deserialize, Serialize};
41use thiserror::Error;
42use validator::{Validate, ValidationErrors};
43
44/// Configuration errors
45#[derive(Debug, Error)]
46pub enum ConfigError {
47    #[error("Failed to extract configuration: {0}")]
48    Extraction(#[from] Box<figment::Error>),
49
50    #[error("Configuration validation failed: {0}")]
51    Validation(#[from] ValidationErrors),
52
53    #[error("Configuration error: {0}")]
54    Other(#[from] anyhow::Error),
55}
56
57/// Top-level KVBM configuration.
58///
59/// Use Figment profiles to configure role-specific settings. For example,
60/// leader and worker can have different `tokio.worker_threads` values by
61/// putting them under `"leader"` and `"worker"` profile keys in JSON.
62#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
63pub struct KvbmConfig {
64    #[validate(nested)]
65    pub tokio: TokioConfig,
66
67    #[validate(nested)]
68    pub rayon: RayonConfig,
69
70    #[validate(nested)]
71    pub messenger: MessengerConfig,
72
73    /// NixL configuration. None = NixL disabled.
74    #[validate(nested)]
75    #[serde(default)]
76    pub nixl: Option<NixlConfig>,
77
78    /// Cache configuration (host G2 tier and disk G3 tier).
79    #[validate(nested)]
80    #[serde(default)]
81    pub cache: CacheConfig,
82
83    /// Offload policy configuration (G1→G2, G2→G3 transitions).
84    #[validate(nested)]
85    #[serde(default)]
86    pub offload: OffloadConfig,
87
88    /// Onboard configuration (G2→G1 loading strategy).
89    #[serde(default)]
90    pub onboard: OnboardConfig,
91
92    /// Object storage configuration (G4 tier).
93    /// None = object storage disabled.
94    #[validate(nested)]
95    #[serde(default)]
96    pub object: Option<ObjectConfig>,
97
98    /// Event publishing configuration for distributed coordination.
99    #[validate(nested)]
100    #[serde(default)]
101    pub events: EventsConfig,
102}
103
104impl KvbmConfig {
105    /// Create a Figment configuration with all sources merged.
106    ///
107    /// Configuration sources in priority order (lowest to highest):
108    /// 1. Code defaults
109    /// 2. System config file at /opt/dynamo/etc/kvbm.toml
110    /// 3. TOML file from KVBM_CONFIG_PATH environment variable
111    /// 4. Environment variables (KVBM_* prefixed)
112    pub fn figment() -> Figment {
113        let config_path = std::env::var("KVBM_CONFIG_PATH").unwrap_or_default();
114
115        Figment::new()
116            .merge(Serialized::defaults(KvbmConfig::default()))
117            .merge(Toml::file("/opt/dynamo/etc/kvbm.toml"))
118            .merge(Toml::file(&config_path))
119            // Tokio config: KVBM_TOKIO_WORKER_THREADS, KVBM_TOKIO_MAX_BLOCKING_THREADS
120            .merge(
121                Env::prefixed("KVBM_TOKIO_")
122                    .map(|k| format!("tokio.{}", k.as_str().to_lowercase()).into()),
123            )
124            // Rayon config: KVBM_RAYON_NUM_THREADS
125            .merge(
126                Env::prefixed("KVBM_RAYON_")
127                    .map(|k| format!("rayon.{}", k.as_str().to_lowercase()).into()),
128            )
129            // Messenger backend config: KVBM_MESSENGER_BACKEND_TCP_ADDR, etc.
130            .merge(
131                Env::prefixed("KVBM_MESSENGER_BACKEND_")
132                    .map(|k| format!("messenger.backend.{}", k.as_str().to_lowercase()).into()),
133            )
134            // Messenger discovery config: KVBM_MESSENGER_DISCOVERY_CLUSTER_ID, etc.
135            .merge(
136                Env::prefixed("KVBM_MESSENGER_DISCOVERY_")
137                    .map(|k| format!("messenger.discovery.{}", k.as_str().to_lowercase()).into()),
138            )
139            // NixL config: KVBM_NIXL_BACKENDS (comma-separated list)
140            .merge(
141                Env::prefixed("KVBM_NIXL_")
142                    .map(|k| format!("nixl.{}", k.as_str().to_lowercase()).into()),
143            )
144            // Cache host config: KVBM_CACHE_HOST_SIZE_GB, KVBM_CACHE_HOST_NUM_BLOCKS
145            .merge(
146                Env::prefixed("KVBM_CACHE_HOST_")
147                    .map(|k| format!("cache.host.{}", k.as_str().to_lowercase()).into()),
148            )
149            // Cache disk config: KVBM_CACHE_DISK_SIZE_GB, KVBM_CACHE_DISK_NUM_BLOCKS, etc.
150            .merge(
151                Env::prefixed("KVBM_CACHE_DISK_")
152                    .map(|k| format!("cache.disk.{}", k.as_str().to_lowercase()).into()),
153            )
154            // Cache parallelism mode: KVBM_CACHE_PARALLELISM=tensor_parallel|replicated_data
155            .merge(Env::prefixed("KVBM_CACHE_PARALLELISM").map(|_| "cache.parallelism".into()))
156            // Events config: KVBM_EVENTS_ENABLED, KVBM_EVENTS_SUBJECT
157            .merge(
158                Env::prefixed("KVBM_EVENTS_")
159                    .map(|k| format!("events.{}", k.as_str().to_lowercase()).into()),
160            )
161            // Events batching config: KVBM_EVENTS_BATCHING_WINDOW_DURATION_MS, etc.
162            .merge(
163                Env::prefixed("KVBM_EVENTS_BATCHING_")
164                    .map(|k| format!("events.batching.{}", k.as_str().to_lowercase()).into()),
165            )
166    }
167
168    /// Load configuration from default figment (env and files).
169    pub fn from_env() -> Result<Self, ConfigError> {
170        Self::extract_from(Self::figment())
171    }
172
173    /// Extract configuration from any provider.
174    ///
175    /// Use this to load config from custom sources or to add programmatic overrides.
176    ///
177    /// # Example
178    /// ```rust,ignore
179    /// // Merge tuple pairs for programmatic overrides (figment best practice)
180    /// let config = KvbmConfig::extract_from(
181    ///     KvbmConfig::figment()
182    ///         .merge(("messenger.backend.tcp_port", 8080u16))
183    ///         .merge(("tokio.worker_threads", 4usize))
184    /// )?;
185    /// ```
186    pub fn extract_from<T: Provider>(provider: T) -> Result<Self, ConfigError> {
187        let config: Self = Figment::from(provider)
188            .extract()
189            .map_err(|e| ConfigError::Extraction(Box::new(e)))?;
190        config.validate()?;
191        Ok(config)
192    }
193
194    /// Build a figment from defaults, then merge a custom provider.
195    ///
196    /// Convenience method for adding programmatic overrides with highest priority.
197    ///
198    /// # Example
199    /// ```rust,ignore
200    /// let figment = KvbmConfig::figment_with(("messenger.backend.tcp_port", 8080u16));
201    /// let config = KvbmConfig::extract_from(figment)?;
202    /// ```
203    pub fn figment_with<T: Provider>(extra: T) -> Figment {
204        Self::figment().merge(extra)
205    }
206
207    /// Load configuration merging JSON overrides from Python.
208    ///
209    /// JSON has highest priority - overrides env vars, TOML files, and defaults.
210    /// This is the primary entrypoint for vLLM's `kv_connector_extra_config` dict.
211    ///
212    /// # Example
213    /// ```rust,ignore
214    /// let json = r#"{"tokio": {"worker_threads": 8}, "messenger": {"backend": {"tcp_port": 9000}}}"#;
215    /// let config = KvbmConfig::from_figment_with_json(json)?;
216    /// ```
217    pub fn from_figment_with_json(json: &str) -> Result<Self, ConfigError> {
218        Self::extract_from(Self::figment().merge(Json::string(json)))
219    }
220
221    // ==================== Profile-based Configuration ====================
222    //
223    // Figment profiles allow role-specific configuration. The `profile` key
224    // in TOML/JSON is special - values under it are stored in named profiles
225    // and overlaid when that profile is selected.
226    //
227    // Example JSON:
228    // {
229    //   "tokio": {"worker_threads": 4},           // default profile (all roles)
230    //   "profile": {
231    //     "leader": {"tokio": {"worker_threads": 2}},  // leader-only overlay
232    //     "worker": {"tokio": {"worker_threads": 8}}   // worker-only overlay
233    //   }
234    // }
235    //
236    // When `build_leader()` selects "leader" profile:
237    // - tokio.worker_threads = 2 (from leader profile overlay)
238    //
239    // When `build_worker()` selects "worker" profile:
240    // - tokio.worker_threads = 8 (from worker profile overlay)
241
242    /// Figment with leader profile selected.
243    ///
244    /// This merges `profile.leader.*` values over the defaults.
245    /// If no `profile.leader` section exists, defaults are used.
246    pub fn figment_for_leader() -> Figment {
247        Self::figment().select(Profile::new("leader"))
248    }
249
250    /// Figment with worker profile selected.
251    ///
252    /// This merges `profile.worker.*` values over the defaults.
253    /// If no `profile.worker` section exists, defaults are used.
254    pub fn figment_for_worker() -> Figment {
255        Self::figment().select(Profile::new("worker"))
256    }
257
258    /// Load leader config from env/files with leader profile selected.
259    pub fn from_env_for_leader() -> Result<Self, ConfigError> {
260        Self::extract_from(Self::figment_for_leader())
261    }
262
263    /// Load worker config from env/files with worker profile selected.
264    pub fn from_env_for_worker() -> Result<Self, ConfigError> {
265        Self::extract_from(Self::figment_for_worker())
266    }
267
268    /// Load leader config with JSON overrides and leader profile selected.
269    ///
270    /// JSON top-level keys are treated as profile names when using `.nested()`.
271    /// Keys under `default` apply to all profiles, keys under `leader` only to leader.
272    ///
273    /// Example JSON:
274    /// ```json
275    /// {
276    ///   "leader": {
277    ///     "cache": {"host": {"cache_size_gb": 1.0}},
278    ///     "tokio": {"worker_threads": 2}
279    ///   }
280    /// }
281    /// ```
282    pub fn from_figment_with_json_for_leader(json: &str) -> Result<Self, ConfigError> {
283        Self::extract_from(Self::figment_for_leader().merge(Json::string(json).nested()))
284    }
285
286    /// Load worker config with JSON overrides and worker profile selected.
287    ///
288    /// JSON top-level keys are treated as profile names when using `.nested()`.
289    /// Keys under `default` apply to all profiles, keys under `worker` only to worker.
290    ///
291    /// Example JSON:
292    /// ```json
293    /// {
294    ///   "worker": {"tokio": {"worker_threads": 8}}
295    /// }
296    /// ```
297    pub fn from_figment_with_json_for_worker(json: &str) -> Result<Self, ConfigError> {
298        Self::extract_from(Self::figment_for_worker().merge(Json::string(json).nested()))
299    }
300}
301
302/// Implement Provider trait for KvbmConfig.
303///
304/// This allows KvbmConfig to be used as a configuration source itself,
305/// enabling composition with other providers. Dependent libraries can
306/// extract their own config from the same Figment.
307impl Provider for KvbmConfig {
308    fn metadata(&self) -> Metadata {
309        Metadata::named("KvbmConfig")
310    }
311
312    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
313        Serialized::defaults(self).data()
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_default_config() {
323        let config = KvbmConfig::default();
324        // TokioConfig defaults to 1 worker thread
325        assert_eq!(config.tokio.worker_threads, Some(1));
326        assert!(config.tokio.max_blocking_threads.is_none());
327        assert!(config.rayon.num_threads.is_none());
328    }
329
330    #[test]
331    fn test_figment_defaults() {
332        temp_env::with_vars_unset(
333            vec![
334                "KVBM_CONFIG_PATH",
335                "KVBM_TOKIO_WORKER_THREADS",
336                "KVBM_RAYON_NUM_THREADS",
337                "KVBM_MESSENGER_BACKEND_TCP_ADDR",
338                "KVBM_MESSENGER_DISCOVERY_CLUSTER_ID",
339            ],
340            || {
341                let figment = KvbmConfig::figment();
342                let config: KvbmConfig = figment.extract().unwrap();
343                // TokioConfig defaults to 1 worker thread
344                assert_eq!(config.tokio.worker_threads, Some(1));
345            },
346        );
347    }
348
349    #[test]
350    fn test_env_override_tokio() {
351        temp_env::with_vars(
352            vec![
353                ("KVBM_TOKIO_WORKER_THREADS", Some("2")),
354                ("KVBM_TOKIO_MAX_BLOCKING_THREADS", Some("32")),
355            ],
356            || {
357                let figment = KvbmConfig::figment();
358                let config: KvbmConfig = figment.extract().unwrap();
359                assert_eq!(config.tokio.worker_threads, Some(2));
360                assert_eq!(config.tokio.max_blocking_threads, Some(32));
361            },
362        );
363    }
364
365    #[test]
366    fn test_extract_from_with_tuple_override() {
367        temp_env::with_vars_unset(
368            vec![
369                "KVBM_CONFIG_PATH",
370                "KVBM_TOKIO_WORKER_THREADS",
371                "KVBM_MESSENGER_BACKEND_TCP_PORT",
372            ],
373            || {
374                // Use tuple pair for programmatic override (figment best practice)
375                let figment = KvbmConfig::figment()
376                    .merge(("tokio.worker_threads", 2usize))
377                    .merge(("messenger.backend.tcp_port", 9090u16));
378
379                let config = KvbmConfig::extract_from(figment).unwrap();
380                assert_eq!(config.tokio.worker_threads, Some(2));
381                assert_eq!(config.messenger.backend.tcp_port, 9090);
382            },
383        );
384    }
385
386    #[test]
387    fn test_figment_with_helper() {
388        temp_env::with_vars_unset(vec!["KVBM_CONFIG_PATH", "KVBM_RAYON_NUM_THREADS"], || {
389            let figment = KvbmConfig::figment_with(("rayon.num_threads", 8usize));
390            let config = KvbmConfig::extract_from(figment).unwrap();
391            assert_eq!(config.rayon.num_threads, Some(8));
392        });
393    }
394
395    #[test]
396    fn test_config_as_provider() {
397        // KvbmConfig implements Provider, so it can be used as a source
398        let original = KvbmConfig {
399            tokio: TokioConfig {
400                worker_threads: Some(4),
401                max_blocking_threads: Some(128),
402            },
403            ..Default::default()
404        };
405
406        // Use the config as a provider to create a new figment
407        let figment = Figment::from(&original);
408        let extracted: KvbmConfig = figment.extract().unwrap();
409
410        assert_eq!(extracted.tokio.worker_threads, Some(4));
411        assert_eq!(extracted.tokio.max_blocking_threads, Some(128));
412    }
413
414    #[test]
415    fn test_from_figment_with_json() {
416        temp_env::with_vars_unset(
417            vec![
418                "KVBM_CONFIG_PATH",
419                "KVBM_TOKIO_WORKER_THREADS",
420                "KVBM_MESSENGER_BACKEND_TCP_PORT",
421            ],
422            || {
423                let json = r#"{"tokio": {"worker_threads": 2}, "messenger": {"backend": {"tcp_port": 9090}}}"#;
424                let config = KvbmConfig::from_figment_with_json(json).unwrap();
425
426                assert_eq!(config.tokio.worker_threads, Some(2));
427                assert_eq!(config.messenger.backend.tcp_port, 9090);
428            },
429        );
430    }
431
432    #[test]
433    fn test_from_figment_with_json_overrides_env() {
434        // JSON should override env vars (highest priority)
435        temp_env::with_vars(vec![("KVBM_TOKIO_WORKER_THREADS", Some("1"))], || {
436            let json = r#"{"tokio": {"worker_threads": 2}}"#;
437            let config = KvbmConfig::from_figment_with_json(json).unwrap();
438
439            // JSON (2) should override env var (1)
440            assert_eq!(config.tokio.worker_threads, Some(2));
441        });
442    }
443
444    #[test]
445    fn test_from_figment_with_empty_json() {
446        temp_env::with_vars_unset(["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"], || {
447            let config = KvbmConfig::from_figment_with_json("{}");
448            assert!(config.is_ok(), "Empty JSON should not cause errors");
449        });
450    }
451
452    // ==================== Profile Selection Tests ====================
453    //
454    // Figment profiles work with `.nested()` JSON provider - top-level keys
455    // become profile names. Use "default" for values that apply to all profiles.
456
457    #[test]
458    fn test_profile_selection_leader_vs_worker() {
459        // Test that leader and worker profiles get different values
460        // JSON top-level keys are profile names when using .nested()
461        temp_env::with_vars_unset(
462            vec!["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"],
463            || {
464                // JSON with nested profiles - top-level keys are profile names
465                let json = r#"{
466                    "default": {"tokio": {"worker_threads": 4}},
467                    "leader": {"tokio": {"worker_threads": 2}},
468                    "worker": {"tokio": {"worker_threads": 8}}
469                }"#;
470
471                // Leader should get 2 threads (from leader profile)
472                let leader_config = KvbmConfig::from_figment_with_json_for_leader(json).unwrap();
473                assert_eq!(
474                    leader_config.tokio.worker_threads,
475                    Some(2),
476                    "Leader should get leader profile's tokio.worker_threads"
477                );
478
479                // Worker should get 8 threads (from worker profile)
480                let worker_config = KvbmConfig::from_figment_with_json_for_worker(json).unwrap();
481                assert_eq!(
482                    worker_config.tokio.worker_threads,
483                    Some(8),
484                    "Worker should get worker profile's tokio.worker_threads"
485                );
486            },
487        );
488    }
489
490    #[test]
491    fn test_profile_no_override_uses_default() {
492        // When no profile-specific section exists, default profile values are used
493        temp_env::with_vars_unset(
494            vec!["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"],
495            || {
496                // JSON with only default profile
497                let json = r#"{"default": {"tokio": {"worker_threads": 4}}}"#;
498
499                // Both leader and worker should get the default (4)
500                let leader_config = KvbmConfig::from_figment_with_json_for_leader(json).unwrap();
501                assert_eq!(
502                    leader_config.tokio.worker_threads,
503                    Some(4),
504                    "Leader should use default when no leader profile exists"
505                );
506
507                let worker_config = KvbmConfig::from_figment_with_json_for_worker(json).unwrap();
508                assert_eq!(
509                    worker_config.tokio.worker_threads,
510                    Some(4),
511                    "Worker should use default when no worker profile exists"
512                );
513            },
514        );
515    }
516
517    #[test]
518    fn test_profile_with_defaults_and_overlay() {
519        // Test that default profile values apply to all roles, profile-specific overlay on top
520        temp_env::with_vars_unset(
521            vec!["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"],
522            || {
523                // cache.host in default applies to all profiles
524                // leader profile adds tokio override
525                let json = r#"{
526                    "default": {"cache": {"host": {"cache_size_gb": 2.0}}},
527                    "leader": {"tokio": {"worker_threads": 2}}
528                }"#;
529
530                // Leader: gets cache.host from default + tokio from leader profile
531                let leader_config = KvbmConfig::from_figment_with_json_for_leader(json).unwrap();
532                assert_eq!(leader_config.cache.host.cache_size_gb, Some(2.0));
533                assert_eq!(leader_config.tokio.worker_threads, Some(2));
534
535                // Worker: gets cache.host from default, uses default tokio (not leader's override)
536                let worker_config = KvbmConfig::from_figment_with_json_for_worker(json).unwrap();
537                assert_eq!(worker_config.cache.host.cache_size_gb, Some(2.0));
538                // Worker gets default tokio.worker_threads (1), NOT leader's override (2)
539                assert_eq!(
540                    worker_config.tokio.worker_threads,
541                    Some(1),
542                    "Worker should get default tokio, not leader's override"
543                );
544            },
545        );
546    }
547
548    #[test]
549    fn test_from_env_for_leader_and_worker() {
550        // Test from_env_for_leader and from_env_for_worker work without error
551        temp_env::with_vars_unset(
552            vec!["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"],
553            || {
554                // Both should succeed with default values
555                let leader_config = KvbmConfig::from_env_for_leader();
556                assert!(leader_config.is_ok(), "from_env_for_leader should succeed");
557
558                let worker_config = KvbmConfig::from_env_for_worker();
559                assert!(worker_config.is_ok(), "from_env_for_worker should succeed");
560            },
561        );
562    }
563}