Skip to main content

rlx_runtime/
export.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//! First-class hardware **export** (artifacts), parallel to [`crate::Backend`] (run).
6//!
7//! ```text
8//! Session / Backend::compile  →  ExecutableGraph::run()   (µs–ms)
9//! ExportSession / export_graph →  SystemVerilog / …       (offline)
10//! ```
11//!
12//! There is **no** `Device::Fpga` — FPGA export never goes through
13//! `Session::compile`. Use `ExportSession` instead.
14
15#[cfg(feature = "fpga")]
16use std::path::Path;
17use std::path::PathBuf;
18
19use rlx_ir::Graph;
20
21/// Offline artifact targets (not runtime devices).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum ExportTarget {
24    /// IR → SystemVerilog datapath (`rlx-fpga`).
25    #[cfg(feature = "fpga")]
26    Fpga,
27}
28
29impl ExportTarget {
30    pub fn name(self) -> &'static str {
31        match self {
32            #[cfg(feature = "fpga")]
33            Self::Fpga => "fpga",
34        }
35    }
36}
37
38/// Options for [`export_graph`].
39#[derive(Debug, Clone)]
40pub struct ExportOptions {
41    /// Destination directory for artifacts.
42    pub out_dir: PathBuf,
43    /// FPGA-specific configuration (ignored for other targets).
44    #[cfg(feature = "fpga")]
45    pub fpga: rlx_fpga::FpgaExportConfig,
46}
47
48impl ExportOptions {
49    pub fn new(out_dir: impl Into<PathBuf>) -> Self {
50        Self {
51            out_dir: out_dir.into(),
52            #[cfg(feature = "fpga")]
53            fpga: rlx_fpga::FpgaExportConfig::default(),
54        }
55    }
56
57    #[cfg(feature = "fpga")]
58    pub fn with_fpga(mut self, cfg: rlx_fpga::FpgaExportConfig) -> Self {
59        self.fpga = cfg;
60        self
61    }
62
63    #[cfg(feature = "fpga")]
64    pub fn fpga_generic(out_dir: impl Into<PathBuf>) -> Self {
65        Self::new(out_dir).with_fpga(
66            rlx_fpga::FpgaExportConfig::default().with_hw_target(rlx_fpga::HwTarget::Generic),
67        )
68    }
69}
70
71/// Summary of written artifacts.
72#[derive(Debug, Clone)]
73pub struct ExportedArtifacts {
74    pub target: ExportTarget,
75    pub out_dir: PathBuf,
76    pub files: Vec<String>,
77    pub estimate_summary: Option<String>,
78    pub optimize_summary: Option<String>,
79}
80
81/// Builder-style offline export API (counterpart to [`crate::Session`]).
82///
83/// ```ignore
84/// let exp = ExportSession::fpga("hw/out")
85///     .quant_mode(ExportQuantMode::Int4)
86///     .hw_target(HwTarget::Generic);
87/// exp.export(&graph)?;
88/// ```
89#[cfg(feature = "fpga")]
90pub struct ExportSession {
91    opts: ExportOptions,
92}
93
94#[cfg(feature = "fpga")]
95impl ExportSession {
96    /// Target-agnostic SystemVerilog under `out_dir`.
97    pub fn fpga(out_dir: impl Into<PathBuf>) -> Self {
98        Self {
99            opts: ExportOptions::fpga_generic(out_dir),
100        }
101    }
102
103    pub fn with_config(mut self, cfg: rlx_fpga::FpgaExportConfig) -> Self {
104        self.opts.fpga = cfg;
105        self
106    }
107
108    pub fn quant_mode(mut self, mode: rlx_fpga::ExportQuantMode) -> Self {
109        self.opts.fpga = self.opts.fpga.with_quant_mode(mode);
110        self
111    }
112
113    pub fn hw_target(mut self, hw: rlx_fpga::HwTarget) -> Self {
114        self.opts.fpga = self.opts.fpga.with_hw_target(hw);
115        self
116    }
117
118    pub fn output_kind(mut self, kind: rlx_fpga::OutputKind) -> Self {
119        self.opts.fpga = self.opts.fpga.with_output_kind(kind);
120        self
121    }
122
123    pub fn io(mut self, io: rlx_fpga::IoConfig) -> Self {
124        self.opts.fpga = self.opts.fpga.with_io(io);
125        self
126    }
127
128    pub fn bind_input(mut self, name: impl Into<String>) -> Self {
129        self.opts.fpga = self.opts.fpga.bind_input(name);
130        self
131    }
132
133    pub fn bind_outputs<I, S>(mut self, names: I) -> Self
134    where
135        I: IntoIterator<Item = S>,
136        S: Into<String>,
137    {
138        self.opts.fpga = self.opts.fpga.bind_outputs(names);
139        self
140    }
141
142    pub fn bind_sideband_inputs<I, S>(mut self, names: I) -> Self
143    where
144        I: IntoIterator<Item = S>,
145        S: Into<String>,
146    {
147        self.opts.fpga = self.opts.fpga.bind_sideband_inputs(names);
148        self
149    }
150
151    pub fn sideband(mut self, spec: rlx_fpga::SidebandSpec) -> Self {
152        self.opts.fpga = self.opts.fpga.sideband(spec);
153        self
154    }
155
156    pub fn legalize(mut self, opts: rlx_fpga::LegalizeOptions) -> Self {
157        self.opts.fpga = self.opts.fpga.with_legalize(opts);
158        self
159    }
160
161    pub fn export(&self, graph: &Graph) -> Result<ExportedArtifacts, String> {
162        export_graph(graph, ExportTarget::Fpga, &self.opts)
163    }
164
165    pub fn export_model(&self, model: &rlx_fpga::Model) -> Result<ExportedArtifacts, String> {
166        let result = rlx_fpga::export_model(model, &self.opts.fpga, &self.opts.out_dir)
167            .map_err(|e| e.to_string())?;
168        Ok(ExportedArtifacts {
169            target: ExportTarget::Fpga,
170            out_dir: result.out_dir,
171            files: result.files,
172            estimate_summary: Some(result.estimate.summary()),
173            optimize_summary: Some(result.optimize_summary),
174        })
175    }
176}
177
178/// Export `graph` to hardware artifacts for `target`.
179pub fn export_graph(
180    graph: &Graph,
181    target: ExportTarget,
182    opts: &ExportOptions,
183) -> Result<ExportedArtifacts, String> {
184    #[cfg(feature = "fpga")]
185    {
186        match target {
187            ExportTarget::Fpga => export_fpga(graph, opts),
188        }
189    }
190    #[cfg(not(feature = "fpga"))]
191    {
192        let _ = (graph, target, opts);
193        Err("no export targets enabled; enable the `fpga` feature".into())
194    }
195}
196
197#[cfg(feature = "fpga")]
198fn export_fpga(graph: &Graph, opts: &ExportOptions) -> Result<ExportedArtifacts, String> {
199    let result = rlx_fpga::export_graph(graph, &opts.fpga, &opts.out_dir)?;
200    Ok(ExportedArtifacts {
201        target: ExportTarget::Fpga,
202        out_dir: result.out_dir,
203        files: result.files,
204        estimate_summary: Some(result.estimate.summary()),
205        optimize_summary: Some(result.optimize_summary),
206    })
207}
208
209/// Convenience: export TinyConv-MNIST (cortexm weights) as target-agnostic Verilog.
210#[cfg(feature = "fpga")]
211pub fn export_tinyconv_mnist(
212    out_dir: &Path,
213    cfg: &rlx_fpga::FpgaExportConfig,
214) -> Result<ExportedArtifacts, String> {
215    let model = rlx_fpga::tinyconv_mnist_from_cortexm();
216    let result = rlx_fpga::export_model(&model, cfg, out_dir).map_err(|e| e.to_string())?;
217    Ok(ExportedArtifacts {
218        target: ExportTarget::Fpga,
219        out_dir: result.out_dir,
220        files: result.files,
221        estimate_summary: Some(result.estimate.summary()),
222        optimize_summary: Some(result.optimize_summary),
223    })
224}
225
226#[cfg(all(test, feature = "fpga"))]
227mod tests {
228    use super::*;
229    use rlx_fpga::ir::to_graph;
230    use rlx_fpga::tinyconv_mnist_from_cortexm;
231
232    #[test]
233    fn export_session_tinyconv_generic() {
234        let model = tinyconv_mnist_from_cortexm();
235        let ir = to_graph(&model);
236        let dir = tempfile::tempdir().unwrap();
237        let arts = ExportSession::fpga(dir.path())
238            .quant_mode(rlx_fpga::ExportQuantMode::Int8)
239            .export(&ir.graph)
240            .expect("export");
241        assert!(dir.path().join("top.sv").is_file());
242        assert!(dir.path().join("synth.sh").is_file());
243        assert!(arts.files.iter().any(|f| f == "top.sv"));
244    }
245
246    #[test]
247    fn export_ecp5_emits_board_shell() {
248        let dir = tempfile::tempdir().unwrap();
249        let cfg =
250            rlx_fpga::FpgaExportConfig::default().with_hw_target(rlx_fpga::HwTarget::ecp5_25k());
251        export_tinyconv_mnist(dir.path(), &cfg).unwrap();
252        assert!(dir.path().join("board_top.sv").is_file());
253        assert!(dir.path().join("constraints.lpf").is_file());
254    }
255}