qualia_core_db/wgsl_forge/
cache.rs1use std::path::{Path, PathBuf};
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use super::{CertificationManifest, ForgeError, TuningManifest};
5
6#[derive(Debug, Clone)]
8pub struct ManifestCache {
9 root: PathBuf,
10}
11
12impl ManifestCache {
13 pub fn new(root: impl Into<PathBuf>) -> Self {
14 Self { root: root.into() }
15 }
16
17 pub fn root(&self) -> &Path {
18 &self.root
19 }
20
21 pub fn store_certification(
22 &self,
23 manifest: &CertificationManifest,
24 ) -> Result<PathBuf, ForgeError> {
25 let key = manifest.cache_key.as_deref().ok_or_else(|| {
26 ForgeError::Serialization("certification manifest has no adapter cache key".to_string())
27 })?;
28 self.store_json(key, "certification", manifest)
29 }
30
31 pub fn store_tuning(&self, manifest: &TuningManifest) -> Result<PathBuf, ForgeError> {
32 self.store_json(&manifest.cache_key, "tuning", manifest)
33 }
34
35 pub fn load_tuning(&self, key: &str) -> Result<Option<TuningManifest>, ForgeError> {
36 validate_cache_key(key)?;
37 let path = self.path_for(key, "tuning");
38 if !path.exists() {
39 return Ok(None);
40 }
41 Ok(Some(serde_json::from_slice(&std::fs::read(path)?)?))
42 }
43
44 pub fn topology_key(topology_hash: &str, kernel_id: &str) -> String {
48 blake3::hash(format!("{topology_hash}\0{kernel_id}").as_bytes())
49 .to_hex()
50 .to_string()
51 }
52
53 pub fn load_tuning_for_topology(
54 &self,
55 topology_hash: &str,
56 kernel_id: &str,
57 ) -> Result<Option<TuningManifest>, ForgeError> {
58 self.load_tuning(&Self::topology_key(topology_hash, kernel_id))
59 }
60
61 pub fn store_tuning_for_topology(
62 &self,
63 topology_hash: &str,
64 kernel_id: &str,
65 manifest: &TuningManifest,
66 ) -> Result<PathBuf, ForgeError> {
67 self.store_json(
68 &Self::topology_key(topology_hash, kernel_id),
69 "tuning",
70 manifest,
71 )
72 }
73
74 fn store_json<T: serde::Serialize>(
75 &self,
76 key: &str,
77 kind: &str,
78 value: &T,
79 ) -> Result<PathBuf, ForgeError> {
80 validate_cache_key(key)?;
81 std::fs::create_dir_all(&self.root)?;
82 let final_path = self.path_for(key, kind);
83 let bytes = serde_json::to_vec_pretty(value)?;
84 if final_path.exists() {
85 if std::fs::read(&final_path)? == bytes {
86 return Ok(final_path);
87 }
88 return Err(ForgeError::Serialization(format!(
89 "immutable cache collision at {}",
90 final_path.display()
91 )));
92 }
93
94 static WRITE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
95 let sequence = WRITE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
96 let temporary_path = self.root.join(format!(
97 ".{kind}-{key}-{}-{sequence}.tmp",
98 std::process::id()
99 ));
100 std::fs::write(&temporary_path, &bytes)?;
101 if let Err(error) = std::fs::rename(&temporary_path, &final_path) {
102 if final_path.exists() && std::fs::read(&final_path)? == bytes {
103 let _ = std::fs::remove_file(&temporary_path);
104 return Ok(final_path);
105 }
106 let _ = std::fs::remove_file(&temporary_path);
107 return Err(ForgeError::Io(error.to_string()));
108 }
109 Ok(final_path)
110 }
111
112 fn path_for(&self, key: &str, kind: &str) -> PathBuf {
113 self.root.join(format!("{kind}-{key}.json"))
114 }
115}
116
117fn validate_cache_key(key: &str) -> Result<(), ForgeError> {
118 if key.len() != 64 || !key.bytes().all(|value| value.is_ascii_hexdigit()) {
119 return Err(ForgeError::Serialization(
120 "cache key must be exactly 64 hexadecimal characters".to_string(),
121 ));
122 }
123 Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::wgsl_forge::{
130 AdapterIdentity, BuiltinKernel, CandidateResult, ComparisonReport, Schedule, TimingSource,
131 TimingSummary, TuningResult,
132 };
133
134 #[test]
135 fn tuning_cache_round_trips_by_adapter_key() {
136 let root =
137 std::env::temp_dir().join(format!("qualia-wgsl-forge-cache-{}", std::process::id()));
138 let _ = std::fs::remove_dir_all(&root);
139 let generated = crate::wgsl_forge::generate_builtin(
140 BuiltinKernel::AffineF32,
141 Schedule::default(),
142 crate::wgsl_forge::TargetBackend::Wgsl,
143 )
144 .unwrap();
145 let adapter = AdapterIdentity {
146 name: "test".to_string(),
147 vendor: 1,
148 device: 2,
149 device_type: "DiscreteGpu".to_string(),
150 backend: "Vulkan".to_string(),
151 driver: "test".to_string(),
152 driver_info: "1".to_string(),
153 };
154 let winner = CandidateResult {
155 schedule: Schedule::default(),
156 oracle: ComparisonReport {
157 compared: 1,
158 mismatch_count: 0,
159 first_mismatch: None,
160 max_absolute_error: 0.0,
161 max_relative_error: 0.0,
162 },
163 timing: TimingSummary::from_samples(TimingSource::Synthetic, &[10]).unwrap(),
164 };
165 let manifest = TuningManifest::new(
166 &generated,
167 adapter,
168 TuningResult {
169 evaluated_candidates: 1,
170 rejected_candidates: 0,
171 failures: Vec::new(),
172 winner: winner.clone(),
173 finalists: vec![winner],
174 },
175 )
176 .unwrap();
177 let cache = ManifestCache::new(&root);
178 let first_path = cache.store_tuning(&manifest).unwrap();
179 let second_path = cache.store_tuning(&manifest).unwrap();
180 assert_eq!(first_path, second_path);
181 assert_eq!(
182 cache.load_tuning(&manifest.cache_key).unwrap(),
183 Some(manifest)
184 );
185 std::fs::remove_dir_all(root).unwrap();
186 }
187}