Skip to main content

cortiq_engine/
sam3.rs

1//! SAM3 image inference on CPU (f32) over the sections written by
2//! `cortiq-cli sam3-pack` (`trunk.*`, `fpn.*`, `text.*`, `det.*`).
3//!
4//! Semantics follow transformers 5.20.2 `Sam3Model` (fixtures: run
5//! `tools/sam3_ref.py` on the pod): CLIP-L text tower + resizer, ViT trunk
6//! with windowed axial RoPE (per-layer baked freqs_cis), SAM2-style FPN
7//! neck, DETR encoder/decoder (presence token + 200 queries + log
8//! box-RPB, POST-norm decoder layers) and the mask decoder. Geometry
9//! prompts are not part of the image path. Batch size 1; `pixels` is a
10//! preprocessed [3,1008,1008] tensor.
11
12use std::sync::Arc;
13
14use anyhow::{anyhow, Context, Result};
15use cortiq_core::quant::dequant_tensor;
16use cortiq_core::{CmfModel, TensorDtype};
17
18use crate::inference::gelu_erf;
19use crate::pool::Pool;
20use crate::qtensor::QTensor;
21
22/// Additive mask for "key is padded": saturates softmax to 0.
23const NEG: f32 = -1e30;
24
25/// The vendor image preprocess, exactly as `cfg.processor` spells it:
26/// resize to 1008×1008 with `resample: 2` (bilinear), rescale 1/255,
27/// normalize with mean/std 0.5 per channel, emit CHW f32. This produces
28/// the parity fixtures' `pixels.f32` from the source image.
29pub fn preprocess(frame: &crate::media::RgbFrame) -> Vec<f32> {
30    use image::imageops::{self, FilterType};
31    let img = image::RgbImage::from_raw(
32        frame.width as u32,
33        frame.height as u32,
34        frame.data.clone(),
35    )
36    .expect("RgbFrame buffer matches its own dimensions");
37    let rs = imageops::resize(&img, PIX as u32, PIX as u32, FilterType::Triangle);
38    let raw = rs.into_raw();
39    let mut out = vec![0f32; 3 * PIX * PIX];
40    for i in 0..PIX * PIX {
41        for c in 0..3 {
42            out[c * PIX * PIX + i] = (raw[i * 3 + c] as f32 / 255.0 - 0.5) / 0.5;
43        }
44    }
45    out
46}
47
48const PIX: usize = 1008;
49const PATCH: usize = 14;
50const GRID: usize = PIX / PATCH; // 72
51const WIN: usize = 24;
52const TD: usize = 1024;
53const TH: usize = 16;
54const TL: usize = 32;
55const TI: usize = 4736;
56const T_EPS: f32 = 1e-6;
57const GLOBAL: [usize; 4] = [7, 15, 23, 31];
58
59const SS: usize = 32;
60const SD: usize = 1024;
61const SL: usize = 24;
62const SI: usize = 4096;
63const S_EPS: f32 = 1e-5;
64const PROJ: usize = 512;
65
66const FD: usize = 256;
67const FI: usize = 2048;
68const FH: usize = 8;
69const D_EPS: f32 = 1e-6;
70const NL: usize = 6;
71const NQ: usize = 200;
72const MH: usize = 288;
73
74/// One forward's outputs (batch 1), flat row-major f32.
75pub struct Sam3Out {
76    /// CLIP text projection of the pooled token, [512].
77    pub text_embeds: Vec<f32>,
78    /// Resizer output fed to the DETR stack, [32 * 256].
79    pub text_feats: Vec<f32>,
80    /// Trunk features, [5184 * 1024].
81    pub trunk_feats: Vec<f32>,
82    /// FPN levels 288/144/72/36, each [256 * H * W].
83    pub fpn: Vec<Vec<f32>>,
84    /// DETR encoder output, [5184 * 256].
85    pub enc_hidden: Vec<f32>,
86    /// Stacked decoder layer outputs, [6 * 200 * 256].
87    pub dec_hidden: Vec<f32>,
88    /// Final boxes (xyxy), [200 * 4].
89    pub pred_boxes: Vec<f32>,
90    /// Instance masks, [200 * 288 * 288].
91    pub pred_masks: Vec<f32>,
92    /// Per-query classification logits from the dot-product scorer
93    /// (`det.scoring.*`), [200] — the final score of a query is
94    /// `sigmoid(pred_logits) * sigmoid(presence_logit)` (HF's
95    /// `post_process_object_detection`). Empty when `SAM3_ONLY`
96    /// skips the decoder.
97    pub pred_logits: Vec<f32>,
98    /// Presence-token logit of the final decoder layer (clamped ±10).
99    /// 0.0 when the decoder did not run.
100    pub presence_logit: f32,
101}
102
103/// Additive attention masks (added to scores before softmax).
104#[derive(Clone, Copy)]
105pub(crate) enum Mask<'a> {
106    None,
107    /// One value per key, shared by all queries/heads (padding).
108    Key(&'a [f32]),
109    /// [n_q, n_k] matrix, shared across heads (CLIP causal).
110    Add(&'a [f32]),
111    /// [heads, n_q, n_k] per-head bias (box RPB).
112    Head(&'a [f32]),
113}
114
115pub struct Sam3 {
116    model: Arc<CmfModel>,
117    pool: Option<Arc<Pool>>,
118    /// `fpn.convs` (image detector neck) or `fpn.sam2_convs` (SAM3_FPN).
119    fpn_prefix: &'static str,
120    /// `trunk.pos_embed` is [1,577,1024]: Meta's leading cls row is
121    /// dropped by the HF conversion (SAM3_POS=first keeps rows 0..576).
122    pos_skip_cls: bool,
123}
124
125impl Sam3 {
126    pub fn open(path: &str) -> Result<Self> {
127        let model = CmfModel::open(path)
128            .with_context(|| format!("open {path}"))?
129            .into();
130        let fpn_prefix = match std::env::var("SAM3_FPN").as_deref() {
131            Ok("sam2_convs") => "fpn.sam2_convs",
132            _ => "fpn.convs",
133        };
134        let pos_skip_cls = std::env::var("SAM3_POS").as_deref() != Ok("first");
135        Ok(Self { model, pool: Pool::from_env(), fpn_prefix, pos_skip_cls })
136    }
137
138    /// The shared CMF handle (the video tracker reads its `trk.*` section
139    /// through it).
140    pub(crate) fn cmf(&self) -> &Arc<CmfModel> { &self.model }
141
142    /// The GEMM thread pool the detector was opened with.
143    pub(crate) fn pool_handle(&self) -> Option<&Arc<Pool>> { self.pool.as_ref() }
144
145    /// Debug: `"name dtype shape"` lines for every tensor in the CMF
146    /// directory (the parity tests filter them by section).
147    pub fn directory(&self) -> Vec<String> {
148        self.model
149            .tensors
150            .iter()
151            .map(|t| format!("{} {:?} {:?}", t.name, t.dtype, t.shape))
152            .collect()
153    }
154
155    /// Debug: raw payload bytes of one directory tensor.
156    pub fn raw_tensor(&self, name: &str) -> Option<Vec<u8>> {
157        let e = self.model.tensor(name)?;
158        Some(self.model.entry_bytes(e).to_vec())
159    }
160
161    /// Debug: every `q8_row` row scale and every `f16` payload element
162    /// whose bit pattern is non-finite, as `"name@idx bad=0x…"` lines.
163    pub fn bad_scales(&self) -> Vec<String> {
164        let f16_bad = |h: u16| h & 0x7C00 == 0x7C00;
165        let mut out = Vec::new();
166        for t in self.model.tensors.iter() {
167            let bytes = self.model.entry_bytes(t);
168            match t.dtype {
169                TensorDtype::Q8Row if t.shape.len() == 2 => {
170                    let (rows, cols) = (t.shape[0], t.shape[1]);
171                    let off = rows * cols;
172                    if bytes.len() < off + rows * 2 {
173                        out.push(format!("{}@payload short", t.name));
174                        continue;
175                    }
176                    for r in 0..rows {
177                        let h = u16::from_le_bytes([bytes[off + r * 2], bytes[off + r * 2 + 1]]);
178                        if f16_bad(h) {
179                            out.push(format!("{}@row{r} scale_f16=0x{h:04x}", t.name));
180                        }
181                    }
182                }
183                TensorDtype::F16 => {
184                    for (i, c) in bytes.chunks_exact(2).enumerate() {
185                        let h = u16::from_le_bytes([c[0], c[1]]);
186                        if f16_bad(h) {
187                            out.push(format!("{}@{i} val_f16=0x{h:04x}", t.name));
188                            if out.len() > 200 { return out; }
189                        }
190                    }
191                }
192                _ => {}
193            }
194        }
195        out
196    }
197
198    /// `pixels` [3,1008,1008] preprocessed; `ids`/`attn` length 32
199    /// (Sam3Processor, padding to max_length=32).
200    ///
201    /// Debug knobs (unset in normal runs): `SAM3_ONLY=trunk,...` runs only
202    /// the listed stages and returns empty tensors for the rest (the
203    /// parity test skips while it is set); `SAM3_DUMP_DIR` makes `trunk`
204    /// write its stage outputs (`trunk_patch.f32`, `trunk_pre.f32`,
205    /// `trunk_b{l}.f32`) for reference bisection; `SAM3_TEXT_DEBUG=1`
206    /// prints per-stage finiteness stats of the text tower.
207    pub fn forward(&mut self, pixels: &[f32], ids: &[u32], attn: &[u8]) -> Result<Sam3Out> {
208        if pixels.len() != 3 * PIX * PIX {
209            return Err(anyhow!("pixels {} != 3*{}*{}", pixels.len(), PIX, PIX));
210        }
211        if ids.len() != SS || attn.len() != SS {
212            return Err(anyhow!("ids/attn must be {SS} long"));
213        }
214        let only = std::env::var("SAM3_ONLY").ok();
215        let want = |k: &str| {
216            only
217                .as_deref()
218                .map_or(true, |o| o.split(',').any(|s| s.trim() == k))
219        };
220        let pool = self.pool.as_deref();
221        let (text_embeds, text_feats) = self.text(ids, attn, pool)?;
222        let blank = |te: Vec<f32>, tf: Vec<f32>| Sam3Out {
223            text_embeds: te,
224            text_feats: tf,
225            trunk_feats: Vec::new(),
226            fpn: Vec::new(),
227            enc_hidden: Vec::new(),
228            dec_hidden: Vec::new(),
229            pred_boxes: Vec::new(),
230            pred_masks: Vec::new(),
231            pred_logits: Vec::new(),
232            presence_logit: 0.0,
233        };
234        if !want("trunk") {
235            return Ok(blank(text_embeds, text_feats));
236        }
237        let trunk_feats = self.trunk(pixels, pool)?;
238        if !want("fpn") {
239            let mut o = blank(text_embeds, text_feats);
240            o.trunk_feats = trunk_feats;
241            return Ok(o);
242        }
243        let fpn = self.fpn(&trunk_feats, pool)?;
244        let pos = sine_pos(GRID, GRID, FD / 2); // level-2 (72) encoding, rows
245        let vision = chw_to_rows(&fpn[2], GRID, GRID);
246        let enc_hidden = self.detr_encoder(&vision, &pos, &text_feats, attn, pool)?;
247        if !want("dec") {
248            let mut o = blank(text_embeds, text_feats);
249            o.trunk_feats = trunk_feats;
250            o.fpn = fpn;
251            o.enc_hidden = enc_hidden;
252            return Ok(o);
253        }
254        let (dec_hidden, final_refs, presence_logit) =
255            self.detr_decoder(&enc_hidden, &pos, &text_feats, attn, pool)?;
256        // Dot-product scoring (det.scoring.*): the FINAL layer's DETR
257        // queries against the mean-pooled prompt features. Logits, not
258        // probabilities — the caller multiplies both sigmoids, as HF's
259        // post-processor does (`all_pred_logits[-1]`).
260        let pred_logits = self.score(
261            &text_feats,
262            attn,
263            &dec_hidden[(NL - 1) * NQ * FD..],
264            pool,
265        )?;
266        if !want("masks") {
267            let mut o = blank(text_embeds, text_feats);
268            o.trunk_feats = trunk_feats;
269            o.fpn = fpn;
270            o.enc_hidden = enc_hidden;
271            o.dec_hidden = dec_hidden.clone();
272            o.pred_boxes = xyxy(&final_refs);
273            o.pred_logits = pred_logits;
274            o.presence_logit = presence_logit;
275            return Ok(o);
276        }
277        let pred_boxes = xyxy(&final_refs);
278        let pred_masks = self.mask_decoder(
279            &dec_hidden[(NL - 1) * NQ * FD..],
280            &enc_hidden,
281            &fpn,
282            &text_feats,
283            attn,
284            pool,
285        )?;
286        Ok(Sam3Out {
287            text_embeds,
288            text_feats,
289            trunk_feats,
290            fpn,
291            enc_hidden,
292            dec_hidden,
293            pred_boxes,
294            pred_masks,
295            pred_logits,
296            presence_logit,
297        })
298    }
299
300    // ── text tower (CLIP-L, pre-norm transformer) ───────────────────
301
302    fn text(&self, ids: &[u32], attn: &[u8], pool: Option<&Pool>) -> Result<(Vec<f32>, Vec<f32>)> {
303        let m = &self.model;
304        let tok = lin(m, "text.encoder.token_embedding.weight")?;
305        let pos = load_vec(m, "text.encoder.positional_embedding")?;
306        let mut x = vec![0f32; SS * SD];
307        let mut row = vec![0f32; SD];
308        for (i, &id) in ids.iter().enumerate() {
309            tok.row_f32(id as usize, &mut row);
310            let r = &mut x[i * SD..(i + 1) * SD];
311            r.copy_from_slice(&row);
312            for (v, p) in r.iter_mut().zip(&pos[i * SD..(i + 1) * SD]) {
313                *v += *p;
314            }
315        }
316        dbg_stats("text.emb", &x);
317        // causal + padding additive mask [SS, SS]
318        let mut cm = vec![0f32; SS * SS];
319        for q in 0..SS {
320            for k in 0..=q {
321                if attn[k] == 0 {
322                    cm[q * SS + k] = NEG;
323                }
324            }
325            for k in (q + 1)..SS {
326                cm[q * SS + k] = NEG;
327            }
328        }
329        for l in 0..SL {
330            let b = format!("text.encoder.transformer.resblocks.{l}.");
331            let (lw, lb) = (load_vec(m, &format!("{b}ln_1.weight"))?, load_vec(m, &format!("{b}ln_1.bias"))?);
332            // Pre-norm: branch reads a normalized CLONE, x keeps the residual.
333            let mut h = x.clone();
334            layer_norm_rows(&mut h, &lw, &lb, S_EPS);
335            let qkv = lin(m, &format!("{b}attn.in_proj_weight"))?;
336            let ib = load_vec(m, &format!("{b}attn.in_proj_bias"))?;
337            let op = lin(m, &format!("{b}attn.out_proj.weight"))?;
338            let ob = load_vec(m, &format!("{b}attn.out_proj.bias"))?;
339            let mut ao = vec![0f32; SS * SD];
340            run_attn(
341                &qkv, &ib, &op, &ob,
342                &h, &h, &h, SS, SS, SD, 16, None, Mask::Add(&cm), pool, &mut ao,
343            );
344            for (a, c) in x.iter_mut().zip(&ao) { *a += *c; }
345            let (lw, lb) = (load_vec(m, &format!("{b}ln_2.weight"))?, load_vec(m, &format!("{b}ln_2.bias"))?);
346            let mut h = x.clone();
347            layer_norm_rows(&mut h, &lw, &lb, S_EPS);
348            let f1 = lin(m, &format!("{b}mlp.c_fc.weight"))?;
349            let f1b = load_vec(m, &format!("{b}mlp.c_fc.bias"))?;
350            let f2 = lin(m, &format!("{b}mlp.c_proj.weight"))?;
351            let f2b = load_vec(m, &format!("{b}mlp.c_proj.bias"))?;
352            let mut ff = vec![0f32; SS * SI];
353            f1.matmat(&h, SS, &mut ff, pool);
354            add_bias(&mut ff, SI, &f1b);
355            ff.iter_mut().for_each(|v| *v = gelu_erf(*v));
356            let mut fo = vec![0f32; SS * SD];
357            f2.matmat(&ff, SS, &mut fo, pool);
358            add_bias(&mut fo, SD, &f2b);
359            for (a, c) in x.iter_mut().zip(&fo) { *a += *c; }
360            dbg_stats(&format!("text.l{l}"), &x);
361        }
362        let (lw, lb) = (load_vec(m, "text.encoder.ln_final.weight")?, load_vec(m, "text.encoder.ln_final.bias")?);
363        layer_norm_rows(&mut x, &lw, &lb, S_EPS);
364        dbg_stats("text.ln_final", &x);
365        if let Some(d) = std::env::var("SAM3_DUMP_DIR").ok() {
366            dump_f32(&d, "text_lh", &x);
367        }
368        // CLIP pools at the first eos token: (ids == eos).argmax()
369        let p = ids.iter().position(|&v| v == 49407).ok_or_else(|| anyhow!("no eos token"))?;
370        let pooled = &x[p * SD..(p + 1) * SD];
371        // text_embeds = pooled @ projection; projection is stored [1024, 512]
372        let proj = load_vec(m, "text.encoder.text_projection")?;
373        dbg_stats("text.proj", &proj);
374        let mut embeds = vec![0f32; PROJ];
375        for j in 0..PROJ {
376            let mut acc = 0f32;
377            for i in 0..SD { acc += pooled[i] * proj[i * PROJ + j]; }
378            embeds[j] = acc;
379        }
380        dbg_stats("text.pooled", pooled);
381        dbg_stats("text.embeds", &embeds);
382        // DETR text features: Linear(1024 -> 256) over the full sequence
383        let rz = lin(m, "text.resizer.weight")?;
384        let rb = load_vec(m, "text.resizer.bias")?;
385        let mut feats = vec![0f32; SS * FD];
386        rz.matmat(&x, SS, &mut feats, pool);
387        add_bias(&mut feats, FD, &rb);
388        dbg_stats("text.feats", &feats);
389        Ok((embeds, feats))
390    }
391
392    // ── trunk (ViT, pre-norm layers, windowed/global axial RoPE) ────
393
394    fn trunk(&self, pixels: &[f32], pool: Option<&Pool>) -> Result<Vec<f32>> {
395        let m = &self.model;
396        let np = GRID * GRID;
397        let kc = 3 * PATCH * PATCH;
398        let dump = std::env::var("SAM3_DUMP_DIR").ok();
399        let dlayers: Option<Vec<usize>> = std::env::var("SAM3_DUMP_LAYERS")
400            .ok()
401            .map(|s| s.split(',').filter_map(|t| t.trim().parse().ok()).collect());
402        let w = load_vec(m, "trunk.patch_embed.proj.weight")?; // [1024,3,14,14]
403        let mut pv = vec![0f32; np * kc];
404        let pvb = pv.as_mut_ptr() as usize;
405        let fill = |s: usize, e: usize| {
406            let pv = unsafe { std::slice::from_raw_parts_mut(pvb as *mut f32, np * kc) };
407            for pt in s..e {
408                let (py, px) = (pt / GRID, pt % GRID);
409                let d = &mut pv[pt * kc..(pt + 1) * kc];
410                let mut o = 0;
411                for c in 0..3 {
412                    for ky in 0..PATCH {
413                        let y = py * PATCH + ky;
414                        for kx in 0..PATCH {
415                            d[o] = pixels[c * PIX * PIX + y * PIX + px * PATCH + kx];
416                            o += 1;
417                        }
418                    }
419                }
420            }
421        };
422        match pool { Some(p) => p.run_rows(np, &fill), None => fill(0, np) };
423        let mut x = vec![0f32; np * TD];
424        let xb = x.as_mut_ptr() as usize;
425        let gemm = |s: usize, e: usize| {
426            let x = unsafe { std::slice::from_raw_parts_mut(xb as *mut f32, np * TD) };
427            for ch in s..e {
428                let wr = &w[ch * kc..(ch + 1) * kc];
429                for pt in 0..np {
430                    let xv = &pv[pt * kc..(pt + 1) * kc];
431                    let mut acc = 0f32;
432                    for k in 0..kc { acc += wr[k] * xv[k]; }
433                    x[pt * TD + ch] = acc;
434                }
435            }
436        };
437        match pool { Some(p) => p.run_rows(TD, &gemm), None => gemm(0, TD) };
438        if let Some(d) = &dump { dump_f32(d, "trunk_patch", &x); }
439        // position embeddings [1,577,1024]: drop Meta's cls row, tile 24x24 -> 72x72
440        let pos = load_vec(m, "trunk.pos_embed")?;
441        let base = if self.pos_skip_cls { 1 } else { 0 };
442        if pos.len() < (base + 576) * TD { return Err(anyhow!("pos_embed too small")); }
443        for pt in 0..np {
444            let (py, px) = (pt / GRID, pt % GRID);
445            let src = (base + (py % 24) * 24 + px % 24) * TD;
446            for d in 0..TD { x[pt * TD + d] += pos[src + d]; }
447        }
448        let (lw, lb) = (load_vec(m, "trunk.ln_pre.weight")?, load_vec(m, "trunk.ln_pre.bias")?);
449        layer_norm_rows(&mut x, &lw, &lb, T_EPS);
450        if let Some(d) = &dump { dump_f32(d, "trunk_pre", &x); }
451
452        for l in 0..TL {
453            let b = format!("trunk.blocks.{l}.");
454            let (nw, nb) = (load_vec(m, &format!("{b}norm1.weight"))?, load_vec(m, &format!("{b}norm1.bias"))?);
455            let mut h = x.clone();
456            layer_norm_rows(&mut h, &nw, &nb, T_EPS);
457            let dbg0 = dump.is_some() && l == 0;
458            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_ln1", &h); }
459            let qkv = lin(m, &format!("{b}attn.qkv.weight"))?;
460            let qb = load_vec(m, &format!("{b}attn.qkv.bias"))?;
461            let proj = lin(m, &format!("{b}attn.proj.weight"))?;
462            let pb = load_vec(m, &format!("{b}attn.proj.bias"))?;
463            let fc = load_vec(m, &format!("{b}attn.freqs_cis"))?;
464            let attn_out = if GLOBAL.contains(&l) {
465                let mut o = vec![0f32; np * TD];
466                run_attn(&qkv, &qb, &proj, &pb, &h, &h, &h, np, np, TD, TH, Some(&fc), Mask::None, pool, &mut o);
467                o
468            } else {
469                self.vit_windows(&h, &qkv, &qb, &proj, &pb, &fc, pool)?
470            };
471            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_attn", &attn_out); }
472            for (a, c) in x.iter_mut().zip(&attn_out) { *a += *c; }
473            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_x2", &x); }
474            // pre-norm MLP branch: norm a CLONE — x keeps the residual
475            // (norming x in place made the block add mlp(ln2(x)) instead
476            // of x + mlp(ln2(x))).
477            let (nw, nb) = (load_vec(m, &format!("{b}norm2.weight"))?, load_vec(m, &format!("{b}norm2.bias"))?);
478            let mut h = x.clone();
479            layer_norm_rows(&mut h, &nw, &nb, T_EPS);
480            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_ln2", &h); }
481            let f1 = lin(m, &format!("{b}mlp.fc1.weight"))?;
482            let f1b = load_vec(m, &format!("{b}mlp.fc1.bias"))?;
483            let f2 = lin(m, &format!("{b}mlp.fc2.weight"))?;
484            let f2b = load_vec(m, &format!("{b}mlp.fc2.bias"))?;
485            let mut ff = vec![0f32; np * TI];
486            f1.matmat(&h, np, &mut ff, pool);
487            add_bias(&mut ff, TI, &f1b);
488            ff.iter_mut().for_each(|v| *v = gelu_erf(*v));
489            let mut fo = vec![0f32; np * TD];
490            f2.matmat(&ff, np, &mut fo, pool);
491            add_bias(&mut fo, TD, &f2b);
492            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_mlp", &fo); }
493            for (a, c) in x.iter_mut().zip(&fo) { *a += *c; }
494            if let (Some(d), true) = (&dump, dlayers.as_ref().map_or(true, |ls| ls.contains(&l))) {
495                dump_f32(d, &format!("trunk_b{l}"), &x);
496            }
497        }
498        Ok(x)
499    }
500
501    /// Windowed trunk attention: 3x3 windows of 24x24 tokens (freqs [576]).
502    fn vit_windows(
503        &self,
504        h: &[f32],
505        qkv: &QTensor,
506        qb: &[f32],
507        proj: &QTensor,
508        pb: &[f32],
509        fc: &[f32],
510        pool: Option<&Pool>,
511    ) -> Result<Vec<f32>> {
512        let np = GRID * GRID;
513        let nw = GRID / WIN;
514        let wl = WIN * WIN;
515        let mut wh = vec![0f32; wl * TD];
516        let mut ao = vec![0f32; wl * TD];
517        let mut out = vec![0f32; np * TD];
518        for wy in 0..nw {
519            for wx in 0..nw {
520                for y in 0..WIN {
521                    for x2 in 0..WIN {
522                        let s = ((wy * WIN + y) * GRID + wx * WIN + x2) * TD;
523                        let d = (y * WIN + x2) * TD;
524                        wh[d..d + TD].copy_from_slice(&h[s..s + TD]);
525                    }
526                }
527                run_attn(qkv, qb, proj, pb, &wh, &wh, &wh, wl, wl, TD, TH, Some(fc), Mask::None, pool, &mut ao);
528                for y in 0..WIN {
529                    for x2 in 0..WIN {
530                        let d = ((wy * WIN + y) * GRID + wx * WIN + x2) * TD;
531                        let s = (y * WIN + x2) * TD;
532                        out[d..d + TD].copy_from_slice(&ao[s..s + TD]);
533                    }
534                }
535            }
536        }
537        Ok(out)
538    }
539
540    // ── FPN neck ────────────────────────────────────────────────────
541
542    /// Four pyramid levels from the trunk map: 288/144/72/36, each
543    /// [256, H, W] channels-first.
544    fn fpn(&self, trunk: &[f32], pool: Option<&Pool>) -> Result<Vec<Vec<f32>>> {
545        fpn_levels(&self.model, self.fpn_prefix, trunk, pool, 4)
546    }
547
548    // ── DETR encoder (pre-norm layers) ──────────────────────────────
549
550    #[allow(clippy::too_many_arguments)]
551    fn detr_encoder(
552        &self,
553        vision: &[f32],
554        pos: &[f32],
555        text: &[f32],
556        am: &[u8],
557        pool: Option<&Pool>,
558    ) -> Result<Vec<f32>> {
559        let m = &self.model;
560        let n = vision.len() / FD;
561        let mut xs = vision.to_vec();
562        let mut km = vec![0f32; SS];
563        for (k, &v) in am.iter().enumerate() { km[k] = if v == 0 { NEG } else { 0.0 }; }
564        for l in 0..NL {
565            let b = format!("det.transformer.encoder.layers.{l}.");
566            // pre-norm self attention: q=k=normed+pos, v=normed
567            let (nw, nb) = (load_vec(m, &format!("{b}norm1.weight"))?, load_vec(m, &format!("{b}norm1.bias"))?);
568            let mut h = xs.clone();
569            layer_norm_rows(&mut h, &nw, &nb, D_EPS);
570            let hp: Vec<f32> = h
571                .chunks_exact(FD)
572                .zip(pos.chunks_exact(FD))
573                .flat_map(|(a, c)| a.iter().zip(c).map(|(x, y)| x + y))
574                .collect();
575            let qkv = lin(m, &format!("{b}self_attn.in_proj_weight"))?;
576            let ib = load_vec(m, &format!("{b}self_attn.in_proj_bias"))?;
577            let op = lin(m, &format!("{b}self_attn.out_proj.weight"))?;
578            let ob = load_vec(m, &format!("{b}self_attn.out_proj.bias"))?;
579            let mut ao = vec![0f32; n * FD];
580            run_attn(&qkv, &ib, &op, &ob, &hp, &hp, &h, n, n, FD, FH, None, Mask::None, pool, &mut ao);
581            for (a, c) in xs.iter_mut().zip(&ao) { *a += *c; }
582            // cross attention to the text features (query has no pos add)
583            let (nw, nb) = (load_vec(m, &format!("{b}norm2.weight"))?, load_vec(m, &format!("{b}norm2.bias"))?);
584            let mut h = xs.clone();
585            layer_norm_rows(&mut h, &nw, &nb, D_EPS);
586            let qkv = lin(m, &format!("{b}cross_attn_image.in_proj_weight"))?;
587            let ib = load_vec(m, &format!("{b}cross_attn_image.in_proj_bias"))?;
588            let op = lin(m, &format!("{b}cross_attn_image.out_proj.weight"))?;
589            let ob = load_vec(m, &format!("{b}cross_attn_image.out_proj.bias"))?;
590            let mut ao = vec![0f32; n * FD];
591            run_attn(&qkv, &ib, &op, &ob, &h, text, text, n, SS, FD, FH, None, Mask::Key(&km), pool, &mut ao);
592            for (a, c) in xs.iter_mut().zip(&ao) { *a += *c; }
593            // relu MLP
594            let (nw, nb) = (load_vec(m, &format!("{b}norm3.weight"))?, load_vec(m, &format!("{b}norm3.bias"))?);
595            let mut h = xs.clone();
596            layer_norm_rows(&mut h, &nw, &nb, D_EPS);
597            let l1 = lin(m, &format!("{b}linear1.weight"))?;
598            let l1b = load_vec(m, &format!("{b}linear1.bias"))?;
599            let l2 = lin(m, &format!("{b}linear2.weight"))?;
600            let l2b = load_vec(m, &format!("{b}linear2.bias"))?;
601            let mut mid = vec![0f32; n * FI];
602            l1.matmat(&h, n, &mut mid, pool);
603            add_bias(&mut mid, FI, &l1b);
604            mid.iter_mut().for_each(|v| *v = v.max(0.0));
605            let mut fo = vec![0f32; n * FD];
606            l2.matmat(&mid, n, &mut fo, pool);
607            add_bias(&mut fo, FD, &l2b);
608            for (a, c) in xs.iter_mut().zip(&fo) { *a += *c; }
609        }
610        Ok(xs)
611    }
612
613    // ── DETR decoder (POST-norm layers) ─────────────────────────────
614
615    /// Returns (stacked layer outputs [6*200*256], final reference boxes
616    /// in cxcywh — the box_head refinement of the last layer, which
617    /// equals Sam3Model's `all_pred_boxes[-1]` pre-xyxy conversion — and
618    /// the FINAL layer's presence logit: the presence row through
619    /// `presence_token_out_norm` + the 3-layer presence head, clamped
620    /// ±10 as HF clamps it).
621    fn detr_decoder(
622        &self,
623        enc: &[f32],
624        pos: &[f32],
625        text: &[f32],
626        am: &[u8],
627        pool: Option<&Pool>,
628    ) -> Result<(Vec<f32>, Vec<[f32; 4]>, f32)> {
629        let m = &self.model;
630        let n = enc.len() / FD;
631        let db = "det.transformer.decoder.";
632        let query = load_vec(m, &format!("{db}query_embed.weight"))?;
633        let presence = load_vec(m, &format!("{db}presence_token.weight"))?;
634        let ref0 = load_vec(m, &format!("{db}reference_points.weight"))?;
635        let mut hidden = vec![0f32; (NQ + 1) * FD];
636        hidden[..FD].copy_from_slice(&presence);
637        hidden[FD..].copy_from_slice(&query);
638        let mut refs: Vec<[f32; 4]> = ref0
639            .chunks_exact(4)
640            .map(|c| [sigm(c[0]), sigm(c[1]), sigm(c[2]), sigm(c[3])])
641            .collect();
642        let mut km = vec![0f32; SS];
643        for (k, &v) in am.iter().enumerate() { km[k] = if v == 0 { NEG } else { 0.0 }; }
644        // heads (all loaded once, biases included)
645        let rh0 = lin(m, &format!("{db}ref_point_head.layers.0.weight"))?;
646        let rh0b = load_vec(m, &format!("{db}ref_point_head.layers.0.bias"))?;
647        let rh1 = lin(m, &format!("{db}ref_point_head.layers.1.weight"))?;
648        let rh1b = load_vec(m, &format!("{db}ref_point_head.layers.1.bias"))?;
649        let rx = DecoderMlp::load(m, &format!("{db}boxRPB_embed_x."))?;
650        let ry = DecoderMlp::load(m, &format!("{db}boxRPB_embed_y."))?;
651        let bbox = DecoderMlp3::load3(m, &format!("{db}bbox_embed."))?;
652        let olnw = load_vec(m, &format!("{db}norm.weight"))?;
653        let olnb = load_vec(m, &format!("{db}norm.bias"))?;
654        let pres_w = load_vec(m, &format!("{db}presence_token_out_norm.weight"))?;
655        let pres_b = load_vec(m, &format!("{db}presence_token_out_norm.bias"))?;
656        let pres_head = DecoderMlp3::load3(m, &format!("{db}presence_token_head."))?;
657        let key = add_rows(enc, pos); // enc + pos, [n, FD]
658        let mut inter: Vec<Vec<f32>> = Vec::with_capacity(NL);
659        for l in 0..NL {
660            let b = format!("{db}layers.{l}.");
661            // query position from the sine encoding of the reference boxes
662            let sine = sine_boxes(&refs);
663            let mut qp = vec![0f32; NQ * FD];
664            rh0.matmat(&sine, NQ, &mut qp, pool);
665            add_bias(&mut qp, FD, &rh0b);
666            qp.iter_mut().for_each(|v| *v = v.max(0.0)); // DecoderMLP: relu between
667            let mut qp2 = vec![0f32; NQ * FD];
668            rh1.matmat(&qp, NQ, &mut qp2, pool);
669            add_bias(&mut qp2, FD, &rh1b);
670            let qp = qp2;
671            let mut qp_full = vec![0f32; (NQ + 1) * FD];
672            qp_full[FD..].copy_from_slice(&qp);
673            let rpb = rpb_bias(&refs, &rx, &ry, n, pool)?;
674            // self attention: q=k=h+qp (pad row stays qp=0), v=h ; POST-norm
675            let hqp = add_rows(&hidden, &qp_full);
676            let qkv = lin(m, &format!("{b}self_attn.in_proj_weight"))?;
677            let ib = load_vec(m, &format!("{b}self_attn.in_proj_bias"))?;
678            let op = lin(m, &format!("{b}self_attn.out_proj.weight"))?;
679            let ob = load_vec(m, &format!("{b}self_attn.out_proj.bias"))?;
680            let mut ao = vec![0f32; (NQ + 1) * FD];
681            run_attn(&qkv, &ib, &op, &ob, &hqp, &hqp, &hidden, NQ + 1, NQ + 1, FD, FH, None, Mask::None, pool, &mut ao);
682            for (a, c) in hidden.iter_mut().zip(&ao) { *a += *c; }
683            let (nw, nb) = (load_vec(m, &format!("{b}norm1.weight"))?, load_vec(m, &format!("{b}norm1.bias"))?);
684            layer_norm_rows(&mut hidden, &nw, &nb, D_EPS);
685            // text cross attention
686            let hqp = add_rows(&hidden, &qp_full);
687            let qkv = lin(m, &format!("{b}ca_text.in_proj_weight"))?;
688            let ib = load_vec(m, &format!("{b}ca_text.in_proj_bias"))?;
689            let op = lin(m, &format!("{b}ca_text.out_proj.weight"))?;
690            let ob = load_vec(m, &format!("{b}ca_text.out_proj.bias"))?;
691            let mut ao = vec![0f32; (NQ + 1) * FD];
692            run_attn(&qkv, &ib, &op, &ob, &hqp, text, text, NQ + 1, SS, FD, FH, None, Mask::Key(&km), pool, &mut ao);
693            for (a, c) in hidden.iter_mut().zip(&ao) { *a += *c; }
694            let (nw, nb) = (load_vec(m, &format!("{b}catext_norm.weight"))?, load_vec(m, &format!("{b}catext_norm.bias"))?);
695            layer_norm_rows(&mut hidden, &nw, &nb, D_EPS);
696            // vision cross attention (keys = enc + pos, values = enc, RPB bias)
697            let hqp = add_rows(&hidden, &qp_full);
698            let qkv = lin(m, &format!("{b}cross_attn.in_proj_weight"))?;
699            let ib = load_vec(m, &format!("{b}cross_attn.in_proj_bias"))?;
700            let op = lin(m, &format!("{b}cross_attn.out_proj.weight"))?;
701            let ob = load_vec(m, &format!("{b}cross_attn.out_proj.bias"))?;
702            let mut ao = vec![0f32; (NQ + 1) * FD];
703            run_attn(&qkv, &ib, &op, &ob, &hqp, &key, enc, NQ + 1, n, FD, FH, None, Mask::Head(&rpb), pool, &mut ao);
704            for (a, c) in hidden.iter_mut().zip(&ao) { *a += *c; }
705            let (nw, nb) = (load_vec(m, &format!("{b}norm2.weight"))?, load_vec(m, &format!("{b}norm2.bias"))?);
706            layer_norm_rows(&mut hidden, &nw, &nb, D_EPS);
707            // relu MLP (Sam3MLP)
708            let l1 = lin(m, &format!("{b}linear1.weight"))?;
709            let l1b = load_vec(m, &format!("{b}linear1.bias"))?;
710            let l2 = lin(m, &format!("{b}linear2.weight"))?;
711            let l2b = load_vec(m, &format!("{b}linear2.bias"))?;
712            let mut mid = vec![0f32; (NQ + 1) * FI];
713            l1.matmat(&hidden, NQ + 1, &mut mid, pool);
714            add_bias(&mut mid, FI, &l1b);
715            mid.iter_mut().for_each(|v| *v = v.max(0.0));
716            let mut fo = vec![0f32; (NQ + 1) * FD];
717            l2.matmat(&mid, NQ + 1, &mut fo, pool);
718            add_bias(&mut fo, FD, &l2b);
719            for (a, c) in hidden.iter_mut().zip(&fo) { *a += *c; }
720            let (nw, nb) = (load_vec(m, &format!("{b}norm3.weight"))?, load_vec(m, &format!("{b}norm3.bias"))?);
721            layer_norm_rows(&mut hidden, &nw, &nb, D_EPS);
722            // box refine + intermediate (query rows only, output_layer_norm)
723            let mut qh = hidden[FD..].to_vec();
724            layer_norm_rows(&mut qh, &olnw, &olnb, D_EPS);
725            inter.push(qh.clone());
726            let d = bbox.forward(&qh, NQ, pool);
727            for (r, dd) in refs.iter_mut().zip(d.chunks_exact(4)) {
728                for k in 0..4 { r[k] = sigm(inv_sigmoid(r[k]) + dd[k]); }
729            }
730        }
731        let dec = inter.iter().flat_map(|v| v.iter().copied()).collect();
732        // Presence logit of the FINAL layer: row 0 (the presence token)
733        // is post-normed by the layer stack like every other row — feed
734        // it through presence_layer_norm + presence_head (relu, relu,
735        // linear — 3 layers), clamp ±10 (HF `clamp_presence_logit_max_val`).
736        let mut ph = hidden[0..FD].to_vec();
737        layer_norm_rows(&mut ph, &pres_w, &pres_b, D_EPS);
738        let pl = pres_head.forward(&ph, 1, pool);
739        let presence_logit = pl[0].clamp(-10.0, 10.0);
740        Ok((dec, refs, presence_logit))
741    }
742
743    /// Dot-product concept scoring — HF `Sam3DotProductScoring` over
744    /// the `det.scoring.*` section: the prompt MLP (2 layers, relu
745    /// between) + residual + out-norm on the resizer features,
746    /// mean-pooled over the unpadded positions, projected by
747    /// `prompt_proj`; the output-normed queries of the FINAL decoder
748    /// layer projected by `hs_proj`; the dot product scaled by
749    /// 1/sqrt(256) and clamped to ±12 (HF `clamp_max_val`). `dec` is
750    /// one layer's `[nq * FD]` queries; returns `nq` logits.
751    fn score(
752        &self,
753        text: &[f32],
754        am: &[u8],
755        dec: &[f32],
756        pool: Option<&Pool>,
757    ) -> Result<Vec<f32>> {
758        let m = &self.model;
759        let mlp = DecoderMlp::load(m, "det.scoring.prompt_mlp.")?;
760        let mut h = mlp.forward(text, SS, pool, FD);
761        for (a, b) in h.iter_mut().zip(text) {
762            *a += *b;
763        }
764        let (ow, ob) = (
765            load_vec(m, "det.scoring.prompt_mlp.out_norm.weight")?,
766            load_vec(m, "det.scoring.prompt_mlp.out_norm.bias")?,
767        );
768        layer_norm_rows(&mut h, &ow, &ob, D_EPS);
769        // Mean pool over attention_mask == 1 (HF `_pool_text_features`).
770        let n_valid = am.iter().filter(|&&v| v == 1).count().max(1) as f32;
771        let mut pooled = vec![0f32; FD];
772        for (i, &v) in am.iter().enumerate().take(SS) {
773            if v == 1 {
774                for d in 0..FD {
775                    pooled[d] += h[i * FD + d];
776                }
777            }
778        }
779        for v in pooled.iter_mut() {
780            *v /= n_valid;
781        }
782        let pt = lin(m, "det.scoring.prompt_proj.weight")?;
783        let pb = load_vec(m, "det.scoring.prompt_proj.bias")?;
784        let mut tp = vec![0f32; FD];
785        pt.matmat(&pooled, 1, &mut tp, pool);
786        add_bias(&mut tp, FD, &pb);
787        let nq = dec.len() / FD;
788        let hs = lin(m, "det.scoring.hs_proj.weight")?;
789        let hb = load_vec(m, "det.scoring.hs_proj.bias")?;
790        let mut q = vec![0f32; nq * FD];
791        hs.matmat(dec, nq, &mut q, pool);
792        add_bias(&mut q, FD, &hb);
793        let scale = 1.0 / (FD as f32).sqrt();
794        Ok(q
795            .chunks(FD)
796            .map(|r| {
797                let mut acc = 0f32;
798                for d in 0..FD {
799                    acc += r[d] * tp[d];
800                }
801                (acc * scale).clamp(-12.0, 12.0)
802            })
803            .collect())
804    }
805
806    // ── mask decoder ────────────────────────────────────────────────
807
808    /// Prompt cross-attention over the encoder states, pixel decoder
809    /// (nearest upsample + sum + conv3x3 + GroupNorm + ReLU), then
810    /// pred_masks[q] = einsum(mask_embed[q], instance_embed).
811    #[allow(clippy::too_many_arguments)]
812    fn mask_decoder(
813        &self,
814        queries: &[f32],
815        enc: &[f32],
816        fpn: &[Vec<f32>],
817        text: &[f32],
818        am: &[u8],
819        pool: Option<&Pool>,
820    ) -> Result<Vec<f32>> {
821        let m = &self.model;
822        let db = "det.seghead.";
823        let n = enc.len() / FD;
824        let mut km = vec![0f32; SS];
825        for (k, &v) in am.iter().enumerate() { km[k] = if v == 0 { NEG } else { 0.0 }; }
826        let nw = load_vec(m, &format!("{db}cross_attn_norm.weight"))?;
827        let nb = load_vec(m, &format!("{db}cross_attn_norm.bias"))?;
828        let mut h = enc.to_vec();
829        layer_norm_rows(&mut h, &nw, &nb, D_EPS);
830        let qkv = lin(m, &format!("{db}cross_attend_prompt.in_proj_weight"))?;
831        let ib = load_vec(m, &format!("{db}cross_attend_prompt.in_proj_bias"))?;
832        let op = lin(m, &format!("{db}cross_attend_prompt.out_proj.weight"))?;
833        let ob = load_vec(m, &format!("{db}cross_attend_prompt.out_proj.bias"))?;
834        let mut e = enc.to_vec();
835        let mut ao = vec![0f32; n * FD];
836        run_attn(&qkv, &ib, &op, &ob, &h, text, text, n, SS, FD, FH, None, Mask::Key(&km), pool, &mut ao);
837        for (a, c) in e.iter_mut().zip(&ao) { *a += *c; }
838        // pixel decoder: encoder states replace level 2 (72), then upsample
839        let l72 = rows_to_chw(&e, GRID, GRID);
840        let sizes = [(144usize, 144usize), (288, 288)];
841        let mut prev = l72;
842        for (li, &(sh, sw)) in sizes.iter().enumerate() {
843            prev = upsample_nearest(&prev, FD, sh / 2, sh / 2, sh, sw);
844            let fw = &fpn[1 - li]; // reversed(fpn[:-1]): 144 then 288
845            for (a, b) in prev.iter_mut().zip(fw.iter()) { *a += *b; }
846            let cw = load_vec(m, &format!("{db}pixel_decoder.conv_layers.{li}.weight"))?;
847            let cb = load_vec(m, &format!("{db}pixel_decoder.conv_layers.{li}.bias"))?;
848            prev = conv2d(&cw, &cb, &prev, FD, sh, sw, 3, 3, 1, 1, pool);
849            let gw = load_vec(m, &format!("{db}pixel_decoder.norms.{li}.weight"))?;
850            let gb = load_vec(m, &format!("{db}pixel_decoder.norms.{li}.bias"))?;
851            group_norm(&mut prev, 8, &gw, &gb, 1e-5);
852            prev.iter_mut().for_each(|v| *v = v.max(0.0));
853        }
854        let iw = load_vec(m, &format!("{db}instance_seg_head.weight"))?;
855        let ib2 = load_vec(m, &format!("{db}instance_seg_head.bias"))?;
856        let inst = conv2d(&iw, &ib2, &prev, FD, MH, MH, 1, 1, 1, 0, pool);
857        // mask embeddings from the (already output-normed) queries
858        let mut q = queries.to_vec();
859        for i in 0..3 {
860            let w = lin(m, &format!("{db}mask_predictor.mask_embed.layers.{i}.weight"))?;
861            let b = load_vec(m, &format!("{db}mask_predictor.mask_embed.layers.{i}.bias"))?;
862            let mut o = vec![0f32; NQ * FD];
863            w.matmat(&q, NQ, &mut o, pool);
864            add_bias(&mut o, FD, &b);
865            if i < 2 { o.iter_mut().for_each(|v| *v = v.max(0.0)); }
866            q = o;
867        }
868        let hw = MH * MH;
869        let mut out = vec![0f32; NQ * hw];
870        let ob2 = out.as_mut_ptr() as usize;
871        let run = |s: usize, e: usize| {
872            let o = unsafe { std::slice::from_raw_parts_mut(ob2 as *mut f32, NQ * hw) };
873            for qi in s..e {
874                let qq = &q[qi * FD..(qi + 1) * FD];
875                let dst = &mut o[qi * hw..(qi + 1) * hw];
876                for c in 0..FD {
877                    let wgt = qq[c];
878                    if wgt == 0.0 { continue; }
879                    let ic = &inst[c * hw..(c + 1) * hw];
880                    for p in 0..hw { dst[p] += wgt * ic[p]; }
881                }
882            }
883        };
884        match pool { Some(p) => p.run_rows(NQ, &run), None => run(0, NQ) }
885        Ok(out)
886    }
887}
888
889/// `Sam3VisionNeck` (`Sam3FPNLayer` stack) over the trunk map: the first
890/// `n_levels` pyramid levels at 288/144/72/36, each [256, H, W]
891/// channels-first. The DETR path uses all four; the video tracker's neck
892/// (`fpn.sam2_convs`) only consumes the first three.
893pub(crate) fn fpn_levels(
894    m: &Arc<CmfModel>,
895    p: &str,
896    trunk: &[f32],
897    pool: Option<&Pool>,
898    n_levels: usize,
899) -> Result<Vec<Vec<f32>>> {
900    let hidden = rows_to_chw(trunk, GRID, GRID); // [1024,72,72]
901    let scales = [4.0f32, 2.0, 1.0, 0.5];
902    let mut levels = Vec::with_capacity(n_levels);
903    for (i, &s) in scales.iter().enumerate().take(n_levels) {
904        let (mut ci, mut ch, mut cw) = (TD, GRID, GRID);
905        let mut cur = hidden.clone();
906        match s {
907            4.0 => {
908                let w0 = load_vec(m, &format!("{p}.{i}.dconv_2x2_0.weight"))?;
909                let b0 = load_vec(m, &format!("{p}.{i}.dconv_2x2_0.bias"))?;
910                cur = convt2x2(&w0, &b0, &cur, ci, ch, cw, pool);
911                (ch, cw, ci) = (ch * 2, cw * 2, b0.len());
912                cur.iter_mut().for_each(|v| *v = gelu_erf(*v));
913                let w1 = load_vec(m, &format!("{p}.{i}.dconv_2x2_1.weight"))?;
914                let b1 = load_vec(m, &format!("{p}.{i}.dconv_2x2_1.bias"))?;
915                cur = convt2x2(&w1, &b1, &cur, ci, ch, cw, pool);
916                (ch, cw, ci) = (ch * 2, cw * 2, b1.len());
917            }
918            2.0 => {
919                let w0 = load_vec(m, &format!("{p}.{i}.dconv_2x2.weight"))?;
920                let b0 = load_vec(m, &format!("{p}.{i}.dconv_2x2.bias"))?;
921                cur = convt2x2(&w0, &b0, &cur, ci, ch, cw, pool);
922                (ch, cw, ci) = (ch * 2, cw * 2, b0.len());
923            }
924            1.0 => {}
925            _ => {
926                cur = maxpool2(&cur, ci, ch, cw);
927                (ch, cw) = (ch / 2, cw / 2);
928            }
929        }
930        let w1 = load_vec(m, &format!("{p}.{i}.conv_1x1.weight"))?;
931        let b1 = load_vec(m, &format!("{p}.{i}.conv_1x1.bias"))?;
932        cur = conv2d(&w1, &b1, &cur, ci, ch, cw, 1, 1, 1, 0, pool);
933        ci = b1.len();
934        let w3 = load_vec(m, &format!("{p}.{i}.conv_3x3.weight"))?;
935        let b3 = load_vec(m, &format!("{p}.{i}.conv_3x3.bias"))?;
936        cur = conv2d(&w3, &b3, &cur, ci, ch, cw, 3, 3, 1, 1, pool);
937        levels.push(cur);
938    }
939    Ok(levels)
940}
941
942// ───────────────────── model pieces ─────────────────────
943
944/// Sam3DecoderMLP, 2 layers: relu(l1), l2 (with biases).
945struct DecoderMlp {
946    w0: QTensor,
947    b0: Vec<f32>,
948    w1: QTensor,
949    b1: Vec<f32>,
950}
951
952impl DecoderMlp {
953    fn load(m: &Arc<CmfModel>, prefix: &str) -> Result<Self> {
954        Ok(Self {
955            w0: lin(m, &format!("{prefix}layers.0.weight"))?,
956            b0: load_vec(m, &format!("{prefix}layers.0.bias"))?,
957            w1: lin(m, &format!("{prefix}layers.1.weight"))?,
958            b1: load_vec(m, &format!("{prefix}layers.1.bias"))?,
959        })
960    }
961    fn forward(&self, x: &[f32], rows: usize, pool: Option<&Pool>, out_w: usize) -> Vec<f32> {
962        let mut h = vec![0f32; rows * self.b0.len()];
963        self.w0.matmat(x, rows, &mut h, pool);
964        add_bias(&mut h, self.b0.len(), &self.b0);
965        h.iter_mut().for_each(|v| *v = v.max(0.0));
966        let mut o = vec![0f32; rows * out_w];
967        self.w1.matmat(&h, rows, &mut o, pool);
968        add_bias(&mut o, out_w, &self.b1);
969        o
970    }
971}
972
973/// Sam3DecoderMLP, 3 layers: relu(l1), relu(l2), l3 (box head).
974struct DecoderMlp3 {
975    w0: QTensor,
976    b0: Vec<f32>,
977    w1: QTensor,
978    b1: Vec<f32>,
979    w2: QTensor,
980    b2: Vec<f32>,
981}
982
983impl DecoderMlp3 {
984    fn load3(m: &Arc<CmfModel>, prefix: &str) -> Result<Self> {
985        Ok(Self {
986            w0: lin(m, &format!("{prefix}layers.0.weight"))?,
987            b0: load_vec(m, &format!("{prefix}layers.0.bias"))?,
988            w1: lin(m, &format!("{prefix}layers.1.weight"))?,
989            b1: load_vec(m, &format!("{prefix}layers.1.bias"))?,
990            w2: lin(m, &format!("{prefix}layers.2.weight"))?,
991            b2: load_vec(m, &format!("{prefix}layers.2.bias"))?,
992        })
993    }
994    fn forward(&self, x: &[f32], rows: usize, pool: Option<&Pool>) -> Vec<f32> {
995        let mut d = vec![0f32; rows * self.b0.len()];
996        self.w0.matmat(x, rows, &mut d, pool);
997        add_bias(&mut d, self.b0.len(), &self.b0);
998        d.iter_mut().for_each(|v| *v = v.max(0.0));
999        let mut d2 = vec![0f32; rows * self.b1.len()];
1000        self.w1.matmat(&d, rows, &mut d2, pool);
1001        add_bias(&mut d2, self.b1.len(), &self.b1);
1002        d2.iter_mut().for_each(|v| *v = v.max(0.0));
1003        let mut o = vec![0f32; rows * self.b2.len()];
1004        self.w2.matmat(&d2, rows, &mut o, pool);
1005        add_bias(&mut o, self.b2.len(), &self.b2);
1006        o
1007    }
1008}
1009
1010pub(crate) fn sigm(x: f32) -> f32 {
1011    1.0 / (1.0 + (-x).exp())
1012}
1013
1014fn inv_sigmoid(x: f32) -> f32 {
1015    let x = x.clamp(0.0, 1.0);
1016    ((x.max(1e-3)) / ((1.0 - x).max(1e-3))).ln()
1017}
1018
1019fn xyxy(boxes: &[[f32; 4]]) -> Vec<f32> {
1020    let mut out = vec![0f32; boxes.len() * 4];
1021    for (i, b) in boxes.iter().enumerate() {
1022        out[i * 4] = b[0] - 0.5 * b[2];
1023        out[i * 4 + 1] = b[1] - 0.5 * b[3];
1024        out[i * 4 + 2] = b[0] + 0.5 * b[2];
1025        out[i * 4 + 3] = b[1] + 0.5 * b[3];
1026    }
1027    out
1028}
1029
1030pub(crate) fn lin(m: &Arc<CmfModel>, name: &str) -> Result<QTensor> {
1031    QTensor::from_model(m, name).map_err(|e| anyhow!(e))
1032}
1033
1034/// Dequantize a tensor to flat f32 (norms, biases, tables, convs).
1035pub(crate) fn load_vec(m: &Arc<CmfModel>, name: &str) -> Result<Vec<f32>> {
1036    let e = m.tensor(name).ok_or_else(|| anyhow!("tensor '{name}' not found"))?;
1037    if matches!(e.dtype, TensorDtype::U8) { return Err(anyhow!("'{name}' is u8")); }
1038    let bytes = m.entry_bytes(e);
1039    let mut dst = vec![0f32; e.n_elems()];
1040    dequant_tensor(e, bytes, &mut dst).map_err(|er| anyhow!("{name}: {er}"))?;
1041    Ok(dst)
1042}
1043
1044/// Row-major [rows, cols] += bias (broadcast over rows).
1045pub(crate) fn add_bias(out: &mut [f32], cols: usize, bias: &[f32]) {
1046    debug_assert_eq!(bias.len(), cols);
1047    for r in out.chunks_exact_mut(cols) {
1048        for (v, b) in r.iter_mut().zip(bias) { *v += *b; }
1049    }
1050}
1051
1052/// LayerNorm over rows of `x` (row length = w.len()).
1053pub(crate) fn layer_norm_rows(x: &mut [f32], w: &[f32], b: &[f32], eps: f32) {
1054    let d = w.len();
1055    for r in x.chunks_exact_mut(d) {
1056        let mean = r.iter().sum::<f32>() / d as f32;
1057        let var = r.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / d as f32;
1058        let inv = 1.0 / (var + eps).sqrt();
1059        for (v, (&ww, &bb)) in r.iter_mut().zip(w.iter().zip(b.iter())) {
1060            *v = (*v - mean) * inv * ww + bb;
1061        }
1062    }
1063}
1064
1065/// Axial 2D RoPE in place on a fused QKV block: rotates the q part
1066/// (dims [0, dim)) and the k part ([dim, 2*dim)); v untouched. freqs_cis
1067/// = [n, 32, 2] = (cos, sin) per pair; pairs 0..15 from w-position,
1068/// 16..31 from h-position; per head block of 64 dims (16 heads share the
1069/// same position-dependent table).
1070fn apply_rope(buf: &mut [f32], n: usize, stride: usize, width: usize, fc: &[f32]) {
1071    let npairs = 32;
1072    debug_assert_eq!(fc.len(), n * npairs * 2);
1073    let blocks = width / 64;
1074    for i in 0..n {
1075        let f = &fc[i * npairs * 2..(i + 1) * npairs * 2];
1076        let row = &mut buf[i * stride..i * stride + width];
1077        for b in 0..blocks {
1078            let base = b * 64;
1079            for j in 0..npairs {
1080                let (c, s) = (f[j * 2], f[j * 2 + 1]);
1081                let (a, d) = (row[base + j * 2], row[base + j * 2 + 1]);
1082                row[base + j * 2] = a * c - d * s;
1083                row[base + j * 2 + 1] = d * c + a * s;
1084            }
1085        }
1086    }
1087}
1088
1089/// Multi-head attention for one sequence: fused QKV projection
1090/// (`w_qkv` rows = 3*dim laid out [q | k | v] per row, `qb` its bias),
1091/// optional axial RoPE on q/k, additive mask, softmax, output projection
1092/// (`w_o` + `ob`). `out` is [n_q, dim]; scale = head_dim^-0.5.
1093#[allow(clippy::too_many_arguments)]
1094pub(crate) fn run_attn(
1095    w_qkv: &QTensor,
1096    qb: &[f32],
1097    w_o: &QTensor,
1098    ob: &[f32],
1099    xs_q: &[f32],
1100    xs_k: &[f32],
1101    xs_v: &[f32],
1102    n_q: usize,
1103    n_k: usize,
1104    dim: usize,
1105    heads: usize,
1106    rope: Option<&[f32]>,
1107    mask: Mask,
1108    pool: Option<&Pool>,
1109    out: &mut [f32],
1110) {
1111    let h3 = dim * 3;
1112    let hd = dim / heads;
1113    let scale = 1.0 / (hd as f32).sqrt();
1114    let mut qkv_q = vec![0f32; n_q * h3];
1115    w_qkv.matmat(xs_q, n_q, &mut qkv_q, pool);
1116    add_bias(&mut qkv_q, h3, qb);
1117    let same_k = xs_k.as_ptr() == xs_q.as_ptr();
1118    let mut kbuf = Vec::new();
1119    if !same_k {
1120        kbuf = vec![0f32; n_k * h3];
1121        w_qkv.matmat(xs_k, n_k, &mut kbuf, pool);
1122        add_bias(&mut kbuf, h3, qb);
1123    }
1124    let same_v = xs_v.as_ptr() == xs_q.as_ptr();
1125    let mut vbuf = Vec::new();
1126    if !same_v {
1127        vbuf = vec![0f32; n_k * h3];
1128        w_qkv.matmat(xs_v, n_k, &mut vbuf, pool);
1129        add_bias(&mut vbuf, h3, qb);
1130    }
1131    if let Some(fc) = rope {
1132        // rotate q (and k when they share the buffer) over the first 2*dim dims
1133        apply_rope(&mut qkv_q, n_q, h3, dim * 2, fc);
1134        if !same_k {
1135            apply_rope(&mut kbuf, n_k, h3, dim * 2, fc);
1136        }
1137    }
1138    let qkv_k: &[f32] = if same_k { &qkv_q } else { &kbuf };
1139    let qkv_v: &[f32] = if same_v { &qkv_q } else { &vbuf };
1140    let mut attn = vec![0f32; n_q * dim];
1141    let ab = attn.as_mut_ptr() as usize;
1142    let run = |s: usize, e: usize| {
1143        let a = unsafe { std::slice::from_raw_parts_mut(ab as *mut f32, n_q * dim) };
1144        let mut scores = vec![0f32; n_k];
1145        for i in s..e {
1146            let qrow = &qkv_q[i * h3..i * h3 + dim];
1147            for t in 0..heads {
1148                let qs = &qrow[t * hd..(t + 1) * hd];
1149                let mut mx = f32::NEG_INFINITY;
1150                for j in 0..n_k {
1151                    let kr = &qkv_k[j * h3 + dim + t * hd..j * h3 + dim + (t + 1) * hd];
1152                    let mut v = qs.iter().zip(kr).fold(0.0f32, |c, (a, b)| c + a * b) * scale;
1153                    v += match mask {
1154                        Mask::None => 0.0,
1155                        Mask::Key(km) => km[j],
1156                        Mask::Add(am) => am[i * n_k + j],
1157                        Mask::Head(hm) => hm[(t * n_q + i) * n_k + j],
1158                    };
1159                    scores[j] = v;
1160                    if v > mx { mx = v; }
1161                }
1162                // Stable softmax: the trunk's ViT scores reach ~155, so a
1163                // raw exp() overflows to inf for the row-max and the sum
1164                // turns inf → inv = 0 → the whole attention row silently
1165                // collapses to zeros. Subtract the row max (`mx`).
1166                let mut sum = 0.0f32;
1167                for sv in scores.iter_mut().take(n_k) {
1168                    *sv = (*sv - mx).exp();
1169                    sum += *sv;
1170                }
1171                let inv = 1.0 / sum;
1172                let orow = &mut a[i * dim + t * hd..i * dim + (t + 1) * hd];
1173                for j in 0..n_k {
1174                    let wgt = scores[j] * inv;
1175                    if wgt == 0.0 { continue; }
1176                    let vr = &qkv_v[j * h3 + 2 * dim + t * hd..j * h3 + 2 * dim + (t + 1) * hd];
1177                    for (o, vv) in orow.iter_mut().zip(vr) { *o += wgt * vv; }
1178                }
1179            }
1180        }
1181    };
1182    match pool { Some(p) => p.run_rows(n_q, &run), None => run(0, n_q) }
1183    w_o.matmat(&attn, n_q, out, pool);
1184    add_bias(out, dim, ob);
1185}
1186
1187// ─────────── convolutions (NCHW f32, explicit shapes) ───────────
1188
1189pub(crate) fn conv2d(
1190    w: &[f32],
1191    b: &[f32],
1192    x: &[f32],
1193    ci: usize,
1194    h: usize,
1195    wid: usize,
1196    kh: usize,
1197    kw: usize,
1198    stride: usize,
1199    pad: usize,
1200    pool: Option<&Pool>,
1201) -> Vec<f32> {
1202    let co = b.len();
1203    debug_assert_eq!(w.len(), co * ci * kh * kw);
1204    debug_assert_eq!(x.len(), ci * h * wid);
1205    let oh = (h + 2 * pad - kh) / stride + 1;
1206    let ow = (wid + 2 * pad - kw) / stride + 1;
1207    let mut out = vec![0f32; co * oh * ow];
1208    let ob = out.as_mut_ptr() as usize;
1209    let run = |s: usize, e: usize| {
1210        let o = unsafe { std::slice::from_raw_parts_mut(ob as *mut f32, co * oh * ow) };
1211        for idx in s..e {
1212            let c_o = idx / (oh * ow);
1213            let rem = idx % (oh * ow);
1214            let (oy, ox) = (rem / ow, rem % ow);
1215            let mut acc = b[c_o];
1216            for c in 0..ci {
1217                for dy in 0..kh {
1218                    let iy = oy * stride + dy;
1219                    if iy < pad || iy - pad >= h { continue; }
1220                    for dx in 0..kw {
1221                        let ix = ox * stride + dx;
1222                        if ix < pad || ix - pad >= wid { continue; }
1223                        acc += w[((c_o * ci + c) * kh + dy) * kw + dx]
1224                            * x[(c * h + iy - pad) * wid + (ix - pad)];
1225                    }
1226                }
1227            }
1228            o[idx] = acc;
1229        }
1230    };
1231    match pool { Some(p) => p.run_rows(co * oh * ow, &run), None => run(0, co * oh * ow) }
1232    out
1233}
1234
1235/// ConvTranspose2d(k=2, s=2): w is [ci, co, 2, 2]; x is [ci, h, wid].
1236pub(crate) fn convt2x2(w: &[f32], b: &[f32], x: &[f32], ci: usize, h: usize, wid: usize, pool: Option<&Pool>) -> Vec<f32> {
1237    let co = b.len();
1238    debug_assert_eq!(w.len(), ci * co * 4);
1239    let (oh, ow) = (h * 2, wid * 2);
1240    let mut out = vec![0f32; co * oh * ow];
1241    let ob = out.as_mut_ptr() as usize;
1242    let run = |s: usize, e: usize| {
1243        let o = unsafe { std::slice::from_raw_parts_mut(ob as *mut f32, co * oh * ow) };
1244        for idx in s..e {
1245            let c_o = idx / (oh * ow);
1246            let rem = idx % (oh * ow);
1247            let (oy, ox) = (rem / ow, rem % ow);
1248            let (kh, kx) = (oy & 1, ox & 1);
1249            let (iy, ix) = (oy >> 1, ox >> 1);
1250            let mut acc = b[c_o];
1251            for c in 0..ci {
1252                acc += w[((c * co + c_o) * 2 + kh) * 2 + kx] * x[(c * h + iy) * wid + ix];
1253            }
1254            o[idx] = acc;
1255        }
1256    };
1257    match pool { Some(p) => p.run_rows(co * oh * ow, &run), None => run(0, co * oh * ow) }
1258    out
1259}
1260
1261pub(crate) fn maxpool2(x: &[f32], ci: usize, h: usize, wid: usize) -> Vec<f32> {
1262    let (oh, ow) = (h / 2, wid / 2);
1263    let mut out = vec![0f32; ci * oh * ow];
1264    for c in 0..ci {
1265        for oy in 0..oh {
1266            for ox in 0..ow {
1267                let mut mx = f32::NEG_INFINITY;
1268                for dy in 0..2 {
1269                    for dx in 0..2 {
1270                        let v = x[(c * h + oy * 2 + dy) * wid + ox * 2 + dx];
1271                        if v > mx { mx = v; }
1272                    }
1273                }
1274                out[(c * oh + oy) * ow + ox] = mx;
1275            }
1276        }
1277    }
1278    out
1279}
1280
1281/// Nearest-neighbour upsample of [ci, h, wid] to [ci, sh, sw].
1282pub(crate) fn upsample_nearest(x: &[f32], ci: usize, h: usize, wid: usize, sh: usize, sw: usize) -> Vec<f32> {
1283    let mut out = vec![0f32; ci * sh * sw];
1284    for c in 0..ci {
1285        for oy in 0..sh {
1286            let iy = oy * h / sh;
1287            for ox in 0..sw {
1288                let ix = ox * wid / sw;
1289                out[(c * sh + oy) * sw + ox] = x[(c * h + iy) * wid + ix];
1290            }
1291        }
1292    }
1293    out
1294}
1295
1296/// GroupNorm(eps=1e-5) over channels of [c, h, w].
1297pub(crate) fn group_norm(x: &mut [f32], groups: usize, w: &[f32], b: &[f32], eps: f32) {
1298    let co = w.len();
1299    let gsz = co / groups;
1300    let rest = x.len() / co;
1301    for g in 0..groups {
1302        let s = g * gsz * rest;
1303        let e = s + gsz * rest;
1304        let mean = x[s..e].iter().sum::<f32>() / (e - s) as f32;
1305        let var = x[s..e].iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / (e - s) as f32;
1306        let inv = 1.0 / (var + eps).sqrt();
1307        for (i, v) in x[s..e].iter_mut().enumerate() {
1308            let ch = g * gsz + (i / rest);
1309            *v = (*v - mean) * inv * w[ch] + b[ch];
1310        }
1311    }
1312}
1313
1314// ─────────── layout helpers ───────────
1315
1316pub(crate) fn rows_to_chw(rows: &[f32], h: usize, w: usize) -> Vec<f32> {
1317    let n = h * w;
1318    let c = rows.len() / n;
1319    let mut out = vec![0f32; c * h * w];
1320    for ch in 0..c {
1321        for y in 0..h {
1322            for x in 0..w { out[(ch * h + y) * w + x] = rows[(y * w + x) * c + ch]; }
1323        }
1324    }
1325    out
1326}
1327
1328pub(crate) fn chw_to_rows(chw: &[f32], h: usize, w: usize) -> Vec<f32> {
1329    let c = chw.len() / (h * w);
1330    let mut out = vec![0f32; h * w * c];
1331    for ch in 0..c {
1332        for y in 0..h {
1333            for x in 0..w { out[(y * w + x) * c + ch] = chw[(ch * h + y) * w + x]; }
1334        }
1335    }
1336    out
1337}
1338
1339/// Normalized sine position encoding for an FPN level: rows [h*w, 2n]
1340/// (pos_y then pos_x), scale 2π, temperature 10000.
1341pub(crate) fn sine_pos(h: usize, w: usize, n: usize) -> Vec<f32> {
1342    let scale = std::f32::consts::TAU;
1343    let mut dim_t = vec![0f32; n];
1344    for i in 0..n { dim_t[i] = 10000f32.powf((2 * (i / 2)) as f32 / n as f32); }
1345    let mut out = vec![0f32; h * w * n * 2];
1346    let (hn, wn) = (h as f32, w as f32);
1347    for y in 0..h {
1348        let yy = (y + 1) as f32 / (hn + 1e-6) * scale;
1349        for x in 0..w {
1350            let xx = (x + 1) as f32 / (wn + 1e-6) * scale;
1351            let row = (y * w + x) * n * 2;
1352            for i in 0..n {
1353                let (py, px) = (yy / dim_t[i], xx / dim_t[i]);
1354                if i % 2 == 0 {
1355                    out[row + i] = py.sin();
1356                    out[row + n + i] = px.sin();
1357                } else {
1358                    out[row + i] = py.cos();
1359                    out[row + n + i] = px.cos();
1360                }
1361            }
1362        }
1363    }
1364    out
1365}
1366
1367/// Sine box embedding (decoder query_pos input): [N, 4*128] =
1368/// cat(pos_y, pos_x, pos_w, pos_h) for boxes in (cx, cy, w, h),
1369/// scale 2π (Sam3SinePositionEmbedding.encode_boxes).
1370fn sine_boxes(boxes: &[[f32; 4]]) -> Vec<f32> {
1371    let nf = 128;
1372    let scale = std::f32::consts::TAU;
1373    let mut dim_t = vec![0f32; nf];
1374    for i in 0..nf { dim_t[i] = 10000f32.powf((2 * (i / 2)) as f32 / nf as f32); }
1375    let mut out = vec![0f32; boxes.len() * 4 * nf];
1376    for (q, b) in boxes.iter().enumerate() {
1377        let base = q * 4 * nf;
1378        for (ci, v) in [b[1], b[0], b[2], b[3]].iter().enumerate() {
1379            for i in 0..nf {
1380                let val = v * scale / dim_t[i];
1381                out[base + ci * nf + i] = if i % 2 == 0 { val.sin() } else { val.cos() };
1382            }
1383        }
1384    }
1385    out
1386}
1387
1388/// Per-head log box-RPB additive bias: [heads, NQ+1, n]. Row 0 (the
1389/// presence token) is zero — HF pads the query dim from NQ to NQ+1.
1390fn rpb_bias(refs: &[[f32; 4]], rx: &DecoderMlp, ry: &DecoderMlp, n: usize, pool: Option<&Pool>) -> Result<Vec<f32>> {
1391    let gh = GRID;
1392    debug_assert_eq!(n, gh * gh);
1393    let l2_8 = 8.0f32.ln();
1394    let mut dy = vec![0f32; refs.len() * gh * 2];
1395    let mut dx = vec![0f32; refs.len() * gh * 2];
1396    for (q, r) in refs.iter().enumerate() {
1397        let x1 = r[0] - 0.5 * r[2];
1398        let y1 = r[1] - 0.5 * r[3];
1399        let x2 = r[0] + 0.5 * r[2];
1400        let y2 = r[1] + 0.5 * r[3];
1401        for hi in 0..gh {
1402            let c = hi as f32 / gh as f32;
1403            dy[(q * gh + hi) * 2] = log_compress(c - y1, l2_8);
1404            dy[(q * gh + hi) * 2 + 1] = log_compress(c - y2, l2_8);
1405        }
1406        for wi in 0..gh {
1407            let c = wi as f32 / gh as f32;
1408            dx[(q * gh + wi) * 2] = log_compress(c - x1, l2_8);
1409            dx[(q * gh + wi) * 2 + 1] = log_compress(c - x2, l2_8);
1410        }
1411    }
1412    let py = ry.forward(&dy, refs.len() * gh, pool, FH);
1413    let px = rx.forward(&dx, refs.len() * gh, pool, FH);
1414    let mut out = vec![0f32; FH * (NQ + 1) * n];
1415    for t in 0..FH {
1416        for q in 0..refs.len() {
1417            for hi in 0..gh {
1418                for wi in 0..gh {
1419                    out[(t * (NQ + 1) + (q + 1)) * n + hi * gh + wi] =
1420                        py[(q * gh + hi) * FH + t] + px[(q * gh + wi) * FH + t];
1421                }
1422            }
1423        }
1424    }
1425    Ok(out)
1426}
1427
1428/// sign(d) * log2(|8d| + 1) / log2(8)  (box_rpb_mode = "log").
1429fn log_compress(d: f32, l2_8: f32) -> f32 {
1430    d.signum() * ((8.0 * d).abs() + 1.0).log2() / l2_8
1431}
1432
1433/// elementwise a+b over equal-length buffers
1434fn add_rows(a: &[f32], b: &[f32]) -> Vec<f32> {
1435    a.iter().zip(b).map(|(x, y)| x + y).collect()
1436}
1437
1438/// Finiteness/range stats for one debug stage (env `SAM3_TEXT_DEBUG`).
1439#[allow(dead_code)]
1440fn dbg_stats(tag: &str, v: &[f32]) {
1441    if std::env::var("SAM3_TEXT_DEBUG").is_err() {
1442        return;
1443    }
1444    let (mut bad, mut mn, mut mx) = (0usize, f32::INFINITY, f32::NEG_INFINITY);
1445    for &x in v {
1446        if !x.is_finite() {
1447            bad += 1;
1448            continue;
1449        }
1450        if x < mn { mn = x; }
1451        if x > mx { mx = x; }
1452    }
1453    eprintln!(
1454        "[sam3] {tag} n={} nonfinite={} min={:.4e} max={:.4e}",
1455        v.len(), bad, mn, mx
1456    );
1457}
1458
1459/// Raw f32 dump for reference bisection (env `SAM3_DUMP_DIR`).
1460#[allow(dead_code)]
1461fn dump_f32(dir: &str, name: &str, v: &[f32]) {
1462    use std::io::Write;
1463    let p = std::path::Path::new(dir).join(format!("{name}.f32"));
1464    let mut b = Vec::with_capacity(v.len() * 4);
1465    for x in v {
1466        b.extend_from_slice(&x.to_le_bytes());
1467    }
1468    match std::fs::File::create(&p).and_then(|mut f| f.write_all(&b)) {
1469        Ok(()) => eprintln!("[sam3] dumped {} ({} elems)", p.display(), v.len()),
1470        Err(e) => eprintln!("[sam3] dump {} failed: {e}", p.display()),
1471    }
1472}