1use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9
10use crate::error::{Result, VaultError};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum QuantMethod {
18 Q4_0,
20 Q4KM,
22 Q5KM,
24 Q8_0,
26 F16,
28 F32,
30}
31
32impl QuantMethod {
33 pub fn bits_per_weight(self) -> f64 {
35 match self {
36 Self::Q4_0 => 4.0,
37 Self::Q4KM => 4.5,
38 Self::Q5KM => 5.5,
39 Self::Q8_0 => 8.0,
40 Self::F16 => 16.0,
41 Self::F32 => 32.0,
42 }
43 }
44
45 pub fn as_conversion_str(&self) -> &'static str {
47 match self {
48 Self::Q4_0 => "q4_0",
49 Self::Q4KM => "q4_k_m",
50 Self::Q5KM => "q5_k_m",
51 Self::Q8_0 => "q8_0",
52 Self::F16 => "f16",
53 Self::F32 => "f32",
54 }
55 }
56}
57
58impl std::fmt::Display for QuantMethod {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.write_str(self.as_conversion_str())
61 }
62}
63
64impl std::str::FromStr for QuantMethod {
65 type Err = VaultError;
66 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
67 match s.to_lowercase().replace('-', "_").as_str() {
68 "q4_0" | "q4" => Ok(Self::Q4_0),
69 "q4_k_m" | "q4km" | "q4_k" => Ok(Self::Q4KM),
70 "q5_k_m" | "q5km" | "q5_k" => Ok(Self::Q5KM),
71 "q8_0" | "q8" => Ok(Self::Q8_0),
72 "f16" | "fp16" | "half" => Ok(Self::F16),
73 "f32" | "fp32" | "float" | "full" => Ok(Self::F32),
74 _ => Err(VaultError::InvalidInput(format!(
75 "Unknown quantization method: {s}"
76 ))),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct QuantProfile {
84 pub name: String,
86 pub method: QuantMethod,
88 pub description: Option<String>,
90 #[serde(default)]
92 pub metadata: BTreeMap<String, String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct QuantResult {
98 pub source: PathBuf,
100 pub output: PathBuf,
102 pub method: QuantMethod,
104 pub original_bytes: u64,
106 pub quantized_bytes: u64,
108 pub compression_ratio: f64,
110}
111
112#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114pub struct BatchQuantReport {
115 pub results: Vec<QuantResult>,
116 pub failed: Vec<(PathBuf, String)>,
117}
118
119const PROFILES_FILE: &str = "quant_profiles.json";
122
123#[derive(Debug)]
125pub struct QuantProfileStore {
126 path: PathBuf,
127 profiles: BTreeMap<String, QuantProfile>,
128}
129
130impl QuantProfileStore {
131 pub fn new(vault_path: &Path) -> Result<Self> {
133 let path = vault_path.join(PROFILES_FILE);
134 let profiles = if path.exists() {
135 let data = std::fs::read_to_string(&path)
136 .map_err(|e| VaultError::StorageError(format!("read quant profiles: {e}")))?;
137 serde_json::from_str(&data).unwrap_or_default()
138 } else {
139 BTreeMap::new()
140 };
141 Ok(Self { path, profiles })
142 }
143
144 fn save(&self) -> Result<()> {
145 let data = serde_json::to_string_pretty(&self.profiles)
146 .map_err(|e| VaultError::StorageError(format!("serialize quant profiles: {e}")))?;
147 std::fs::write(&self.path, data)
148 .map_err(|e| VaultError::StorageError(format!("write quant profiles: {e}")))
149 }
150
151 pub fn set(&mut self, profile: QuantProfile) -> Result<()> {
153 self.profiles.insert(profile.name.clone(), profile);
154 self.save()
155 }
156
157 pub fn remove(&mut self, name: &str) -> Result<bool> {
159 let removed = self.profiles.remove(name).is_some();
160 if removed {
161 self.save()?;
162 }
163 Ok(removed)
164 }
165
166 pub fn get(&self, name: &str) -> Option<&QuantProfile> {
168 self.profiles.get(name)
169 }
170
171 pub fn list(&self) -> Vec<&QuantProfile> {
173 self.profiles.values().collect()
174 }
175}
176
177pub fn estimate_quantized_size(original_bytes: u64, from: QuantMethod, to: QuantMethod) -> u64 {
181 let ratio = to.bits_per_weight() / from.bits_per_weight();
182 (original_bytes as f64 * ratio).ceil() as u64
183}
184
185#[cfg(test)]
188mod tests {
189 use super::*;
190 use tempfile::tempdir;
191
192 #[test]
193 fn test_quant_method_roundtrip() {
194 for method in [
195 QuantMethod::Q4_0,
196 QuantMethod::Q4KM,
197 QuantMethod::Q5KM,
198 QuantMethod::Q8_0,
199 QuantMethod::F16,
200 QuantMethod::F32,
201 ] {
202 let s = method.to_string();
203 let parsed: QuantMethod = s.parse().unwrap();
204 assert_eq!(parsed, method);
205 }
206 }
207
208 #[test]
209 fn test_quant_method_parse_aliases() {
210 assert_eq!("q4".parse::<QuantMethod>().unwrap(), QuantMethod::Q4_0);
211 assert_eq!("fp16".parse::<QuantMethod>().unwrap(), QuantMethod::F16);
212 assert_eq!("half".parse::<QuantMethod>().unwrap(), QuantMethod::F16);
213 assert_eq!("q4_k".parse::<QuantMethod>().unwrap(), QuantMethod::Q4KM);
214 assert_eq!("q8".parse::<QuantMethod>().unwrap(), QuantMethod::Q8_0);
215 }
216
217 #[test]
218 fn test_quant_method_parse_invalid() {
219 assert!("q3".parse::<QuantMethod>().is_err());
220 }
221
222 #[test]
223 fn test_bits_per_weight() {
224 assert!((QuantMethod::Q4_0.bits_per_weight() - 4.0).abs() < f64::EPSILON);
225 assert!((QuantMethod::F32.bits_per_weight() - 32.0).abs() < f64::EPSILON);
226 }
227
228 #[test]
229 fn test_estimate_quantized_size() {
230 let est = estimate_quantized_size(1_000_000, QuantMethod::F32, QuantMethod::Q4_0);
232 assert_eq!(est, 125000);
233 }
234
235 #[test]
236 fn test_profile_store_crud() {
237 let dir = tempdir().unwrap();
238 let mut store = QuantProfileStore::new(dir.path()).unwrap();
239
240 let profile = QuantProfile {
241 name: "edge".into(),
242 method: QuantMethod::Q4KM,
243 description: Some("Edge deployment".into()),
244 metadata: BTreeMap::new(),
245 };
246 store.set(profile).unwrap();
247 assert_eq!(store.list().len(), 1);
248 assert_eq!(store.get("edge").unwrap().method, QuantMethod::Q4KM);
249
250 store.remove("edge").unwrap();
251 assert!(store.list().is_empty());
252 }
253
254 #[test]
255 fn test_profile_store_persistence() {
256 let dir = tempdir().unwrap();
257
258 {
259 let mut store = QuantProfileStore::new(dir.path()).unwrap();
260 store
261 .set(QuantProfile {
262 name: "prod".into(),
263 method: QuantMethod::Q8_0,
264 description: None,
265 metadata: BTreeMap::new(),
266 })
267 .unwrap();
268 }
269
270 let store = QuantProfileStore::new(dir.path()).unwrap();
272 assert_eq!(store.list().len(), 1);
273 assert_eq!(store.get("prod").unwrap().method, QuantMethod::Q8_0);
274 }
275
276 #[test]
277 fn test_batch_report_default() {
278 let report = BatchQuantReport::default();
279 assert!(report.results.is_empty());
280 assert!(report.failed.is_empty());
281 }
282}