File size: 13,643 Bytes
eae424a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
//! The inference worker: a thread that owns the meganeura session and
//! publishes coloured feature grids for the renderer to display.
//!
//! # Why a thread at all
//!
//! The encoder is expected to take 40–60 ms on a Quest 3 while the
//! compositor wants a frame every 13.9 ms. Running inference inline would
//! put the render loop on the encoder's cadence, which is uncomfortable at
//! best. Instead the renderer runs free, always drawing the most recent
//! completed result, and inference lands whenever it lands.
//!
//! # Why the worker builds its own session
//!
//! `meganeura::Session` is not `Send` — it owns a `CommandEncoder` holding
//! raw Vulkan handles. It cannot be constructed here and moved. What *is*
//! shareable is `Arc<blade_graphics::Context>`, so the worker receives the
//! context and builds the session in place. See `tests/threading.rs`.
//!
//! # The caveat this design cannot fix
//!
//! Both threads submit to the same Vulkan queue, which blade guards with a
//! mutex. A long compute submission can still delay the render submission
//! queued behind it — threading decouples *CPU* orchestration, not GPU
//! occupancy. Whether that shows up as dropped frames is exactly what the
//! on-device numbers will reveal; if it does, the fix is splitting the plan
//! across frames, which needs meganeura to grow partial execution
//! (`step()` currently submits every dispatch in one go).

use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::time::Instant;

use crate::dinov3::Config;
use crate::pca::{self, Basis};

/// A completed inference result: a square RGB image, either the coarse
/// feature colouring or a full reconstruction.
#[derive(Debug, Clone)]
pub struct FeatureGrid {
    /// Row-major RGB. For [`Display::PcaColour`] this is `[tokens, 3]`
    /// including the prefix tokens; for [`Display::Reconstruction`] it is
    /// already `[grid * grid, 3]` with no prefix.
    pub colors: Vec<f32>,
    /// Side length of the displayable square: the patch grid for PCA
    /// colouring, the image size for a reconstruction.
    pub grid: usize,
    pub prefix_tokens: usize,
    /// Encoder wall time, for the on-screen/logged rate readout.
    pub latency_ms: f64,
}

impl FeatureGrid {
    /// The displayable `grid × grid × 3` image, with any prefix tokens
    /// dropped.
    pub fn patch_rgb(&self) -> &[f32] {
        &self.colors[self.prefix_tokens * pca::COMPONENTS..]
    }
}

/// What the encoder's output is turned into for display.
#[derive(Clone, Debug)]
pub enum Display {
    /// Project patch features onto their top three principal components and
    /// read those off as RGB. No training, one `[hidden, 3]` matmul, and a
    /// `grid × grid` result. Shows what the features *distinguish*.
    PcaColour,
    /// Reconstruct RGB through the trained decoder — the actual roundtrip.
    /// Produces a full `image_size × image_size` picture, so the readback
    /// per frame is ~600 KB rather than ~3 KB. Shows what the features
    /// *retain*.
    Reconstruction(std::path::PathBuf),
}

/// What the worker is told to do next.
enum Command {
    /// Encode this image. `[num_patches, patch_dim]`, from
    /// [`crate::preprocess`].
    Submit(Vec<f32>),
    /// Refit the PCA basis from the next frame's features.
    Refit,
    Stop,
}

/// Handle to a running worker.
pub struct Worker {
    tx: mpsc::Sender<Command>,
    latest: Arc<Mutex<Option<FeatureGrid>>>,
    generation: Arc<AtomicU64>,
    ready: Arc<AtomicBool>,
    handle: Option<std::thread::JoinHandle<()>>,
}

impl Worker {
    /// Queue a frame for encoding.
    ///
    /// Returns `false` if the worker is busy or gone. Dropping frames is
    /// the correct behaviour: the camera produces 60 Hz and the encoder
    /// cannot keep up, so anything queued would be stale by the time it ran.
    pub fn submit(&self, patches: Vec<f32>) -> bool {
        if !self.ready.load(Ordering::Acquire) {
            return false;
        }
        self.ready.store(false, Ordering::Release);
        self.tx.send(Command::Submit(patches)).is_ok()
    }

