1use 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#[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#[derive(Clone, Copy, Debug)]
27pub struct RunEpochOpts {
28 pub normalize: bool,
30}
31
32impl Default for RunEpochOpts {
33 fn default() -> Self {
34 Self { normalize: true }
35 }
36}
37
38pub 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 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 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 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 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}