Skip to main content

dynamo_memory/nixl/
config.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! NIXL backend configuration with Figment support.
5//!
6//! This module provides configuration extraction for NIXL backends from
7//! environment variables with the pattern: `DYN_KVBM_NIXL_BACKEND_<backend>=<value>`
8
9use anyhow::{Result, bail};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Configuration for NIXL backends.
14///
15/// Supports extracting backend configurations from environment variables:
16/// - `DYN_KVBM_NIXL_BACKEND_UCX=true` - Enable UCX backend with default params
17/// - `DYN_KVBM_NIXL_BACKEND_GDS=false` - Explicitly disable GDS backend
18/// - Valid values: true/false, 1/0, on/off, yes/no (case-insensitive)
19/// - Invalid values (e.g., "maybe", "random") will cause an error
20/// - Custom params (e.g., `DYN_KVBM_NIXL_BACKEND_UCX_PARAM1=value`) will cause an error
21///
22/// # Data Structure
23///
24/// Uses a single HashMap where:
25/// - Key presence = backend is enabled
26/// - Value (inner HashMap) = backend-specific parameters (empty = defaults)
27///
28/// # TOML Example
29///
30/// ```toml
31/// [backends.UCX]
32/// # UCX with default params (empty map)
33///
34/// [backends.GDS]
35/// threads = "4"
36/// buffer_size = "1048576"
37/// ```
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct NixlBackendConfig {
40    /// Map of backend name (uppercase) -> optional parameters.
41    ///
42    /// If a backend is present in the map, it's enabled.
43    /// The inner HashMap contains optional override parameters.
44    /// An empty inner map means use default parameters.
45    #[serde(default)]
46    backends: HashMap<String, HashMap<String, String>>,
47}
48
49impl NixlBackendConfig {
50    /// Creates a new configuration with the given backends.
51    ///
52    /// For an empty configuration with no backends, use [`Default::default()`].
53    pub fn new(backends: HashMap<String, HashMap<String, String>>) -> Self {
54        Self { backends }
55    }
56
57    /// Create configuration from environment variables.
58    ///
59    /// Extracts backends from `DYN_KVBM_NIXL_BACKEND_<backend>=<value>` variables.
60    ///
61    /// # Errors
62    /// Returns an error if:
63    /// - Custom parameters are detected (not yet supported)
64    /// - Invalid boolean values are provided (must be truthy or falsey)
65    pub fn from_env() -> Result<Self> {
66        let mut backends = HashMap::new();
67
68        // Extract all environment variables that match our pattern
69        for (key, value) in std::env::vars() {
70            if let Some(remainder) = key.strip_prefix("DYN_KVBM_NIXL_BACKEND_") {
71                // Check if there's an underscore (indicating custom params)
72                if remainder.contains('_') {
73                    bail!(
74                        "Custom NIXL backend parameters are not yet supported. \
75                         Found: {}. Please use only DYN_KVBM_NIXL_BACKEND_<backend>=true \
76                         to enable backends with default parameters.",
77                        key
78                    );
79                }
80
81                // Simple backend enablement (e.g., DYN_KVBM_NIXL_BACKEND_UCX=true)
82                let backend_name = remainder.to_uppercase();
83                match crate::parse_bool(&value) {
84                    Ok(true) => {
85                        backends.insert(backend_name, HashMap::new());
86                    }
87                    Ok(false) => {
88                        // Explicitly disabled, don't add to backends
89                        continue;
90                    }
91                    Err(e) => bail!("Invalid value for {}: {}", key, e),
92                }
93            }
94        }
95
96        Ok(Self { backends })
97    }
98
99    /// Add a backend with default parameters.
100    /// Backend name is normalized to uppercase.
101    pub fn with_backend(mut self, backend: impl Into<String>) -> Self {
102        self.backends
103            .insert(backend.into().to_uppercase(), HashMap::new());
104        self
105    }
106
107    /// Add a backend with custom parameters.
108    /// Backend name is normalized to uppercase.
109    pub fn with_backend_params(
110        mut self,
111        backend: impl Into<String>,
112        params: HashMap<String, String>,
113    ) -> Self {
114        self.backends.insert(backend.into().to_uppercase(), params);
115        self
116    }
117
118    /// Get the list of enabled backend names (uppercase).
119    pub fn backends(&self) -> Vec<String> {
120        self.backends.keys().cloned().collect()
121    }
122
123    /// Get parameters for a specific backend.
124    /// Backend name is normalized to uppercase for lookup.
125    ///
126    /// Returns None if the backend is not enabled.
127    pub fn backend_params(&self, backend: &str) -> Option<&HashMap<String, String>> {
128        self.backends.get(&backend.to_uppercase())
129    }
130
131    /// Check if a specific backend is enabled.
132    pub fn has_backend(&self, backend: &str) -> bool {
133        self.backends.contains_key(&backend.to_uppercase())
134    }
135
136    /// Merge another configuration into this one.
137    ///
138    /// Backends from the other configuration will be added to this one.
139    /// If both have the same backend, params from `other` take precedence.
140    pub fn merge(mut self, other: NixlBackendConfig) -> Self {
141        self.backends.extend(other.backends);
142        self
143    }
144
145    /// Iterate over all enabled backends and their parameters.
146    pub fn iter(&self) -> impl Iterator<Item = (&String, &HashMap<String, String>)> {
147        self.backends.iter()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_new_config_is_empty() {
157        let config = NixlBackendConfig::default();
158        assert_eq!(config.backends().len(), 0);
159    }
160
161    #[test]
162    fn test_default_is_empty() {
163        let config = NixlBackendConfig::default();
164        assert!(config.backends().is_empty()); // default() has no backends
165    }
166
167    #[test]
168    fn test_with_backend() {
169        let config = NixlBackendConfig::default()
170            .with_backend("ucx")
171            .with_backend("gds_mt");
172
173        assert!(config.has_backend("ucx"));
174        assert!(config.has_backend("UCX"));
175        assert!(config.has_backend("gds_mt"));
176        assert!(config.has_backend("GDS_MT"));
177        assert!(!config.has_backend("other"));
178    }
179
180    #[test]
181    fn test_with_backend_params() {
182        let mut params = HashMap::new();
183        params.insert("threads".to_string(), "4".to_string());
184        params.insert("buffer_size".to_string(), "1048576".to_string());
185
186        let config = NixlBackendConfig::default()
187            .with_backend("UCX")
188            .with_backend_params("GDS", params);
189
190        // UCX should have empty params
191        let ucx_params = config.backend_params("UCX").unwrap();
192        assert!(ucx_params.is_empty());
193
194        // GDS should have custom params
195        let gds_params = config.backend_params("GDS").unwrap();
196        assert_eq!(gds_params.get("threads"), Some(&"4".to_string()));
197        assert_eq!(gds_params.get("buffer_size"), Some(&"1048576".to_string()));
198    }
199
200    #[test]
201    fn test_merge_configs() {
202        let config1 = NixlBackendConfig::default().with_backend("ucx");
203        let config2 = NixlBackendConfig::default().with_backend("gds");
204
205        let merged = config1.merge(config2);
206
207        assert!(merged.has_backend("ucx"));
208        assert!(merged.has_backend("gds"));
209    }
210
211    #[test]
212    fn test_backend_name_case_insensitive() {
213        let config = NixlBackendConfig::default()
214            .with_backend("ucx")
215            .with_backend("Gds_mt")
216            .with_backend("OTHER");
217
218        assert!(config.has_backend("UCX"));
219        assert!(config.has_backend("ucx"));
220        assert!(config.has_backend("GDS_MT"));
221        assert!(config.has_backend("gds_mt"));
222        assert!(config.has_backend("OTHER"));
223        assert!(config.has_backend("other"));
224    }
225
226    #[test]
227    fn test_iter() {
228        let mut params = HashMap::new();
229        params.insert("key".to_string(), "value".to_string());
230
231        let config = NixlBackendConfig::default()
232            .with_backend("UCX")
233            .with_backend_params("GDS", params);
234
235        let items: Vec<_> = config.iter().collect();
236        assert_eq!(items.len(), 2);
237    }
238
239    // Note: Testing from_env() would require setting environment variables,
240    // which is challenging in unit tests. This is better tested with integration tests.
241}