    /// Ask for the colour basis to be refitted from the next frame.
    pub fn request_refit(&self) {
        let _ = self.tx.send(Command::Refit);
    }

    /// The most recent completed result, if any.
    pub fn latest(&self) -> Option<FeatureGrid> {
        self.latest.lock().ok().and_then(|g| g.clone())
    }

    /// Monotonic count of completed encodes, so the renderer can tell a
    /// fresh result from a repeat without comparing pixel data.
    pub fn generation(&self) -> u64 {
        self.generation.load(Ordering::Acquire)
    }

    /// True when the worker is idle and would accept a frame.
    pub fn is_ready(&self) -> bool {
        self.ready.load(Ordering::Acquire)
    }
}

impl Drop for Worker {
    fn drop(&mut self) {
        let _ = self.tx.send(Command::Stop);
        if let Some(handle) = self.handle.take() {
            let _ = handle.join();
        }
    }
}

/// How the worker should obtain its weights.
pub enum Weights {
    /// Load a checkpoint from disk. On device this is a path the app has
    /// pushed or unpacked from its assets.
    SafeTensors(std::path::PathBuf),
    /// Deterministic synthetic weights. Produces meaningless features, but
    /// exercises the entire pipeline end to end — useful for bringing the
    /// render path up before the real checkpoint is on the device.
    Synthetic,
}

/// Start the worker.
///
/// Returns immediately; the session is built on the worker thread, which
/// takes a few seconds on a mobile CPU. [`Worker::is_ready`] reports false
/// until it is done, and [`Worker::latest`] yields `None`.
pub fn spawn(
    gpu: Arc<blade_graphics::Context>,
    config: Config,
    weights: Weights,
    plan_cache: Option<std::path::PathBuf>,
    submission_chunks: usize,
    display: Display,
) -> Worker {
    let (tx, rx) = mpsc::channel();
    let latest = Arc::new(Mutex::new(None));
    let generation = Arc::new(AtomicU64::new(0));
    let ready = Arc::new(AtomicBool::new(false));

    let handle = {
        let latest = Arc::clone(&latest);
        let generation = Arc::clone(&generation);
        let ready = Arc::clone(&ready);
        std::thread::Builder::new()
            .name("dinovision-inference".into())
            .spawn(move || {
                worker_main(
                    gpu,
                    config,
                    weights,
                    plan_cache,
                    submission_chunks,
                    display,
                    rx,
                    latest,
                    generation,
                    ready,
                );
            })
            .expect("failed to spawn inference thread")
    };

    Worker {
        tx,
        latest,
        generation,
        ready,
        handle: Some(handle),
    }
}

