1use std::fmt;
8use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11
12use rlx_ir::DimBinding;
13use rlx_ir::Graph;
14use rlx_ir::LirFingerprint;
15use rlx_ir::LirModule;
16use rlx_ir::hir::HirModule;
17use rlx_opt::CompileResult;
18
19use crate::stages;
20use crate::{CompileOptions, CompiledGraph, Device};
21
22#[derive(Debug)]
24pub enum AotCacheError {
25 Io(io::Error),
26 Serde(String),
27 Lower(rlx_ir::hir::LowerError),
28}
29
30impl fmt::Display for AotCacheError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Self::Io(e) => write!(f, "{e}"),
34 Self::Serde(e) => write!(f, "serde: {e}"),
35 Self::Lower(e) => write!(f, "{e}"),
36 }
37 }
38}
39
40impl std::error::Error for AotCacheError {}
41
42impl From<io::Error> for AotCacheError {
43 fn from(e: io::Error) -> Self {
44 Self::Io(e)
45 }
46}
47
48impl From<rlx_ir::hir::LowerError> for AotCacheError {
49 fn from(e: rlx_ir::hir::LowerError) -> Self {
50 Self::Lower(e)
51 }
52}
53
54pub struct AotCache {
56 root: PathBuf,
57}
58
59impl AotCache {
60 pub fn new(root: impl Into<PathBuf>) -> Self {
61 Self { root: root.into() }
62 }
63
64 pub fn root(&self) -> &Path {
65 &self.root
66 }
67
68 fn lir_path(&self, key: &str) -> PathBuf {
69 self.root.join(format!("{key}.lir.bin"))
70 }
71
72 fn meta_path(&self, key: &str) -> PathBuf {
73 self.root.join(format!("{key}.meta.json"))
74 }
75
76 pub fn put_lir(&self, key: &str, lir: &LirModule) -> io::Result<LirFingerprint> {
78 fs::create_dir_all(&self.root)?;
79 let fp = LirFingerprint::of(lir);
80 let bytes = rlx_ir::lir_to_bytes(lir)
84 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
85 fs::write(self.lir_path(key), bytes)?;
86 fs::write(
87 self.meta_path(key),
88 format!("{{\"fingerprint\":{}}}\n", fp.0),
89 )?;
90 Ok(fp)
91 }
92
93 pub fn get_lir(&self, key: &str) -> io::Result<LirModule> {
95 let bytes = fs::read(self.lir_path(key))?;
96 rlx_ir::lir_from_bytes(&bytes)
97 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
98 }
99
100 pub fn contains(&self, key: &str) -> bool {
101 self.lir_path(key).is_file()
102 }
103
104 pub fn compile_graph_cached(
110 &self,
111 key: &str,
112 device: Device,
113 graph: Graph,
114 options: &CompileOptions,
115 ) -> Result<CompiledGraph, AotCacheError> {
116 if self.contains(key) {
117 match self.get_lir(key) {
118 Ok(lir) => return Ok(self.compile_lir(device, lir, options)),
119 Err(e) => {
120 eprintln!("[aot] cache miss (reload failed for {key}): {e}");
121 let _ = fs::remove_file(self.lir_path(key));
122 let _ = fs::remove_file(self.meta_path(key));
123 }
124 }
125 }
126 let result = stages::compile_graph_stages(device, graph, options);
127 stages::maybe_log_fusion(&result.fusion);
128 self.put_lir(key, &result.lir)?;
129 Ok(self.compile_lir(device, result.lir, options))
130 }
131
132 pub fn compile_hir_cached(
134 &self,
135 key: &str,
136 device: Device,
137 hir: HirModule,
138 options: &CompileOptions,
139 ) -> Result<CompiledGraph, AotCacheError> {
140 if self.contains(key) {
141 match self.get_lir(key) {
142 Ok(lir) => return Ok(self.compile_lir(device, lir, options)),
143 Err(e) => {
144 eprintln!("[aot] cache miss (reload failed for {key}): {e}");
146 let _ = fs::remove_file(self.lir_path(key));
147 let _ = fs::remove_file(self.meta_path(key));
148 }
149 }
150 }
151 let result = stages::compile_hir_stages(device, hir, options)?;
152 stages::maybe_log_fusion(&result.fusion);
153 self.put_lir(key, &result.lir)?;
154 Ok(self.compile_lir(device, result.lir, options))
155 }
156
157 pub fn specialize_cached(
159 &self,
160 base_key: &str,
161 binding: &DimBinding,
162 device: Device,
163 template: &CompileResult,
164 options: &CompileOptions,
165 ) -> Result<CompiledGraph, AotCacheError> {
166 let spec_key = format!("{base_key}__{}", binding_hash(binding));
167 if self.contains(&spec_key) {
168 match self.get_lir(&spec_key) {
169 Ok(lir) => return Ok(self.compile_lir(device, lir, options)),
170 Err(e) => {
171 eprintln!("[aot] cache miss (reload failed for {spec_key}): {e}");
172 let _ = fs::remove_file(self.lir_path(&spec_key));
173 let _ = fs::remove_file(self.meta_path(&spec_key));
174 }
175 }
176 }
177 let pipe = stages::pipeline_for(device, options);
178 let specialized = template.specialize(&pipe, binding);
179 self.put_lir(&spec_key, &specialized.lir)?;
180 Ok(self.compile_lir(device, specialized.lir, options))
181 }
182
183 fn compile_lir(
184 &self,
185 device: Device,
186 lir: LirModule,
187 options: &CompileOptions,
188 ) -> CompiledGraph {
189 let backend = crate::registry::backend_for(device).expect("backend registered");
190 let executable = backend.compile_lir(lir, options);
191 CompiledGraph::new(executable, device)
192 }
193}
194
195fn binding_hash(binding: &DimBinding) -> u64 {
196 use std::collections::hash_map::DefaultHasher;
197 use std::hash::{Hash, Hasher};
198 let mut h = DefaultHasher::new();
199 for (sym, size) in binding.iter() {
200 sym.hash(&mut h);
201 size.hash(&mut h);
202 }
203 h.finish()
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use rlx_ir::DType;
210 use rlx_ir::Shape;
211
212 #[test]
213 fn aot_lir_roundtrip_on_disk() {
214 let dir = std::env::temp_dir().join(format!("rlx_aot_{}", std::process::id()));
215 let cache = AotCache::new(&dir);
216 let mut hir = HirModule::new("aot");
217 let x = hir.input("x", Shape::new(&[1, 4], DType::F32));
218 let w = hir.param("w", Shape::new(&[4, 2], DType::F32));
219 let y = hir.linear(x, w, None, None, Shape::new(&[1, 2], DType::F32));
220 hir.set_outputs(vec![y]);
221 let opts = CompileOptions::new();
222 let _compiled = cache
223 .compile_hir_cached("tiny", Device::Cpu, hir, &opts)
224 .expect("compile + cache");
225 assert!(cache.contains("tiny"));
226 let lir = cache.get_lir("tiny").expect("reload LIR");
227 assert_eq!(lir.name(), "aot");
228 fs::remove_dir_all(&dir).ok();
229 }
230}