Skip to main content

rlx_runtime/
session.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//! Session — the main entry point for compiling and executing graphs.
6
7use crate::backend::Backend;
8use crate::compiled::CompiledGraph;
9use crate::precision::Precision;
10use rlx_driver::Device;
11use rlx_ir::Graph;
12use rlx_ir::GraphModule;
13use rlx_ir::hir::HirModule;
14use rlx_opt::PrecisionPolicy;
15
16/// A session manages graph compilation and execution on a device.
17pub struct Session {
18    device: Device,
19    precision: Precision,
20    /// Optional per-op precision policy. If set, runs AutoMixedPrecision
21    /// rewrite before backend compile. Works identically across all modes
22    /// (AOT compile, trace/JIT, proc-macro AOT) — it's just a graph pass.
23    policy: Option<PrecisionPolicy>,
24}
25
26impl Session {
27    /// Create a session for the given device at default (F32) precision.
28    ///
29    /// # Panics
30    /// Panics if the device is not available (missing feature flag).
31    pub fn new(device: Device) -> Self {
32        Self::new_with_precision(device, Precision::F32)
33    }
34
35    /// Create a session targeting a specific numeric precision.
36    /// Backends fall back to F32 if the requested precision isn't supported.
37    pub fn new_with_precision(device: Device, precision: Precision) -> Self {
38        // Availability has two halves — feature compiled in, and device present.
39        // Name the one that is actually missing: "enable the `gpu` feature" is
40        // wrong (and misleading) on a headless host where it already is enabled.
41        assert!(
42            crate::device_ext::is_available(device),
43            "device {} is not available — {}",
44            device,
45            if crate::device_ext::feature_compiled(device) {
46                format!(
47                    "the `{}` feature is enabled but no such device/driver was found on this host",
48                    feature_name(device)
49                )
50            } else {
51                format!("enable the `{}` Cargo feature", feature_name(device))
52            }
53        );
54        Self {
55            device,
56            precision,
57            policy: None,
58        }
59    }
60
61    /// Builder: set a per-op precision policy. Applied as a graph rewrite
62    /// before backend compile. Same mechanism works for AOT compile, JIT
63    /// tracing, and proc-macro AOT — it's a graph pass, not a runtime mode.
64    pub fn with_policy(mut self, policy: PrecisionPolicy) -> Self {
65        self.policy = Some(policy);
66        self
67    }
68
69    pub fn device(&self) -> Device {
70        self.device
71    }
72    pub fn precision(&self) -> Precision {
73        self.precision
74    }
75    pub fn policy(&self) -> Option<&PrecisionPolicy> {
76        self.policy.as_ref()
77    }
78
79    /// Compile a MIR graph through the fusion-first pipeline (`GraphModule` → LIR).
80    ///
81    /// Prefer [`Self::compile_hir`] or [`Self::compile_module`] for new code.
82    /// This entry wraps the graph as a MIR-stage [`GraphModule`].
83    ///
84    /// If the graph carries `Dim::Dynamic` dims it is compiled lazily: the first
85    /// `run` infers the concrete shape and specializes, so the same graph runs at
86    /// multiple input shapes on every backend. See `crate::deferred`.
87    ///
88    /// On ANE with `RLX_COREML_NATIVE_FLEX=1`, dynamic graphs compile once with
89    /// CoreML `ShapeRange` and infer shapes at predict time instead.
90    pub fn compile(&self, graph: Graph) -> CompiledGraph {
91        if rlx_ir::dynamic::has_dynamic_dims(&graph) && !self.coreml_native_flex() {
92            return self.compile_deferred(graph, self.default_options());
93        }
94        let opts = self.default_options();
95        if opts.cache_param_invariant
96            && let Some(staged) = self.try_hoist_param_invariant(&graph, &opts)
97        {
98            return staged;
99        }
100        self.compile_module(GraphModule::from_graph(graph))
101            .expect("compile MIR graph through fusion pipeline")
102    }
103
104    /// If `graph` has a hoistable param-invariant closure, compile it as a
105    /// separate run-once `prepare` graph and attach it to the compiled main
106    /// graph. Returns `None` when there is nothing to hoist. Sub-compiles clear
107    /// the flag so this does not recurse.
108    fn try_hoist_param_invariant(
109        &self,
110        graph: &Graph,
111        options: &crate::CompileOptions,
112    ) -> Option<CompiledGraph> {
113        let split = rlx_compile::split_param_invariant(graph)?;
114        let mut sub = options.clone();
115        sub.cache_param_invariant = false;
116        let prepare = self.compile_with(split.prepare, &sub);
117        let main = self.compile_with(split.main, &sub);
118        Some(main.with_staging(
119            prepare,
120            split.boundary,
121            split.prepare_params,
122            split.main_params,
123        ))
124    }
125
126    /// Wrap a dynamic-shape graph so it specializes + recompiles per input shape.
127    fn compile_deferred(&self, graph: Graph, options: crate::CompileOptions) -> CompiledGraph {
128        let backend = self.create_backend();
129        let io_dtypes: Vec<(String, rlx_ir::DType)> = graph
130            .nodes()
131            .iter()
132            .filter_map(|n| match &n.op {
133                rlx_ir::Op::Input { name } | rlx_ir::Op::Param { name } => {
134                    Some((name.clone(), n.shape.dtype()))
135                }
136                _ => None,
137            })
138            .collect();
139        let inner = Box::new(crate::deferred::DeferredExecutable::new(
140            graph,
141            backend,
142            options,
143            self.device,
144        ));
145        let mut compiled = CompiledGraph::new(inner, self.device);
146        compiled.set_io_dtypes(io_dtypes);
147        compiled
148    }
149
150    /// Explicit legacy alias — same as [`Self::compile`].
151    pub fn compile_graph(&self, graph: Graph) -> CompiledGraph {
152        self.compile(graph)
153    }
154
155    /// Compile with explicit options (full control over the pipeline).
156    /// Most callers use `compile()` and configure the session via
157    /// `new_with_precision` / `with_policy`. This escape hatch is for
158    /// callers that need finer control (e.g., disable DCE for debugging).
159    pub fn compile_with(&self, graph: Graph, options: &crate::CompileOptions) -> CompiledGraph {
160        // Opt-in native low-precision GEMM: rewrite every 2-D matmul into a
161        // dynamically-quantized `ScaledMatMul` in the requested format before
162        // the rest of the pipeline (must precede constant folding). Requires
163        // static matmul shapes.
164        let graph = match options.scaled_quant {
165            Some(cfg) => {
166                rlx_opt::rlx_compile::scaled_quant_insert::insert_scaled_matmul(graph, cfg)
167            }
168            None => graph,
169        };
170        if rlx_ir::dynamic::has_dynamic_dims(&graph) && !self.coreml_native_flex() {
171            return self.compile_deferred(graph, options.clone());
172        }
173        if options.cache_param_invariant
174            && let Some(staged) = self.try_hoist_param_invariant(&graph, options)
175        {
176            return staged;
177        }
178        self.compile_module_with(GraphModule::from_graph(graph), options)
179            .expect("compile MIR graph through fusion pipeline")
180    }
181
182    /// Compile a fusion-first HIR module through HIR → MIR → LIR.
183    pub fn compile_hir(&self, hir: HirModule) -> Result<CompiledGraph, rlx_ir::hir::LowerError> {
184        self.compile_hir_with(hir, &self.default_options())
185    }
186
187    /// Compile HIR with explicit compile options.
188    pub fn compile_hir_with(
189        &self,
190        hir: HirModule,
191        options: &crate::CompileOptions,
192    ) -> Result<CompiledGraph, rlx_ir::hir::LowerError> {
193        let backend = self.create_backend();
194        let executable = backend.compile_hir(hir, self.device, options)?;
195        Ok(CompiledGraph::new(executable, self.device))
196    }
197
198    /// Compile a [`GraphModule`] (HIR/MIR/LIR stage) through the pipeline.
199    pub fn compile_module(
200        &self,
201        module: GraphModule,
202    ) -> Result<CompiledGraph, rlx_ir::hir::LowerError> {
203        self.compile_module_with(module, &self.default_options())
204    }
205
206    /// Compile a [`GraphModule`] with explicit compile options.
207    pub fn compile_module_with(
208        &self,
209        module: GraphModule,
210        options: &crate::CompileOptions,
211    ) -> Result<CompiledGraph, rlx_ir::hir::LowerError> {
212        let backend = self.create_backend();
213        // Read the declared boundary dtypes before `module` is consumed: the
214        // f32 entry points need them to convert rather than reinterpret an
215        // F16/BF16 boundary. Every `Session::compile*` path funnels here.
216        let io_dtypes = module.io_dtypes();
217        let executable = backend.compile_module(module, self.device, options)?;
218        let mut compiled = CompiledGraph::new(executable, self.device);
219        compiled.set_io_dtypes(io_dtypes);
220        Ok(compiled)
221    }
222
223    fn default_options(&self) -> crate::CompileOptions {
224        let opts = crate::CompileOptions::new().precision(self.precision);
225        match &self.policy {
226            Some(p) => opts.policy(p.clone()),
227            None => opts,
228        }
229    }
230
231    /// ANE + `RLX_COREML_NATIVE_FLEX=1`: one CoreML model with `ShapeRange`.
232    fn coreml_native_flex(&self) -> bool {
233        #[cfg(all(
234            feature = "coreml",
235            target_vendor = "apple",
236            not(target_os = "watchos")
237        ))]
238        {
239            self.device == Device::Ane
240                && rlx_ir::env::var("RLX_COREML_NATIVE_FLEX").as_deref() == Some("1")
241        }
242        #[cfg(not(all(
243            feature = "coreml",
244            target_vendor = "apple",
245            not(target_os = "watchos")
246        )))]
247        {
248            false
249        }
250    }
251
252    fn create_backend(&self) -> Box<dyn Backend> {
253        // Single dispatch point: consult the registry. Backends register
254        // themselves (builtins via cfg-gated `register_builtin`; external
255        // crates via `register_backend`). No hardcoded match here.
256        crate::registry::backend_for(self.device).unwrap_or_else(|| {
257            panic!(
258                "no backend registered for device {} — enable feature `{}` \
259                 (or call `rlx_runtime::register_backend` for an external backend)",
260                self.device,
261                feature_name(self.device)
262            )
263        })
264    }
265}
266
267fn feature_name(device: Device) -> &'static str {
268    match device {
269        Device::Cpu => "cpu",
270        Device::Metal => "metal",
271        Device::Mlx => "mlx",
272        Device::Ane => "ane",
273        Device::Cuda => "cuda",
274        Device::Rocm => "rocm",
275        Device::Xdna => "xdna",
276        Device::Egpu => "egpu",
277        Device::OneApi => "oneapi",
278        Device::Tpu => "tpu",
279        Device::Hexagon => "qnn",
280        Device::Gpu => "gpu",
281        Device::Vulkan => "vulkan",
282        Device::OpenGl => "opengl",
283        Device::DirectX => "directx",
284        Device::WebGpu => "webgpu",
285    }
286}