|
| 1 | +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 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 | +
|
| 37 | +use serde::{Deserialize, Serialize}; |
| 38 | +use 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")] |
| 45 | +pub 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)] |
| 59 | +pub struct PresenceFilterConfig {} |
| 60 | + |
| 61 | +/// Default LFU count threshold. |
| 62 | +fn 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)] |
| 71 | +pub 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 | + |
| 83 | +impl 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)] |
| 97 | +pub struct TierOffloadConfig { |
| 98 | + /// Ordered list of policies to apply (implicit AND). |
| 99 | + /// |
| 100 | + /// If empty, defaults to pass-all behavior. |
| 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)] |
| 124 | +pub 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)] |
| 137 | +mod tests { |
| 138 | + use super::*; |
| 139 | + |
| 140 | + #[test] |
| 141 | + fn test_default_config() { |
| 142 | + let config = OffloadConfig::default(); |
| 143 | + assert!(config.g1_to_g2.policies.is_empty()); |
| 144 | + assert!(config.g2_to_g3.policies.is_empty()); |
| 145 | + assert_eq!(config.g2_to_g3.presence_lfu.min_lfu_count, 8); |
| 146 | + } |
| 147 | + |
| 148 | + #[test] |
| 149 | + fn test_policy_type_serde() { |
| 150 | + let json = r#"["pass_all", "presence", "presence_lfu"]"#; |
| 151 | + let policies: Vec<PolicyType> = serde_json::from_str(json).unwrap(); |
| 152 | + assert_eq!(policies.len(), 3); |
| 153 | + assert_eq!(policies[0], PolicyType::PassAll); |
| 154 | + assert_eq!(policies[1], PolicyType::Presence); |
| 155 | + assert_eq!(policies[2], PolicyType::PresenceLfu); |
| 156 | + |
| 157 | + // Roundtrip (serde_json doesn't add spaces after commas) |
| 158 | + let serialized = serde_json::to_string(&policies).unwrap(); |
| 159 | + let roundtrip: Vec<PolicyType> = serde_json::from_str(&serialized).unwrap(); |
| 160 | + assert_eq!(policies, roundtrip); |
| 161 | + } |
| 162 | + |
| 163 | + #[test] |
| 164 | + fn test_tier_config_serde() { |
| 165 | + let json = r#"{ |
| 166 | + "policies": ["presence_lfu"], |
| 167 | + "presence_lfu": { "min_lfu_count": 16 } |
| 168 | + }"#; |
| 169 | + |
| 170 | + let config: TierOffloadConfig = serde_json::from_str(json).unwrap(); |
| 171 | + assert_eq!(config.policies.len(), 1); |
| 172 | + assert_eq!(config.policies[0], PolicyType::PresenceLfu); |
| 173 | + assert_eq!(config.presence_lfu.min_lfu_count, 16); |
| 174 | + } |
| 175 | + |
| 176 | + #[test] |
| 177 | + fn test_offload_config_serde() { |
| 178 | + let json = r#"{ |
| 179 | + "g1_to_g2": { |
| 180 | + "policies": ["presence"] |
| 181 | + }, |
| 182 | + "g2_to_g3": { |
| 183 | + "policies": ["presence_lfu"], |
| 184 | + "presence_lfu": { "min_lfu_count": 4 } |
| 185 | + } |
| 186 | + }"#; |
| 187 | + |
| 188 | + let config: OffloadConfig = serde_json::from_str(json).unwrap(); |
| 189 | + assert_eq!(config.g1_to_g2.policies, vec![PolicyType::Presence]); |
| 190 | + assert_eq!(config.g2_to_g3.policies, vec![PolicyType::PresenceLfu]); |
| 191 | + assert_eq!(config.g2_to_g3.presence_lfu.min_lfu_count, 4); |
| 192 | + } |
| 193 | + |
| 194 | + #[test] |
| 195 | + fn test_default_lfu_threshold() { |
| 196 | + let json = r#"{"policies": ["presence_lfu"]}"#; |
| 197 | + let config: TierOffloadConfig = serde_json::from_str(json).unwrap(); |
| 198 | + // Should use default of 8 |
| 199 | + assert_eq!(config.presence_lfu.min_lfu_count, 8); |
| 200 | + } |
| 201 | + |
| 202 | + #[test] |
| 203 | + fn test_validation() { |
| 204 | + let config = OffloadConfig::default(); |
| 205 | + assert!(config.validate().is_ok()); |
| 206 | + |
| 207 | + let config_with_lfu = OffloadConfig { |
| 208 | + g2_to_g3: TierOffloadConfig { |
| 209 | + policies: vec![PolicyType::PresenceLfu], |
| 210 | + presence_lfu: PresenceLfuFilterConfig { min_lfu_count: 1 }, |
| 211 | + ..Default::default() |
| 212 | + }, |
| 213 | + ..Default::default() |
| 214 | + }; |
| 215 | + assert!(config_with_lfu.validate().is_ok()); |
| 216 | + } |
| 217 | +} |
0 commit comments