#[allow(clippy::too_many_arguments)]
fn worker_main(
    gpu: Arc<blade_graphics::Context>,
    config: Config,
    weights: Weights,
    plan_cache: Option<std::path::PathBuf>,
    submission_chunks: usize,
    display: Display,
    rx: mpsc::Receiver<Command>,
    latest: Arc<Mutex<Option<FeatureGrid>>>,
    generation: Arc<AtomicU64>,
    ready: Arc<AtomicBool>,
) {
    // For PCA colouring both the colours and the features are outputs:
    // colours every frame (3 KB), features only when refitting the basis
    // (300 KB), which is why they are separate. A reconstruction needs
    // neither — the decoder consumes the features inside the graph.
    let reconstructing = matches!(display, Display::Reconstruction(_));
    let mut g = meganeura::Graph::new();
    let features = crate::dinov3::build_encoder(&mut g, &config);
    if reconstructing {
        let rgb = crate::decoder::attach_to_encoder(&mut g, &config, features);
        g.set_outputs(vec![rgb]);
    } else {
        let colors = pca::add_projection(&mut g, features, config.hidden_size);
        g.set_outputs(vec![colors, features]);
    }

    let build_start = Instant::now();
    let (mut session, _) = meganeura::train::build(&g, meganeura::train::SessionConfig {
        mode: meganeura::train::Mode::Inference,
        gpu: Some(gpu),
        cache: plan_cache.as_deref(),
        ..Default::default()
    });
    log::info!(
        "inference session ready in {:.1} s",
        build_start.elapsed().as_secs_f64()
    );

    // Hand the queue back periodically so the renderer can get a frame in.
    // One long submission is faster in isolation but starves everything
    // else sharing the device.
    session.set_submission_chunks(submission_chunks);

    match weights {
        Weights::SafeTensors(path) => {
            match meganeura::data::safetensors::SafeTensorsModel::load(path.clone()) {
                Ok(model) => {
                    if let Err(e) = crate::weights::load_encoder(&mut session, &model, &config) {
                        log::error!("failed to bind weights from {}: {e}", path.display());
                        return;
                    }
                }
                Err(e) => {
                    log::error!("failed to read {}: {e}", path.display());
                    return;
                }
            }
        }
        Weights::Synthetic => {
            log::warn!("running with synthetic weights — features are meaningless");
            crate::bench::fill_parameters(&mut session, &g);
        }
    }

    let tokens = config.num_tokens();
    let mut basis = Basis::placeholder(config.hidden_size);
    let mut refit_pending = false;
    let mut colors_out;
    let mut features_out = vec![0.0f32; tokens * config.hidden_size];
    // Reconstruction reads a planar [3, H, W] image; the renderer wants it
    // interleaved, so keep a staging buffer for the transpose.
    let mut planar = Vec::new();

    match &display {
        Display::PcaColour => {
            // Start with a placeholder basis so the first frame displays
            // something, then refit from real features as soon as one lands.
            refit_pending = true;
            apply_basis(&mut session, &basis);
            colors_out = vec![0.0f32; tokens * pca::COMPONENTS];
        }
        Display::Reconstruction(path) => {
            if let Err(e) = crate::decoder::load_parameters(&mut session, &g, path) {
                log::error!("failed to load the decoder: {e}");
                return;
            }
            let pixels = config.image_size * config.image_size;
            planar = vec![0.0f32; 3 * pixels];
            colors_out = vec![0.0f32; pixels * 3];
        }
    }

    ready.store(true, Ordering::Release);

    while let Ok(cmd) = rx.recv() {
        let patches = match cmd {
            Command::Submit(p) => p,
            Command::Refit => {
                refit_pending = true;
                continue;
            }
            Command::Stop => break,
        };

        let start = Instant::now();
        session.set_input("patches", &patches);
        session.step();
        session.wait();

        if reconstructing {
            session.read_output_by_index(0, &mut planar);
            // [3, H, W] → [H * W, 3].
            let pixels = config.image_size * config.image_size;
            for p in 0..pixels {
                for c in 0..3 {
                    colors_out[p * 3 + c] = planar[c * pixels + p];
                }
            }
        } else {
            session.read_output_by_index(0, &mut colors_out);
        }

        if refit_pending {
            // Refitting needs the full feature matrix, so this frame pays
            // for the larger readback. It happens once at startup and then
            // only on request.
            session.read_output_by_index(1, &mut features_out);
            basis = Basis::fit(
                &features_out,
                tokens,
                config.hidden_size,
                config.num_prefix_tokens(),
            );
            apply_basis(&mut session, &basis);
            refit_pending = false;
            log::info!("refitted colour basis from live features");
            // The colours just read were produced by the old basis; redo
            // the projection on the CPU so this frame is not displayed with
            // a stale palette.
            colors_out = basis.project(&features_out, tokens);
        }

        let grid = FeatureGrid {
            colors: colors_out.clone(),
            grid: if reconstructing {
                config.image_size
            } else {
                config.grid()
            },
            prefix_tokens: if reconstructing {
                0
            } else {
                config.num_prefix_tokens()
            },
            latency_ms: start.elapsed().as_secs_f64() * 1000.0,
        };
        if let Ok(mut slot) = latest.lock() {
            *slot = Some(grid);
        }
        generation.fetch_add(1, Ordering::Release);
        ready.store(true, Ordering::Release);
    }

    log::info!("inference worker stopped");
}

fn apply_basis(session: &mut meganeura::Session, basis: &Basis) {
    session.set_parameter("pca.weight", &basis.weight_matrix());
    session.set_parameter("pca.bias", &basis.bias_vector());
}