Skip to main content

luna_rs/rlx/
encoder.rs

1//! RLX-backed [`LunaEncoder`] — same role as `crate::encoder::LunaEncoder`.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6use anyhow::Context;
7
8use crate::config::ModelConfig;
9use super::graph::{build_forward_graph, ForwardSpec};
10use super::prepare::{channel_wise_normalize, gather_channel_emb, prepare_tokens};
11use super::rope_helpers::{build_rope_table, precompute_rope};
12use super::weights::{
13    apply_params, build_forward_params, build_prepare_params, load_safetensors, ParamMap,
14};
15
16/// Per-epoch output from LUNA inference.
17#[derive(Clone, Debug)]
18pub struct EpochEmbedding {
19    pub output: Vec<f32>,
20    pub shape: Vec<usize>,
21    pub chan_pos: Vec<f32>,
22    pub n_channels: usize,
23}
24
25/// Options for [`LunaEncoder::run_epoch`].
26#[derive(Clone, Copy, Debug)]
27pub struct RunEpochOpts {
28    /// Apply per-channel z-score normalisation along time (default `true`).
29    pub normalize: bool,
30}
31
32impl Default for RunEpochOpts {
33    fn default() -> Self {
34        Self { normalize: true }
35    }
36}
37
38/// RLX LUNA encoder with per-shape compiled-graph cache.
39pub struct LunaEncoder {
40    pub model_cfg: ModelConfig,
41    pub device: rlx::Device,
42
43    forward_params: ParamMap,
44    prepare_params: ParamMap,
45    rope_table: Vec<f32>,
46
47    session: rlx::Session,
48    forward_cache: HashMap<u64, rlx::CompiledGraph>,
49}
50
51impl LunaEncoder {
52    pub fn load(
53        config_path: &Path,
54        weights_path: &Path,
55        device: rlx::Device,
56    ) -> anyhow::Result<(Self, f64)> {
57        let cfg_str = std::fs::read_to_string(config_path)
58            .with_context(|| format!("reading config: {}", config_path.display()))?;
59        let hf_val: serde_json::Value = serde_json::from_str(&cfg_str)?;
60        let model_cfg: ModelConfig = serde_json::from_value(
61            hf_val.get("model").cloned().unwrap_or(hf_val),
62        )
63        .context("parsing model config")?;
64
65        let t = std::time::Instant::now();
66        let mut raw = load_safetensors(
67            weights_path
68                .to_str()
69                .context("weights path not valid UTF-8")?,
70        )?;
71        let mut raw_prepare = raw.clone();
72        let forward_params = build_forward_params(&mut raw, &model_cfg)?;
73        let prepare_params = build_prepare_params(&mut raw_prepare)?;
74
75        let head_dim = model_cfg.head_dim();
76        let rope_table = build_rope_table(head_dim, 1024, 10_000.0);
77        let session = rlx::Session::new(device);
78        let ms = t.elapsed().as_secs_f64() * 1000.0;
79
80        Ok((
81            Self {
82                model_cfg,
83                device,
84                forward_params,
85                prepare_params,
86                rope_table,
87                session,
88                forward_cache: HashMap::new(),
89            },
90            ms,
91        ))
92    }
93
94    pub fn describe(&self) -> String {
95        let c = &self.model_cfg;
96        if c.num_classes > 0 {
97            format!(
98                "LUNA classifier (RLX, dev={:?})  embed_dim={}  classes={}",
99                self.device, c.embed_dim, c.num_classes,
100            )
101        } else {
102            format!(
103                "LUNA encoder (RLX, dev={:?})  embed_dim={}  queries={}  depth={}  patch={}",
104                self.device, c.embed_dim, c.num_queries, c.depth, c.patch_size,
105            )
106        }
107    }
108
109    fn spec(&self, b: usize, c: usize, t: usize) -> ForwardSpec {
110        let cfg = &self.model_cfg;
111        let s = t / cfg.patch_size;
112        let hidden = cfg.hidden_dim();
113        let nh_ca = cfg.num_heads;
114        let nh_rot = cfg.total_heads();
115        ForwardSpec {
116            b,
117            c,
118            s,
119            bt: b * s,
120            d: cfg.embed_dim,
121            q: cfg.num_queries,
122            hidden,
123            nh_ca,
124            nh_rot,
125            dh_ca: cfg.embed_dim / nh_ca,
126            dh_rot: hidden / nh_rot,
127            depth: cfg.depth,
128            ff_ca: (cfg.embed_dim as f64 * cfg.mlp_ratio) as usize,
129            ff_rot: cfg.ffn_hidden_dim(),
130            patch_size: cfg.patch_size,
131            norm_eps: cfg.norm_eps as f32,
132            num_classes: cfg.num_classes,
133            nh_cls: cfg.num_heads,
134        }
135    }
136
137    fn expand_queries(&self, bt: usize) -> Vec<f32> {
138        let q = self.model_cfg.num_queries;
139        let d = self.model_cfg.embed_dim;
140        let embed = &self.forward_params["cross_attn.query_embed"];
141        let flat = if embed.shape == vec![1, q, d] {
142            embed.data.clone()
143        } else {
144            embed.data[..q * d].to_vec()
145        };
146        let mut out = vec![0f32; bt * q * d];
147        for i in 0..bt {
148            out[i * q * d..(i + 1) * q * d].copy_from_slice(&flat);
149        }
150        out
151    }
152
153    fn expand_agg_query(&self, b: usize) -> Vec<f32> {
154        let hidden = self.model_cfg.hidden_dim();
155        let embed = &self.forward_params["classifier.learned_agg"];
156        let flat = if embed.shape == vec![1, 1, hidden] {
157            embed.data.clone()
158        } else {
159            embed.data[..hidden].to_vec()
160        };
161        let mut out = vec![0f32; b * hidden];
162        for i in 0..b {
163            out[i * hidden..(i + 1) * hidden].copy_from_slice(&flat);
164        }
165        out
166    }
167
168    fn channel_emb_slice(
169        &self,
170        indices: Option<&[i32]>,
171        b: usize,
172        c: usize,
173    ) -> Option<Vec<f32>> {
174        let table = self.prepare_params.get("channel_emb.weight")?;
175        let d = self.model_cfg.embed_dim;
176        let idx = indices?;
177        Some(gather_channel_emb(table, idx, b, c, d))
178    }
179
180    fn cache_key(&self, b: usize, c: usize, t: usize) -> u64 {
181        (b as u64) << 40 | (c as u64) << 20 | (t as u64) | ((self.model_cfg.num_classes as u64) << 60)
182    }
183
184    fn compiled_for(&mut self, b: usize, c: usize, t: usize) -> &mut rlx::CompiledGraph {
185        let key = self.cache_key(b, c, t);
186        if !self.forward_cache.contains_key(&key) {
187            let spec = self.spec(b, c, t);
188            let graph = build_forward_graph(&spec);
189            let mut compiled = self.session.compile(graph);
190            apply_params(&mut compiled, &self.forward_params);
191            self.forward_cache.insert(key, compiled);
192        }
193        self.forward_cache.get_mut(&key).expect("just inserted")
194    }
195
196    /// Run inference on one epoch.
197    pub fn run_epoch(
198        &mut self,
199        signal: &[f32],
200        chan_pos: &[f32],
201        channel_indices: Option<&[i32]>,
202        n_channels: usize,
203        n_samples: usize,
204    ) -> anyhow::Result<EpochEmbedding> {
205        self.run_epoch_opts(
206            signal,
207            chan_pos,
208            channel_indices,
209            n_channels,
210            n_samples,
211            RunEpochOpts::default(),
212        )
213    }
214
215    /// Run inference with explicit options (e.g. skip normalisation for parity vectors).
216    pub fn run_epoch_opts(
217        &mut self,
218        signal: &[f32],
219        chan_pos: &[f32],
220        channel_indices: Option<&[i32]>,
221        n_channels: usize,
222        n_samples: usize,
223        opts: RunEpochOpts,
224    ) -> anyhow::Result<EpochEmbedding> {
225        let b = 1usize;
226        let c = n_channels;
227        let t = n_samples;
228        let patch_size = self.model_cfg.patch_size;
229        let embed_dim = self.model_cfg.embed_dim;
230
231        let mut sig = signal.to_vec();
232        if opts.normalize {
233            channel_wise_normalize(&mut sig, c, t);
234        }
235
236        let ch_emb = self.channel_emb_slice(channel_indices, b, c);
237        let (x_tok, dec_q) = prepare_tokens(
238            &sig,
239            chan_pos,
240            ch_emb.as_deref(),
241            b,
242            c,
243            t,
244            patch_size,
245            embed_dim,
246            &self.prepare_params,
247        );
248        self.run_forward_prepared(&x_tok, &dec_q, b, c, t, chan_pos)
249    }
250
251    /// Run the RLX graph from prepared `x_tokenized` / `decoder_queries` (skips CPU prepare).
252    pub fn run_forward_prepared(
253        &mut self,
254        x_tokenized: &[f32],
255        decoder_queries: &[f32],
256        b: usize,
257        c: usize,
258        t: usize,
259        chan_pos: &[f32],
260    ) -> anyhow::Result<EpochEmbedding> {
261        let num_classes = self.model_cfg.num_classes;
262        let spec = self.spec(b, c, t);
263        let queries = self.expand_queries(spec.bt);
264        let head_dim = spec.dh_rot;
265        let (cos, sin) = precompute_rope(&self.rope_table, head_dim, spec.s);
266        let agg_query = if num_classes > 0 {
267            Some(self.expand_agg_query(b))
268        } else {
269            None
270        };
271
272        let compiled = self.compiled_for(b, c, t);
273        let mut inputs = vec![
274            ("x_tokenized", x_tokenized),
275            ("queries", queries.as_slice()),
276            ("freqs_cos", cos.as_slice()),
277            ("freqs_sin", sin.as_slice()),
278        ];
279        if let Some(ref agg) = agg_query {
280            inputs.push(("agg_query", agg.as_slice()));
281        } else {
282            inputs.push(("decoder_queries", decoder_queries));
283        }
284
285        let outs = compiled.run(&inputs);
286        let output = outs
287            .into_iter()
288            .next()
289            .ok_or_else(|| anyhow::anyhow!("forward graph produced no output"))?;
290
291        let shape = if num_classes > 0 {
292            vec![num_classes]
293        } else {
294            vec![c, t]
295        };
296
297        Ok(EpochEmbedding {
298            output,
299            shape,
300            chan_pos: chan_pos.to_vec(),
301            n_channels: c,
302        })
303    }
304
305    /// Convenience wrapper around [`super::io::RlxEpoch`].
306    pub fn run_rlx_epoch(&mut self, ep: &super::io::RlxEpoch) -> anyhow::Result<EpochEmbedding> {
307        self.run_epoch(
308            &ep.signal,
309            &ep.chan_pos,
310            ep.channel_indices.as_deref(),
311            ep.n_channels,
312            ep.n_samples,
313        )
314    }
315}