Skip to main content

qualia_core_db/wgsl_forge/
cache.rs

1use std::path::{Path, PathBuf};
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use super::{CertificationManifest, ForgeError, TuningManifest};
5
6/// Atomic, adapter-keyed storage for certification and tuning evidence.
7#[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    /// Key a tuning record by (topology, kernel) rather than the winning schedule,
45    /// so `auto-tune-all` can ask "has this kernel been tuned on this hardware?"
46    /// before paying for a fresh search (plan ยง8).
47    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}