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