Skip to main content

kvbm_config/
offload.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Offload policy configuration for KVBM.
5//!
6//! Defines configuration for offload policies that control which blocks
7//! are transferred between storage tiers (G1→G2, G2→G3).
8//!
9//! # Policy Types
10//!
11//! - `pass_all`: No filtering, all blocks pass
12//! - `presence`: Skip blocks already present in destination tier
13//! - `presence_lfu`: Presence check + LFU count threshold
14//!
15//! # Configuration
16//!
17//! Policies are configured per tier transition. Multiple policies in the
18//! `policies` list are applied in order with implicit AND logic (all must pass).
19//!
20//! ## JSON Example
21//!
22//! ```json
23//! {
24//!   "offload": {
25//!     "g1_to_g2": {
26//!       "policies": ["presence"],
27//!       "presence": {}
28//!     },
29//!     "g2_to_g3": {
30//!       "policies": ["presence_lfu"],
31//!       "presence_lfu": { "min_lfu_count": 8 }
32//!     }
33//!   }
34//! }
35//! ```
36
37use serde::{Deserialize, Serialize};
38use validator::Validate;
39
40/// Policy type enum for serialization.
41///
42/// Each variant corresponds to a policy implementation in the kvbm crate.
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum PolicyType {
46    /// PassAllPolicy - no filtering, all blocks pass
47    PassAll,
48    /// PresenceFilter - skip blocks already in destination tier
49    Presence,
50    /// PresenceAndLFUFilter - presence check + LFU threshold
51    PresenceLfu,
52}
53
54/// Configuration for presence filter.
55///
56/// Currently has no parameters, but the struct exists for future extensibility
57/// and to maintain consistent configuration patterns.
58#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
59pub struct PresenceFilterConfig {}
60
61/// Default LFU count threshold.
62fn default_min_lfu_count() -> u32 {
63    8
64}
65
66/// Configuration for presence + LFU filter.
67///
68/// Combines presence checking with LFU (Least Frequently Used) count threshold.
69/// Only blocks with access count above the threshold are offloaded.
70#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
71pub struct PresenceLfuFilterConfig {
72    /// Minimum LFU count threshold for offload.
73    ///
74    /// Blocks must have been accessed more than this many times to be
75    /// considered for offload. This prevents offloading rarely-used blocks.
76    ///
77    /// Default: 8
78    #[serde(default = "default_min_lfu_count")]
79    #[validate(range(min = 1))]
80    pub min_lfu_count: u32,
81}
82
83impl Default for PresenceLfuFilterConfig {
84    fn default() -> Self {
85        Self {
86            min_lfu_count: default_min_lfu_count(),
87        }
88    }
89}
90
91/// Configuration for a tier transition (e.g., G1→G2, G2→G3).
92///
93/// Defines which policies to apply when offloading blocks between tiers.
94/// Policies are evaluated in order with implicit AND logic - a block must
95/// pass ALL policies to be transferred.
96#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
97pub struct TierOffloadConfig {
98    /// Ordered list of policies to apply (implicit AND).
99    ///
100    /// If empty, tier-specific defaults are applied by the engine.
101    /// Policies are evaluated in order; a block must pass all to be transferred.
102    #[serde(default)]
103    pub policies: Vec<PolicyType>,
104
105    /// Presence filter configuration.
106    ///
107    /// Used when "presence" is in the policies list.
108    #[serde(default)]
109    #[validate(nested)]
110    pub presence: PresenceFilterConfig,
111
112    /// Presence + LFU filter configuration.
113    ///
114    /// Used when "presence_lfu" is in the policies list.
115    #[serde(default)]
116    #[validate(nested)]
117    pub presence_lfu: PresenceLfuFilterConfig,
118}
119
120/// Top-level offload configuration.
121///
122/// Groups policy configurations for each tier transition.
123#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
124pub struct OffloadConfig {
125    /// G1 (GPU) → G2 (Host) offload policies.
126    #[serde(default)]
127    #[validate(nested)]
128    pub g1_to_g2: TierOffloadConfig,
129
130    /// G2 (Host) → G3 (Disk) offload policies.
131    #[serde(default)]
132    #[validate(nested)]
133    pub g2_to_g3: TierOffloadConfig,
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_default_config() {
142        let config = OffloadConfig::default();
143        // Empty policies - engine applies tier-specific defaults
144        assert!(config.g1_to_g2.policies.is_empty());
145        assert!(config.g2_to_g3.policies.is_empty());
146        assert_eq!(config.g2_to_g3.presence_lfu.min_lfu_count, 8);
147    }
148
149    #[test]
150    fn test_policy_type_serde() {
151        let json = r#"["pass_all", "presence", "presence_lfu"]"#;
152        let policies: Vec<PolicyType> = serde_json::from_str(json).unwrap();
153        assert_eq!(policies.len(), 3);
154        assert_eq!(policies[0], PolicyType::PassAll);
155        assert_eq!(policies[1], PolicyType::Presence);
156        assert_eq!(policies[2], PolicyType::PresenceLfu);
157
158        // Roundtrip (serde_json doesn't add spaces after commas)
159        let serialized = serde_json::to_string(&policies).unwrap();
160        let roundtrip: Vec<PolicyType> = serde_json::from_str(&serialized).unwrap();
161        assert_eq!(policies, roundtrip);
162    }
163
164    #[test]
165    fn test_tier_config_serde() {
166        let json = r#"{
167            "policies": ["presence_lfu"],
168            "presence_lfu": { "min_lfu_count": 16 }
169        }"#;
170
171        let config: TierOffloadConfig = serde_json::from_str(json).unwrap();
172        assert_eq!(config.policies.len(), 1);
173        assert_eq!(config.policies[0], PolicyType::PresenceLfu);
174        assert_eq!(config.presence_lfu.min_lfu_count, 16);
175    }
176
177    #[test]
178    fn test_offload_config_serde() {
179        let json = r#"{
180            "g1_to_g2": {
181                "policies": ["presence"]
182            },
183            "g2_to_g3": {
184                "policies": ["presence_lfu"],
185                "presence_lfu": { "min_lfu_count": 4 }
186            }
187        }"#;
188
189        let config: OffloadConfig = serde_json::from_str(json).unwrap();
190        assert_eq!(config.g1_to_g2.policies, vec![PolicyType::Presence]);
191        assert_eq!(config.g2_to_g3.policies, vec![PolicyType::PresenceLfu]);
192        assert_eq!(config.g2_to_g3.presence_lfu.min_lfu_count, 4);
193    }
194
195    #[test]
196    fn test_default_lfu_threshold() {
197        let json = r#"{"policies": ["presence_lfu"]}"#;
198        let config: TierOffloadConfig = serde_json::from_str(json).unwrap();
199        // Should use default of 8
200        assert_eq!(config.presence_lfu.min_lfu_count, 8);
201    }
202
203    #[test]
204    fn test_validation() {
205        let config = OffloadConfig::default();
206        assert!(config.validate().is_ok());
207
208        let config_with_lfu = OffloadConfig {
209            g2_to_g3: TierOffloadConfig {
210                policies: vec![PolicyType::PresenceLfu],
211                presence_lfu: PresenceLfuFilterConfig { min_lfu_count: 1 },
212                ..Default::default()
213            },
214            ..Default::default()
215        };
216        assert!(config_with_lfu.validate().is_ok());
217    }
218}