Skip to main content

rlx_runtime/
export.rs

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