1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::Instant;
7
8use crate::config::{ModelConfig, ModelSize};
9use crate::error::{EegDinoError, Result};
10
11use super::graph::{build_encoder_graph, EncoderSpec};
12use super::weights::{
13 apply_params, detect_model_size as detect_size_anyhow, load_safetensors, prepare_params,
14 ParamMap,
15};
16
17struct CachedEntry {
19 compiled: rlx::CompiledGraph,
20 x_buf: Vec<f32>,
21 out_buf: Vec<f32>,
22 out_slots: Vec<(usize, usize)>,
24 host_run: bool,
26}
27
28fn read_output_into(compiled: &rlx::CompiledGraph, slots: &[(usize, usize)], out: &mut Vec<f32>) {
30 let (byte_off, len) = slots
31 .first()
32 .copied()
33 .expect("encoder graph has one output");
34 let ptr = unsafe { compiled.arena_ptr().add(byte_off) as *const f32 };
35 let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
37 if out.len() != len {
38 out.resize(len, 0.0);
39 }
40 out.copy_from_slice(slice);
41}
42
43fn run_host(entry: &mut CachedEntry) {
44 let v = entry
45 .compiled
46 .run(&[("x", entry.x_buf.as_slice())])
47 .into_iter()
48 .next()
49 .expect("encoder graph has one output");
50 if entry.out_buf.len() != v.len() {
51 entry.out_buf = v;
52 } else {
53 entry.out_buf.copy_from_slice(&v);
54 }
55}
56
57fn run_forward(entry: &mut CachedEntry) {
58 let x = entry.x_buf.as_slice();
59 if entry.host_run {
60 run_host(entry);
61 return;
62 }
63 if entry.out_slots.is_empty() {
64 if entry.compiled.arena_ptr().is_null() {
65 entry.host_run = true;
66 run_host(entry);
67 return;
68 }
69 entry.out_slots = entry.compiled.run_slots(&[x]).to_vec();
70 if entry.out_slots.is_empty() {
71 entry.host_run = true;
72 run_host(entry);
73 return;
74 }
75 } else {
76 entry.compiled.run_slots(&[x]);
77 }
78 read_output_into(&entry.compiled, &entry.out_slots, &mut entry.out_buf);
79}
80
81pub struct EncodingResult {
83 pub embeddings: Vec<f32>,
84 pub shape: Vec<usize>,
85 pub ms_encode: f64,
86}
87
88const DEFAULT_MAX_CACHED_CPU: usize = usize::MAX;
91const DEFAULT_MAX_CACHED_GPU: usize = 1;
92
93pub struct EegDinoEncoderBuilder {
94 weights_path: Option<PathBuf>,
95 config: Option<ModelConfig>,
96 normalization: f32,
97 device: Option<rlx::Device>,
98 max_cached_shapes: Option<usize>,
99}
100
101impl Default for EegDinoEncoderBuilder {
102 fn default() -> Self {
103 Self {
104 weights_path: None,
105 config: None,
106 normalization: 100.0,
107 device: None,
108 max_cached_shapes: None,
109 }
110 }
111}
112
113fn default_max_cached_shapes(device: rlx::Device) -> usize {
114 match device {
115 rlx::Device::Cpu => DEFAULT_MAX_CACHED_CPU,
116 rlx::Device::Cuda | rlx::Device::Gpu | rlx::Device::Rocm => DEFAULT_MAX_CACHED_GPU,
117 _ => 8,
118 }
119}
120
121impl EegDinoEncoderBuilder {
122 pub fn weights(mut self, path: impl Into<PathBuf>) -> Self {
123 self.weights_path = Some(path.into());
124 self
125 }
126
127 pub fn size(mut self, size: ModelSize) -> Self {
128 self.config = Some(ModelConfig::from_size(size));
129 self
130 }
131
132 pub fn config(mut self, cfg: ModelConfig) -> Self {
133 self.config = Some(cfg);
134 self
135 }
136
137 pub fn normalization(mut self, n: f32) -> Self {
138 self.normalization = n;
139 self
140 }
141
142 pub fn device(mut self, device: rlx::Device) -> Self {
143 self.device = Some(device);
144 self
145 }
146
147 pub fn max_cached_shapes(mut self, n: usize) -> Self {
150 self.max_cached_shapes = Some(n.max(1));
151 self
152 }
153
154 pub fn build(self) -> Result<EegDinoEncoder> {
155 let weights_path = self
156 .weights_path
157 .ok_or_else(|| EegDinoError::Builder("weights path is required".into()))?;
158 let device = self
159 .device
160 .ok_or_else(|| EegDinoError::Builder("device is required".into()))?;
161
162 let path_str = weights_path
163 .to_str()
164 .ok_or_else(|| EegDinoError::Builder("weights path is not valid UTF-8".into()))?;
165
166 let cfg = match self.config {
167 Some(c) => c,
168 None => {
169 let size = detect_size_anyhow(path_str)
170 .map_err(|e| EegDinoError::WeightLoad(e.to_string()))?;
171 ModelConfig::from_size(size)
172 }
173 };
174
175 let raw =
176 load_safetensors(path_str).map_err(|e| EegDinoError::WeightLoad(e.to_string()))?;
177 let params = Arc::new(
178 prepare_params(&cfg, raw).map_err(|e| EegDinoError::WeightLoad(e.to_string()))?,
179 );
180
181 let max_cached_shapes = self
182 .max_cached_shapes
183 .unwrap_or_else(|| default_max_cached_shapes(device));
184
185 Ok(EegDinoEncoder {
186 cfg,
187 device,
188 normalization: self.normalization,
189 session: rlx::Session::new(device),
190 params,
191 cache: HashMap::new(),
192 batch_flat: Vec::new(),
193 max_cached_shapes,
194 })
195 }
196}
197
198pub struct EegDinoEncoder {
200 pub cfg: ModelConfig,
201 pub device: rlx::Device,
202 pub normalization: f32,
203
204 session: rlx::Session,
205 params: Arc<ParamMap>,
206 cache: HashMap<u64, CachedEntry>,
207 batch_flat: Vec<f32>,
209 max_cached_shapes: usize,
210}
211
212impl EegDinoEncoder {
213 pub fn builder() -> EegDinoEncoderBuilder {
214 EegDinoEncoderBuilder::default()
215 }
216
217 pub fn load(
218 weights_path: &Path,
219 config: Option<ModelConfig>,
220 device: rlx::Device,
221 ) -> Result<(Self, f64)> {
222 let t0 = Instant::now();
223 let mut b = Self::builder().weights(weights_path).device(device);
224 if let Some(c) = config {
225 b = b.config(c);
226 }
227 let enc = b.build()?;
228 Ok((enc, t0.elapsed().as_secs_f64() * 1000.0))
229 }
230
231 fn cache_key(spec: &EncoderSpec) -> u64 {
232 ((spec.b as u64) << 42) ^ ((spec.c as u64) << 21) ^ (spec.p as u64)
233 }
234
235 fn evict_cache_if_needed(&mut self, incoming: u64) {
236 if self.cache.contains_key(&incoming) {
237 return;
238 }
239 if self.cache.len() < self.max_cached_shapes {
240 return;
241 }
242 if self.max_cached_shapes == 1 {
243 self.cache.clear();
244 return;
245 }
246 if let Some(k) = self.cache.keys().next().copied() {
248 self.cache.remove(&k);
249 }
250 if self.cache.len() >= self.max_cached_shapes {
251 self.cache.clear();
252 }
253 }
254
255 pub fn clear_cache(&mut self) {
257 self.cache.clear();
258 self.batch_flat.shrink_to_fit();
260 }
261
262 fn entry_for(&mut self, spec: &EncoderSpec, input_len: usize) -> Result<&mut CachedEntry> {
263 let key = Self::cache_key(spec);
264 if !self.cache.contains_key(&key) {
265 self.evict_cache_if_needed(key);
266 let graph = build_encoder_graph(&self.cfg, spec);
267 let mut compiled = self.session.compile(graph);
268 apply_params(&mut compiled, &self.cfg, spec, &self.params)
269 .map_err(|e| EegDinoError::WeightLoad(e.to_string()))?;
270 let mut x_buf = Vec::with_capacity(input_len);
271 x_buf.resize(input_len, 0.0);
272 let out_buf = Vec::new();
273 let mut entry = CachedEntry {
274 compiled,
275 x_buf,
276 out_buf,
277 out_slots: Vec::new(),
278 host_run: false,
279 };
280 run_forward(&mut entry);
281 self.cache.insert(key, entry);
282 }
283 let entry = self.cache.get_mut(&key).expect("just inserted");
284 if entry.x_buf.len() != input_len {
285 entry.x_buf.resize(input_len, 0.0);
286 }
287 Ok(entry)
288 }
289
290 pub fn prewarm_batch_sizes(
292 &mut self,
293 batch_sizes: &[usize],
294 num_channels: usize,
295 num_samples: usize,
296 ) -> Result<()> {
297 for &b in batch_sizes {
298 if self.max_cached_shapes == 1 {
299 self.cache.clear();
300 }
301 let expected = b * num_channels * num_samples;
302 let spec = EncoderSpec {
303 b,
304 c: num_channels,
305 p: num_samples / self.cfg.patch_size,
306 };
307 self.entry_for(&spec, expected)?;
308 }
309 Ok(())
310 }
311
312 fn validate_encode_input(
313 &self,
314 signal: &[f32],
315 batch_size: usize,
316 num_channels: usize,
317 num_samples: usize,
318 ) -> Result<(usize, EncoderSpec)> {
319 let patch_size = self.cfg.patch_size;
320 if num_channels != self.cfg.num_channels {
321 return Err(EegDinoError::InvalidInput(format!(
322 "num_channels ({num_channels}) must equal model num_channels ({})",
323 self.cfg.num_channels
324 )));
325 }
326 if !num_samples.is_multiple_of(patch_size) {
327 return Err(EegDinoError::InvalidInput(format!(
328 "num_samples ({num_samples}) must be divisible by patch_size ({patch_size})"
329 )));
330 }
331 let expected = batch_size * num_channels * num_samples;
332 if signal.len() != expected {
333 return Err(EegDinoError::InvalidInput(format!(
334 "signal length {} != batch_size({batch_size}) * channels({num_channels}) * samples({num_samples}) = {expected}",
335 signal.len()
336 )));
337 }
338 let spec = EncoderSpec {
339 b: batch_size,
340 c: num_channels,
341 p: num_samples / patch_size,
342 };
343 Ok((expected, spec))
344 }
345
346 fn output_shape(
347 &self,
348 batch_size: usize,
349 num_channels: usize,
350 num_patches: usize,
351 ) -> Vec<usize> {
352 vec![
353 batch_size,
354 self.cfg.num_global_tokens + num_channels * num_patches,
355 self.cfg.feature_size,
356 ]
357 }
358
359 pub fn encode_raw(
364 &mut self,
365 signal: &[f32],
366 batch_size: usize,
367 num_channels: usize,
368 num_samples: usize,
369 ) -> Result<EncodingResult> {
370 let t0 = Instant::now();
371 let (expected, spec) =
372 self.validate_encode_input(signal, batch_size, num_channels, num_samples)?;
373
374 let inv_norm = 1.0f32 / self.normalization;
375 let entry = self.entry_for(&spec, expected)?;
376 for (dst, &v) in entry.x_buf.iter_mut().zip(signal) {
377 *dst = v * inv_norm;
378 }
379 run_forward(entry);
380 let embeddings = std::mem::take(&mut entry.out_buf);
381
382 Ok(EncodingResult {
383 embeddings,
384 shape: self.output_shape(batch_size, num_channels, spec.p),
385 ms_encode: t0.elapsed().as_secs_f64() * 1000.0,
386 })
387 }
388
389 pub fn encode_raw_into(
391 &mut self,
392 signal: &[f32],
393 batch_size: usize,
394 num_channels: usize,
395 num_samples: usize,
396 out: &mut Vec<f32>,
397 ) -> Result<(Vec<usize>, f64)> {
398 let t0 = Instant::now();
399 let (expected, spec) =
400 self.validate_encode_input(signal, batch_size, num_channels, num_samples)?;
401
402 let inv_norm = 1.0f32 / self.normalization;
403 let gtok = self.cfg.num_global_tokens;
404 let feat = self.cfg.feature_size;
405 let entry = self.entry_for(&spec, expected)?;
406 for (dst, &v) in entry.x_buf.iter_mut().zip(signal) {
407 *dst = v * inv_norm;
408 }
409 run_forward(entry);
410 let shape = [batch_size, gtok + num_channels * spec.p, feat];
411 let out_len = entry.out_buf.len();
412 if out.len() == out_len {
413 out.copy_from_slice(&entry.out_buf);
414 entry.out_buf.clear();
415 } else {
416 *out = std::mem::take(&mut entry.out_buf);
417 }
418 Ok((shape.to_vec(), t0.elapsed().as_secs_f64() * 1000.0))
419 }
420
421 pub fn encode_batch(
423 &mut self,
424 signals: &[Vec<f32>],
425 num_channels: usize,
426 num_samples: usize,
427 ) -> Result<EncodingResult> {
428 let expected_len = num_channels * num_samples;
429 let mut flat = std::mem::take(&mut self.batch_flat);
430 flat.clear();
431 flat.reserve(signals.len() * expected_len);
432 for (i, s) in signals.iter().enumerate() {
433 if s.len() != expected_len {
434 self.batch_flat = flat;
435 return Err(EegDinoError::InvalidInput(format!(
436 "signal[{i}] length {} != {expected_len}",
437 s.len()
438 )));
439 }
440 flat.extend_from_slice(s);
441 }
442 let mut embeddings = Vec::new();
443 let (shape, ms_encode) = self.encode_raw_into(
444 &flat,
445 signals.len(),
446 num_channels,
447 num_samples,
448 &mut embeddings,
449 )?;
450 self.batch_flat = flat;
451 Ok(EncodingResult {
452 embeddings,
453 shape,
454 ms_encode,
455 })
456 }
457}