Skip to main content

rlx_runtime/
model_pipeline.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Three-step model compile pipeline (template → specialize → backend).
6//!
7//! Host code loads symbolic HIR once, specializes per [`ModelVariant`](rlx_ir::ModelVariant) /
8//! [`DimBinding`], then lowers to a device executable. Pair with
9//! [`BindingManifest`] for parameter-block style binding.
10
11use std::collections::VecDeque;
12
13use rlx_ir::hir::HirModule;
14use rlx_ir::{BindingManifest, DimBinding, ModelComponent};
15use rlx_opt::CompileResult;
16
17use crate::stages;
18use crate::{CompileOptions, CompiledGraph, Device};
19
20/// Compile-once / specialize-per-variant pipeline with optional FIFO cache.
21pub struct ModelCompilePipeline {
22    device: Device,
23    capacity: usize,
24    template: Option<CompileResult>,
25    entries: Vec<(u64, CompiledGraph)>,
26    order: VecDeque<u64>,
27}
28
29impl ModelCompilePipeline {
30    pub fn new(device: Device) -> Self {
31        Self::with_capacity(device, 8)
32    }
33
34    pub fn with_capacity(device: Device, capacity: usize) -> Self {
35        assert!(capacity > 0, "ModelCompilePipeline capacity must be ≥ 1");
36        Self {
37            device,
38            capacity,
39            template: None,
40            entries: Vec::new(),
41            order: VecDeque::new(),
42        }
43    }
44
45    pub fn device(&self) -> Device {
46        self.device
47    }
48
49    pub fn has_template(&self) -> bool {
50        self.template.is_some()
51    }
52
53    /// **Step 1** — run fusion pipeline on symbolic HIR (dynamic dims allowed).
54    pub fn build_template<F>(
55        &mut self,
56        build_hir: F,
57        options: &CompileOptions,
58    ) -> Result<&CompileResult, rlx_ir::hir::LowerError>
59    where
60        F: FnOnce() -> HirModule,
61    {
62        if self.template.is_none() {
63            let pipe = stages::pipeline_for(self.device, options);
64            self.template = Some(pipe.compile_hir(build_hir())?);
65        }
66        Ok(self.template.as_ref().expect("template set"))
67    }
68
69    pub fn template_binding_manifest(&self) -> BindingManifest {
70        let template = self.template.as_ref().expect("call build_template first");
71        BindingManifest::from_lir(&template.lir)
72    }
73
74    /// **Step 2** — bind symbolic dims and replan buffers.
75    pub fn specialize_template(
76        &self,
77        binding: &DimBinding,
78        options: &CompileOptions,
79    ) -> CompileResult {
80        let template = self
81            .template
82            .as_ref()
83            .expect("call build_template before specialize_template");
84        let pipe = stages::pipeline_for(self.device, options);
85        template.specialize(&pipe, binding)
86    }
87
88    /// **Step 3** — backend executable from specialized LIR.
89    pub fn compile_lir(
90        &self,
91        specialized: CompileResult,
92        options: &CompileOptions,
93    ) -> CompiledGraph {
94        let backend = crate::registry::backend_for(self.device).expect("backend registered");
95        let executable = backend.compile_lir(specialized.lir, options);
96        CompiledGraph::new(executable, self.device)
97    }
98
99    /// Full pipeline: template (once) → specialize → compile; cached by `key`.
100    pub fn get_or_compile<F>(
101        &mut self,
102        key: u64,
103        binding: &DimBinding,
104        build_hir: F,
105        options: &CompileOptions,
106    ) -> Result<&mut CompiledGraph, rlx_ir::hir::LowerError>
107    where
108        F: FnOnce() -> HirModule,
109    {
110        if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
111            return Ok(&mut self.entries[idx].1);
112        }
113        let mut template_opts = options.clone();
114        template_opts.dim_binding = None;
115        self.build_template(build_hir, &template_opts)?;
116        let specialized = self.specialize_template(binding, &template_opts);
117        let mut compile_opts = options.clone();
118        compile_opts.dim_binding = None;
119        let compiled = self.compile_lir(specialized, &compile_opts);
120
121        if self.entries.len() >= self.capacity
122            && let Some(evict) = self.order.pop_front()
123        {
124            self.entries.retain(|(k, _)| *k != evict);
125        }
126        self.entries.push((key, compiled));
127        self.order.push_back(key);
128        Ok(&mut self.entries.last_mut().unwrap().1)
129    }
130
131    /// Manifest for a variant without storing specialized LIR in the cache.
132    pub fn binding_manifest_for_binding(
133        &self,
134        binding: &DimBinding,
135        options: &CompileOptions,
136    ) -> BindingManifest {
137        let specialized = self.specialize_template(binding, options);
138        BindingManifest::from_lir(&specialized.lir)
139    }
140
141    /// Layout for a full [`ModelComponent`] (specialized parameter block).
142    pub fn binding_manifest_for_component(
143        &self,
144        component: &ModelComponent,
145        options: &CompileOptions,
146    ) -> BindingManifest {
147        self.binding_manifest_for_binding(&component.dim_binding(), options)
148    }
149
150    /// Template → specialize → compile; keyed by [`ModelComponent::cache_key`].
151    pub fn get_or_compile_component<F>(
152        &mut self,
153        component: &ModelComponent,
154        build_hir: F,
155        options: &CompileOptions,
156    ) -> Result<(&mut CompiledGraph, BindingManifest), rlx_ir::hir::LowerError>
157    where
158        F: FnOnce() -> HirModule,
159    {
160        let key = component.cache_key();
161        let binding = component.dim_binding();
162        let manifest = self.binding_manifest_for_component(component, options);
163        let compiled = self.get_or_compile(key, &binding, build_hir, options)?;
164        Ok((compiled, manifest))
165    }
166
167    pub fn contains(&self, key: u64) -> bool {
168        self.entries.iter().any(|(k, _)| *k == key)
169    }
170
171    pub fn len(&self) -> usize {
172        self.entries.len()
173    }
174
175    pub fn is_empty(&self) -> bool {
176        self.entries.is_empty()
177    }
178
179    /// Symbolic template from [`Self::build_template`] / [`Self::get_or_compile`].
180    pub fn template_result(&self) -> Option<&CompileResult> {
181        self.template.as_ref()
182    }
183
184    /// Build the symbolic template once (no specialization).
185    pub fn ensure_template<F: FnOnce() -> HirModule>(
186        &mut self,
187        build_hir: F,
188        options: &CompileOptions,
189    ) -> Result<&CompileResult, rlx_ir::hir::LowerError> {
190        self.build_template(build_hir, options)
191    }
192
193    /// Disk-backed specialize ([`CompilationMode::Aot`](rlx_ir::CompilationMode::Aot)); caches by `key`.
194    pub fn get_or_specialize_aot<F: FnOnce() -> HirModule>(
195        &mut self,
196        aot: &crate::AotCache,
197        disk_base: &str,
198        key: u64,
199        binding: &DimBinding,
200        build_hir: F,
201        options: &CompileOptions,
202    ) -> Result<&mut CompiledGraph, crate::AotCacheError> {
203        if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
204            return Ok(&mut self.entries[idx].1);
205        }
206        let device = self.device;
207        let template = self.ensure_template(build_hir, options)?;
208        let compiled = aot.specialize_cached(disk_base, binding, device, template, options)?;
209        if self.entries.len() >= self.capacity
210            && let Some(evict_key) = self.order.pop_front()
211        {
212            self.entries.retain(|(k, _)| *k != evict_key);
213        }
214        self.entries.push((key, compiled));
215        self.order.push_back(key);
216        Ok(&mut self.entries.last_mut().unwrap().1)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use rlx_ir::hir::HirMut;
224    use rlx_ir::{DType, HirModule, Shape};
225
226    #[test]
227    fn template_specialize_compile_smoke() {
228        let device = Device::Cpu;
229        let mut pipe = ModelCompilePipeline::new(device);
230        let opts = CompileOptions::new();
231
232        let build = || {
233            let mut hir = HirModule::new("dyn");
234            let mut gb = HirMut::new(&mut hir);
235            let x = gb.input("x", Shape::new(&[1, 8, 4], DType::F32));
236            let w = gb.param("w", Shape::new(&[4, 2], DType::F32));
237            let y = hir.linear(x, w, None, None, Shape::new(&[1, 8, 2], DType::F32));
238            hir.set_outputs(vec![y]);
239            hir
240        };
241
242        pipe.build_template(build, &opts).unwrap();
243        let binding = rlx_ir::DimBinding::new();
244        let spec = pipe.specialize_template(&binding, &opts);
245        let manifest = BindingManifest::from_lir(&spec.lir);
246        assert_eq!(manifest.params[0].name, "w");
247        let mut compiled = pipe.compile_lir(spec, &opts);
248        compiled.set_param("w", &[0.0f32; 8]);
249        let outs = compiled.run(&[("x", &[0.0f32; 32])]);
250        assert_eq!(outs.len(), 1);
251    }
252}