Skip to main content

reve_rs/rlx/
encoder.rs

1//! RLX-backed REVE encoder.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6use anyhow::Context;
7
8use crate::config::ModelConfig;
9use super::graph::{build_reve_graph, ReveSpec};
10use super::pos_embed;
11use super::weights::{apply_params, build_params, load_safetensors, ParamMap};
12
13/// Per-sample output from REVE.
14#[derive(Clone, Debug)]
15pub struct ReveOutput {
16    /// Output values (row-major f32).
17    pub output: Vec<f32>,
18    /// Shape of the output (no batch dim).
19    pub shape: Vec<usize>,
20    pub n_channels: usize,
21}
22
23/// Collection of outputs.
24#[derive(Clone, Debug)]
25pub struct EncodingResult {
26    pub outputs: Vec<ReveOutput>,
27    pub ms_load: f64,
28    pub ms_encode: f64,
29}
30
31pub struct ReveEncoder {
32    pub model_cfg: ModelConfig,
33    pub device: rlx::Device,
34
35    params: ParamMap,
36    cls_query_token: Option<Vec<f32>>,
37
38    session: rlx::Session,
39    cache: HashMap<usize, rlx::CompiledGraph>,
40}
41
42impl ReveEncoder {
43    pub fn load(
44        config_path: &Path,
45        weights_path: &Path,
46        device: rlx::Device,
47    ) -> anyhow::Result<(Self, f64)> {
48        let cfg_str = std::fs::read_to_string(config_path)
49            .with_context(|| format!("config: {}", config_path.display()))?;
50        let hf_val: serde_json::Value = serde_json::from_str(&cfg_str)?;
51        let mut model_cfg: ModelConfig = serde_json::from_value(
52            hf_val.get("model").cloned().unwrap_or(hf_val.clone()),
53        )
54        .context("parsing model config")?;
55
56        let t = std::time::Instant::now();
57        let mut raw = load_safetensors(
58            weights_path.to_str().context("weights path not valid UTF-8")?,
59        )?;
60
61        if !model_cfg.attention_pooling && raw.contains_key("cls_query_token") {
62            model_cfg.attention_pooling = true;
63        }
64        if model_cfg.n_outputs == 0 {
65            let bias_key = if model_cfg.attention_pooling {
66                "final_layer.1.bias"
67            } else {
68                "final_layer.2.bias"
69            };
70            if let Some(p) = raw.get(bias_key) {
71                anyhow::ensure!(p.shape.len() == 1, "{bias_key} must be 1-D");
72                model_cfg.n_outputs = p.shape[0];
73            } else {
74                model_cfg.n_outputs = 0;
75            }
76        }
77
78        let mut params = build_params(&mut raw, &model_cfg)?;
79
80        let cls_query_token = if model_cfg.attention_pooling {
81            let p = params
82                .remove("cls_query_token")
83                .ok_or_else(|| anyhow::anyhow!("missing weight key: cls_query_token"))?;
84            anyhow::ensure!(
85                p.shape == vec![1, 1, model_cfg.embed_dim],
86                "cls_query_token shape mismatch: {:?}",
87                p.shape
88            );
89            Some(p.data)
90        } else {
91            None
92        };
93
94        super::prepare_device(device);
95        let session = rlx::Session::new(device);
96        let ms = t.elapsed().as_secs_f64() * 1000.0;
97        Ok((
98            Self {
99                model_cfg,
100                device,
101                params,
102                cls_query_token,
103                session,
104                cache: HashMap::new(),
105            },
106            ms,
107        ))
108    }
109
110    pub fn describe(&self) -> String {
111        let c = &self.model_cfg;
112        format!(
113            "REVE (RLX, dev={:?})  embed_dim={}  depth={}  heads={}  head_dim={}  patch={}  outputs={}",
114            self.device, c.embed_dim, c.depth, c.heads, c.head_dim, c.patch_size, c.n_outputs,
115        )
116    }
117
118    pub fn params(&self) -> &super::weights::ParamMap { &self.params }
119    pub fn n_patches(&self) -> usize {
120        let c = &self.model_cfg;
121        let step = c.patch_size - c.patch_overlap;
122        if c.n_times == 0 {
123            0
124        } else {
125            (c.n_times - c.patch_size) / step + 1
126        }
127    }
128
129    fn spec(&self, b: usize) -> ReveSpec {
130        let c = &self.model_cfg;
131        let n_patches = self.n_patches();
132        ReveSpec {
133            b,
134            s: c.n_chans * n_patches,
135            patch_size: c.patch_size,
136            embed_dim: c.embed_dim,
137            n_outputs: c.n_outputs,
138            depth: c.depth,
139            heads: c.heads,
140            head_dim: c.head_dim,
141            mlp_dim: c.mlp_dim(),
142            use_geglu: c.use_geglu,
143            freqs: c.freqs,
144            attention_pooling: c.attention_pooling,
145        }
146    }
147
148    fn compiled_for(&mut self, b: usize, s: usize) -> &mut rlx::CompiledGraph {
149        let key = b * 0x10_0000 + s;
150        if !self.cache.contains_key(&key) {
151            let mut spec = self.spec(b);
152            spec.s = s;
153            let graph = build_reve_graph(&spec);
154            let mut compiled = self.session.compile(graph);
155            apply_params(&mut compiled, &self.params);
156            self.cache.insert(key, compiled);
157        }
158        self.cache.get_mut(&key).expect("just inserted")
159    }
160
161    /// CPU-side z-score normalization per channel, clipped to ±15.
162    fn normalize(signal: &mut [f32], n_channels: usize, n_times: usize) {
163        for c in 0..n_channels {
164            let row = &mut signal[c * n_times..(c + 1) * n_times];
165            let mean = row.iter().copied().sum::<f32>() / (n_times as f32);
166            let mut var = 0.0f32;
167            for &v in row.iter() {
168                let d = v - mean;
169                var += d * d;
170            }
171            var /= n_times as f32;
172            let std = (var + 1e-8).sqrt();
173            let inv = 1.0 / std;
174            for v in row.iter_mut() {
175                let z = (*v - mean) * inv;
176                *v = z.clamp(-15.0, 15.0);
177            }
178        }
179    }
180
181    /// CPU-side pre-processing: per-channel z-score on `signal`, then
182    /// patch-extract `[n_channels, n_patches, patch_size]` (flat
183    /// `[n_chans*n_patches, patch_size]`) and emit the 4-D position
184    /// vectors `[n_chans*n_patches, 4]` (the 4th axis is patch index).
185    /// Both outputs are inputs to the RLX graph.
186    pub fn prep_inputs(
187        &self,
188        mut signal: Vec<f32>,
189        positions_xyz: &[f32],
190        n_channels: usize,
191        n_times: usize,
192    ) -> anyhow::Result<(Vec<f32>, Vec<f32>)> {
193        let c = &self.model_cfg;
194        if c.n_chans != 0 {
195            anyhow::ensure!(
196                n_channels == c.n_chans,
197                "n_channels mismatch: got {n_channels}, cfg {}",
198                c.n_chans
199            );
200        }
201        if c.n_times != 0 {
202            anyhow::ensure!(
203                n_times == c.n_times,
204                "n_times mismatch: got {n_times}, cfg {}",
205                c.n_times
206            );
207        }
208        anyhow::ensure!(positions_xyz.len() == n_channels * 3, "positions_xyz len mismatch");
209        anyhow::ensure!(signal.len() == n_channels * n_times, "signal len mismatch");
210
211        Self::normalize(&mut signal, n_channels, n_times);
212
213        let step = c.patch_size - c.patch_overlap;
214        anyhow::ensure!(
215            n_times >= c.patch_size,
216            "n_times ({n_times}) < patch_size ({})",
217            c.patch_size
218        );
219        let n_patches = (n_times - c.patch_size) / step + 1;
220        let s = n_channels * n_patches;
221
222        let mut patches = vec![0f32; s * c.patch_size];
223        let mut pos4 = vec![0f32; s * 4];
224
225        for ch in 0..n_channels {
226            let x = positions_xyz[ch * 3 + 0];
227            let y = positions_xyz[ch * 3 + 1];
228            let z = positions_xyz[ch * 3 + 2];
229            let row = &signal[ch * n_times..(ch + 1) * n_times];
230            for p in 0..n_patches {
231                let start = p * step;
232                let dst_tok = ch * n_patches + p;
233                let dst_patch = dst_tok * c.patch_size;
234                patches[dst_patch..dst_patch + c.patch_size]
235                    .copy_from_slice(&row[start..start + c.patch_size]);
236
237                let dst_pos = dst_tok * 4;
238                pos4[dst_pos + 0] = x;
239                pos4[dst_pos + 1] = y;
240                pos4[dst_pos + 2] = z;
241                pos4[dst_pos + 3] = p as f32;
242            }
243        }
244
245        Ok((patches, pos4))
246    }
247
248    /// Run REVE up to (but not including) `layer_end` of the transformer,
249    /// returning the raw hidden state `[s * embed_dim]` flattened
250    /// (`s = n_channels * n_patches`). Skips the final classification /
251    /// attention-pool head. Caller is responsible for any downstream
252    /// pooling. `layer_end` is clamped to `depth`.
253    ///
254    /// Used for "intermediate-layer feature extraction" experiments where
255    /// later layers may have specialised away from generic EEG structure.
256    pub fn run_at_layer(
257        &mut self,
258        signal: Vec<f32>,
259        positions_xyz: Vec<f32>,
260        n_channels: usize,
261        n_times: usize,
262        layer_end: usize,
263    ) -> anyhow::Result<ReveOutput> {
264        let (patches, pos4) = self.prep_inputs(signal, &positions_xyz, n_channels, n_times)?;
265        let s = pos4.len() / 4;
266        let d = self.model_cfg.embed_dim;
267        let pos_embed = pos_embed::precompute_pos_embed(&pos4, s, d, &self.params);
268        let depth = self.model_cfg.depth;
269        let layer_end = layer_end.min(depth);
270        // Cache key: high bits encode layer_end, low bits encode `s`, batch=1.
271        let key = 0x8000_0000usize.wrapping_add(layer_end * 0x10_0000 + s);
272        if !self.cache.contains_key(&key) {
273            let mut spec = self.spec(1);
274            spec.s = s;
275            let graph = super::graph::build_reve_graph_range(&spec, 0, layer_end, false);
276            let mut compiled = self.session.compile(graph);
277            super::weights::apply_params(&mut compiled, &self.params);
278            self.cache.insert(key, compiled);
279        }
280        let compiled = self.cache.get_mut(&key).expect("just inserted");
281        let outs = compiled.run(&[("patches", &patches), ("pos_embed", &pos_embed)]);
282        let output = outs
283            .into_iter()
284            .next()
285            .ok_or_else(|| anyhow::anyhow!("reve graph produced no output"))?;
286        Ok(ReveOutput {
287            output,
288            shape: vec![s, d],
289            n_channels,
290        })
291    }
292
293    pub fn run_one(
294        &mut self,
295        signal: Vec<f32>,
296        positions_xyz: Vec<f32>,
297        n_channels: usize,
298        n_times: usize,
299    ) -> anyhow::Result<ReveOutput> {
300        let (patches, pos4) = self.prep_inputs(signal, &positions_xyz, n_channels, n_times)?;
301        let s = pos4.len() / 4;
302        let d = self.model_cfg.embed_dim;
303        let pos_embed = pos_embed::precompute_pos_embed(&pos4, s, d, &self.params);
304        let attention_pooling = self.model_cfg.attention_pooling;
305        let cls_q = self.cls_query_token.clone();
306        let compiled = self.compiled_for(1, s);
307
308        let outs = if attention_pooling {
309            let q = cls_q.as_ref().expect("cls token loaded");
310            compiled.run(&[
311                ("patches", &patches),
312                ("pos_embed", &pos_embed),
313                ("cls_q", q),
314            ])
315        } else {
316            compiled.run(&[("patches", &patches), ("pos_embed", &pos_embed)])
317        };
318
319        let output = outs
320            .into_iter()
321            .next()
322            .ok_or_else(|| anyhow::anyhow!("reve graph produced no output"))?;
323
324        Ok(ReveOutput {
325            output,
326            shape: if self.model_cfg.n_outputs == 0 {
327                vec![self.model_cfg.embed_dim]
328            } else {
329                vec![self.model_cfg.n_outputs]
330            },
331            n_channels,
332        })
333    }
334}