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::atomic::{AtomicU64, Ordering};
13use std::sync::Arc;
14
15use anyhow::{anyhow, Context, Result};
16use cortiq_core::quant::dequant_tensor;
17use cortiq_core::{CmfModel, TensorDtype};
18
19use crate::inference::gelu_erf;
20use crate::pool::Pool;
21use crate::qtensor::QTensor;
22
23/// Additive mask for "key is padded": saturates softmax to 0.
24const NEG: f32 = -1e30;
25
26/// The vendor image preprocess, exactly as `cfg.processor` spells it:
27/// resize to 1008×1008 with `resample: 2` (bilinear), rescale 1/255,
28/// normalize with mean/std 0.5 per channel, emit CHW f32. This produces
29/// the parity fixtures' `pixels.f32` from the source image.
30pub fn preprocess(frame: &crate::media::RgbFrame) -> Vec<f32> {
31    use image::imageops::{self, FilterType};
32    let img = image::RgbImage::from_raw(
33        frame.width as u32,
34        frame.height as u32,
35        frame.data.clone(),
36    )
37    .expect("RgbFrame buffer matches its own dimensions");
38    let rs = imageops::resize(&img, PIX as u32, PIX as u32, FilterType::Triangle);
39    let raw = rs.into_raw();
40    let mut out = vec![0f32; 3 * PIX * PIX];
41    for i in 0..PIX * PIX {
42        for c in 0..3 {
43            out[c * PIX * PIX + i] = (raw[i * 3 + c] as f32 / 255.0 - 0.5) / 0.5;
44        }
45    }
46    out
47}
48
49const PIX: usize = 1008;
50const PATCH: usize = 14;
51const GRID: usize = PIX / PATCH; // 72
52const WIN: usize = 24;
53const TD: usize = 1024;
54const TH: usize = 16;
55const TL: usize = 32;
56const TI: usize = 4736;
57const T_EPS: f32 = 1e-6;
58const GLOBAL: [usize; 4] = [7, 15, 23, 31];
59
60const SS: usize = 32;
61const SD: usize = 1024;
62const SL: usize = 24;
63const SI: usize = 4096;
64const S_EPS: f32 = 1e-5;
65const PROJ: usize = 512;
66
67const FD: usize = 256;
68const FI: usize = 2048;
69const FH: usize = 8;
70const D_EPS: f32 = 1e-6;
71const NL: usize = 6;
72const NQ: usize = 200;
73const MH: usize = 288;
74
75/// One forward's outputs (batch 1), flat row-major f32.
76pub struct Sam3Out {
77    /// CLIP text projection of the pooled token, [512].
78    pub text_embeds: Vec<f32>,
79    /// Resizer output fed to the DETR stack, [32 * 256].
80    pub text_feats: Vec<f32>,
81    /// Trunk features, [5184 * 1024].
82    pub trunk_feats: Vec<f32>,
83    /// FPN levels 288/144/72/36, each [256 * H * W].
84    pub fpn: Vec<Vec<f32>>,
85    /// DETR encoder output, [5184 * 256].
86    pub enc_hidden: Vec<f32>,
87    /// Stacked decoder layer outputs, [6 * 200 * 256].
88    pub dec_hidden: Vec<f32>,
89    /// Final boxes (xyxy), [200 * 4].
90    pub pred_boxes: Vec<f32>,
91    /// Instance masks, [200 * 288 * 288].
92    pub pred_masks: Vec<f32>,
93    /// Per-query classification logits from the dot-product scorer
94    /// (`det.scoring.*`), [200] — the final score of a query is
95    /// `sigmoid(pred_logits) * sigmoid(presence_logit)` (HF's
96    /// `post_process_object_detection`). Empty when `SAM3_ONLY`
97    /// skips the decoder.
98    pub pred_logits: Vec<f32>,
99    /// Presence-token logit of the final decoder layer (clamped ±10).
100    /// 0.0 when the decoder did not run.
101    pub presence_logit: f32,
102}
103
104/// Additive attention masks (added to scores before softmax).
105#[derive(Clone, Copy)]
106pub(crate) enum Mask<'a> {
107    None,
108    /// One value per key, shared by all queries/heads (padding).
109    Key(&'a [f32]),
110    /// [n_q, n_k] matrix, shared across heads (CLIP causal).
111    Add(&'a [f32]),
112    /// [heads, n_q, n_k] per-head bias (box RPB).
113    Head(&'a [f32]),
114}
115
116/// Attention sub-timers (ns), accumulated inside `run_attn` (qkv/proj
117/// GEMMs vs the softmax core) and printed by the session timer's Drop.
118static ATT_PROJ_NS: AtomicU64 = AtomicU64::new(0);
119static ATT_CORE_NS: AtomicU64 = AtomicU64::new(0);
120
121/// `SAM3_STAGES=1` — in-code wall-clock counters: `lap(name)` closes the
122/// current segment under `name` (laps with the same name merge), `add`
123/// accumulates inside loops. On drop the merged totals print as one
124/// `[sam3-prof]` block on stderr. Off by default (one env read per run).
125struct StageTimer {
126    on: bool,
127    main: bool,
128    t0: std::time::Instant,
129    acc: Vec<(&'static str, f64)>,
130}
131
132impl StageTimer {
133    fn new() -> Self {
134        Self {
135            on: std::env::var("SAM3_STAGES").is_ok_and(|v| v != "0"),
136            main: false,
137            t0: std::time::Instant::now(),
138            acc: Vec::new(),
139        }
140    }
141
142    /// The forward-level timer: resets the per-run attention statics so
143    /// multi-frame (video) runs report per-frame numbers.
144    fn new_session() -> Self {
145        ATT_PROJ_NS.store(0, Ordering::Relaxed);
146        ATT_CORE_NS.store(0, Ordering::Relaxed);
147        Self {
148            on: std::env::var("SAM3_STAGES").is_ok_and(|v| v != "0"),
149            main: true,
150            t0: std::time::Instant::now(),
151            acc: Vec::new(),
152        }
153    }
154
155    fn lap(&mut self, name: &'static str) {
156        if self.on {
157            self.acc.push((name, self.t0.elapsed().as_secs_f64() * 1e3));
158        }
159        self.t0 = std::time::Instant::now();
160    }
161}
162
163impl Drop for StageTimer {
164    fn drop(&mut self) {
165        if !self.on || self.acc.is_empty() {
166            return;
167        }
168        let mut merged: Vec<(&'static str, f64)> = Vec::new();
169        for (n, ms) in &self.acc {
170            match merged.iter_mut().find(|(m, _)| m == n) {
171                Some((_, t)) => *t += *ms,
172                None => merged.push((*n, *ms)),
173            }
174        }
175        let total: f64 = merged.iter().map(|(_, t)| t).sum();
176        for (n, ms) in &merged {
177            eprintln!("[sam3-prof] {n:>14} {ms:>10.1} ms  ({:.1}%)", ms / total * 100.0);
178        }
179        eprintln!("[sam3-prof] {:>14} {total:>10.1} ms", "TOTAL");
180        if self.main {
181            eprintln!(
182                "[sam3-prof] {:>14} {:>10.1} ms",
183                "attn_proj",
184                ATT_PROJ_NS.load(Ordering::Relaxed) as f64 / 1e6
185            );
186            eprintln!(
187                "[sam3-prof] {:>14} {:>10.1} ms",
188                "attn_core",
189                ATT_CORE_NS.load(Ordering::Relaxed) as f64 / 1e6
190            );
191            eprintln!(
192                "[sam3-prof] {:>14} hit/gate={}/{}",
193                "matmat_gpu",
194                crate::qtensor::MATMAT_GPU_HIT.load(Ordering::Relaxed),
195                crate::qtensor::MATMAT_GATE.load(Ordering::Relaxed)
196            );
197            #[cfg(feature = "gpu")]
198            eprintln!(
199                "[sam3-prof] {:>14} noctx/shape/entry/bounds/disp={}/{}/{}/{}/{}",
200                "m2f_refuse",
201                crate::gpu_wgpu::M2F_NO_CTX.load(Ordering::Relaxed),
202                crate::gpu_wgpu::M2F_SHAPE.load(Ordering::Relaxed),
203                crate::gpu_wgpu::M2F_ENTRY.load(Ordering::Relaxed),
204                crate::gpu_wgpu::M2F_BOUNDS.load(Ordering::Relaxed),
205                crate::gpu_wgpu::M2F_DISPATCH.load(Ordering::Relaxed),
206            );
207        }
208    }
209}
210
211pub struct Sam3 {
212    model: Arc<CmfModel>,
213    pool: Option<Arc<Pool>>,
214    /// `fpn.convs` (image detector neck) or `fpn.sam2_convs` (SAM3_FPN).
215    fpn_prefix: &'static str,
216    /// `trunk.pos_embed` is [1,577,1024]: Meta's leading cls row is
217    /// dropped by the HF conversion (SAM3_POS=first keeps rows 0..576).
218    pos_skip_cls: bool,
219}
220
221impl Sam3 {
222    pub fn open(path: &str) -> Result<Self> {
223        let model = CmfModel::open(path)
224            .with_context(|| format!("open {path}"))?
225            .into();
226        let fpn_prefix = match std::env::var("SAM3_FPN").as_deref() {
227            Ok("sam2_convs") => "fpn.sam2_convs",
228            _ => "fpn.convs",
229        };
230        let pos_skip_cls = std::env::var("SAM3_POS").as_deref() != Ok("first");
231        Ok(Self { model, pool: Pool::from_env(), fpn_prefix, pos_skip_cls })
232    }
233
234    /// The shared CMF handle (the video tracker reads its `trk.*` section
235    /// through it).
236    pub(crate) fn cmf(&self) -> &Arc<CmfModel> { &self.model }
237
238    /// The GEMM thread pool the detector was opened with.
239    pub(crate) fn pool_handle(&self) -> Option<&Arc<Pool>> { self.pool.as_ref() }
240
241    /// Debug: `"name dtype shape"` lines for every tensor in the CMF
242    /// directory (the parity tests filter them by section).
243    pub fn directory(&self) -> Vec<String> {
244        self.model
245            .tensors
246            .iter()
247            .map(|t| format!("{} {:?} {:?}", t.name, t.dtype, t.shape))
248            .collect()
249    }
250
251    /// Debug: raw payload bytes of one directory tensor.
252    pub fn raw_tensor(&self, name: &str) -> Option<Vec<u8>> {
253        let e = self.model.tensor(name)?;
254        Some(self.model.entry_bytes(e).to_vec())
255    }
256
257    /// Debug: every `q8_row` row scale and every `f16` payload element
258    /// whose bit pattern is non-finite, as `"name@idx bad=0x…"` lines.
259    pub fn bad_scales(&self) -> Vec<String> {
260        let f16_bad = |h: u16| h & 0x7C00 == 0x7C00;
261        let mut out = Vec::new();
262        for t in self.model.tensors.iter() {
263            let bytes = self.model.entry_bytes(t);
264            match t.dtype {
265                TensorDtype::Q8Row if t.shape.len() == 2 => {
266                    let (rows, cols) = (t.shape[0], t.shape[1]);
267                    let off = rows * cols;
268                    if bytes.len() < off + rows * 2 {
269                        out.push(format!("{}@payload short", t.name));
270                        continue;
271                    }
272                    for r in 0..rows {
273                        let h = u16::from_le_bytes([bytes[off + r * 2], bytes[off + r * 2 + 1]]);
274                        if f16_bad(h) {
275                            out.push(format!("{}@row{r} scale_f16=0x{h:04x}", t.name));
276                        }
277                    }
278                }
279                TensorDtype::F16 => {
280                    for (i, c) in bytes.chunks_exact(2).enumerate() {
281                        let h = u16::from_le_bytes([c[0], c[1]]);
282                        if f16_bad(h) {
283                            out.push(format!("{}@{i} val_f16=0x{h:04x}", t.name));
284                            if out.len() > 200 { return out; }
285                        }
286                    }
287                }
288                _ => {}
289            }
290        }
291        out
292    }
293
294    /// `pixels` [3,1008,1008] preprocessed; `ids`/`attn` length 32
295    /// (Sam3Processor, padding to max_length=32).
296    ///
297    /// Debug knobs (unset in normal runs): `SAM3_ONLY=trunk,...` runs only
298    /// the listed stages and returns empty tensors for the rest (the
299    /// parity test skips while it is set); `SAM3_DUMP_DIR` makes `trunk`
300    /// write its stage outputs (`trunk_patch.f32`, `trunk_pre.f32`,
301    /// `trunk_b{l}.f32`) for reference bisection; `SAM3_TEXT_DEBUG=1`
302    /// prints per-stage finiteness stats of the text tower.
303    pub fn forward(&mut self, pixels: &[f32], ids: &[u32], attn: &[u8]) -> Result<Sam3Out> {
304        let _prof = crate::cpuprof::spans::dump_on_drop("image forward");
305        if pixels.len() != 3 * PIX * PIX {
306            return Err(anyhow!("pixels {} != 3*{}*{}", pixels.len(), PIX, PIX));
307        }
308        if ids.len() != SS || attn.len() != SS {
309            return Err(anyhow!("ids/attn must be {SS} long"));
310        }
311        let only = std::env::var("SAM3_ONLY").ok();
312        let want = |k: &str| {
313            only
314                .as_deref()
315                .map_or(true, |o| o.split(',').any(|s| s.trim() == k))
316        };
317        let pool = self.pool.as_deref();
318        let mut stg = StageTimer::new_session();
319        let _p = crate::cpuprof::spans::span("text");
320        let (text_embeds, text_feats) = self.text(ids, attn, pool)?;
321        drop(_p);
322        stg.lap("text");
323        let blank = |te: Vec<f32>, tf: Vec<f32>| Sam3Out {
324            text_embeds: te,
325            text_feats: tf,
326            trunk_feats: Vec::new(),
327            fpn: Vec::new(),
328            enc_hidden: Vec::new(),
329            dec_hidden: Vec::new(),
330            pred_boxes: Vec::new(),
331            pred_masks: Vec::new(),
332            pred_logits: Vec::new(),
333            presence_logit: 0.0,
334        };
335        if !want("trunk") {
336            return Ok(blank(text_embeds, text_feats));
337        }
338        let trunk_feats = {
339            let _p = crate::cpuprof::spans::span("trunk");
340            self.trunk(pixels, pool)?
341        };
342        stg.lap("trunk");
343        if !want("fpn") {
344            let mut o = blank(text_embeds, text_feats);
345            o.trunk_feats = trunk_feats;
346            return Ok(o);
347        }
348        let fpn = self.fpn(&trunk_feats, pool)?;
349        stg.lap("fpn");
350        let pos = sine_pos(GRID, GRID, FD / 2); // level-2 (72) encoding, rows
351        let vision = chw_to_rows(&fpn[2], GRID, GRID);
352        let enc_hidden = {
353            let _p = crate::cpuprof::spans::span("detr_enc");
354            self.detr_encoder(&vision, &pos, &text_feats, attn, pool)?
355        };
356        stg.lap("enc");
357        if !want("dec") {
358            let mut o = blank(text_embeds, text_feats);
359            o.trunk_feats = trunk_feats;
360            o.fpn = fpn;
361            o.enc_hidden = enc_hidden;
362            return Ok(o);
363        }
364        let (dec_hidden, final_refs, presence_logit) = {
365            let _p = crate::cpuprof::spans::span("detr_dec");
366            self.detr_decoder(&enc_hidden, &pos, &text_feats, attn, pool)?
367        };
368        stg.lap("dec");
369        // Dot-product scoring (det.scoring.*): the FINAL layer's DETR
370        // queries against the mean-pooled prompt features. Logits, not
371        // probabilities — the caller multiplies both sigmoids, as HF's
372        // post-processor does (`all_pred_logits[-1]`).
373        let pred_logits = {
374            let _p = crate::cpuprof::spans::span("score");
375            self.score(
376                &text_feats,
377                attn,
378                &dec_hidden[(NL - 1) * NQ * FD..],
379                pool,
380            )?
381        };
382        stg.lap("score");
383        if !want("masks") {
384            let mut o = blank(text_embeds, text_feats);
385            o.trunk_feats = trunk_feats;
386            o.fpn = fpn;
387            o.enc_hidden = enc_hidden;
388            o.dec_hidden = dec_hidden.clone();
389            o.pred_boxes = xyxy(&final_refs);
390            o.pred_logits = pred_logits;
391            o.presence_logit = presence_logit;
392            return Ok(o);
393        }
394        let pred_boxes = xyxy(&final_refs);
395        let pred_masks = {
396            let _p = crate::cpuprof::spans::span("mask_dec");
397            self.mask_decoder(
398                &dec_hidden[(NL - 1) * NQ * FD..],
399                &enc_hidden,
400                &fpn,
401                &text_feats,
402                attn,
403                pool,
404            )?
405        };
406        stg.lap("masks");
407        Ok(Sam3Out {
408            text_embeds,
409            text_feats,
410            trunk_feats,
411            fpn,
412            enc_hidden,
413            dec_hidden,
414            pred_boxes,
415            pred_masks,
416            pred_logits,
417            presence_logit,
418        })
419    }
420
421    // ── text tower (CLIP-L, pre-norm transformer) ───────────────────
422
423    fn text(&self, ids: &[u32], attn: &[u8], pool: Option<&Pool>) -> Result<(Vec<f32>, Vec<f32>)> {
424        let m = &self.model;
425        let tok = lin(m, "text.encoder.token_embedding.weight")?;
426        let pos = load_vec(m, "text.encoder.positional_embedding")?;
427        let mut x = vec![0f32; SS * SD];
428        let mut row = vec![0f32; SD];
429        for (i, &id) in ids.iter().enumerate() {
430            tok.row_f32(id as usize, &mut row);
431            let r = &mut x[i * SD..(i + 1) * SD];
432            r.copy_from_slice(&row);
433            for (v, p) in r.iter_mut().zip(&pos[i * SD..(i + 1) * SD]) {
434                *v += *p;
435            }
436        }
437        dbg_stats("text.emb", &x);
438        // causal + padding additive mask [SS, SS]
439        let mut cm = vec![0f32; SS * SS];
440        for q in 0..SS {
441            for k in 0..=q {
442                if attn[k] == 0 {
443                    cm[q * SS + k] = NEG;
444                }
445            }
446            for k in (q + 1)..SS {
447                cm[q * SS + k] = NEG;
448            }
449        }
450        for l in 0..SL {
451            let b = format!("text.encoder.transformer.resblocks.{l}.");
452            let (lw, lb) = (load_vec(m, &format!("{b}ln_1.weight"))?, load_vec(m, &format!("{b}ln_1.bias"))?);
453            // Pre-norm: branch reads a normalized CLONE, x keeps the residual.
454            let mut h = x.clone();
455            layer_norm_rows(&mut h, &lw, &lb, S_EPS);
456            let qkv = lin(m, &format!("{b}attn.in_proj_weight"))?;
457            let ib = load_vec(m, &format!("{b}attn.in_proj_bias"))?;
458            let op = lin(m, &format!("{b}attn.out_proj.weight"))?;
459            let ob = load_vec(m, &format!("{b}attn.out_proj.bias"))?;
460            let mut ao = vec![0f32; SS * SD];
461            run_attn(
462                &qkv, &ib, &op, &ob,
463                &h, &h, &h, SS, SS, SD, 16, None, Mask::Add(&cm), pool, &mut ao,
464            );
465            for (a, c) in x.iter_mut().zip(&ao) { *a += *c; }
466            let (lw, lb) = (load_vec(m, &format!("{b}ln_2.weight"))?, load_vec(m, &format!("{b}ln_2.bias"))?);
467            let mut h = x.clone();
468            layer_norm_rows(&mut h, &lw, &lb, S_EPS);
469            let f1 = lin(m, &format!("{b}mlp.c_fc.weight"))?;
470            let f1b = load_vec(m, &format!("{b}mlp.c_fc.bias"))?;
471            let f2 = lin(m, &format!("{b}mlp.c_proj.weight"))?;
472            let f2b = load_vec(m, &format!("{b}mlp.c_proj.bias"))?;
473            let mut ff = vec![0f32; SS * SI];
474            f1.matmat(&h, SS, &mut ff, pool);
475            add_bias(&mut ff, SI, &f1b);
476            ff.iter_mut().for_each(|v| *v = gelu_erf(*v));
477            let mut fo = vec![0f32; SS * SD];
478            f2.matmat(&ff, SS, &mut fo, pool);
479            add_bias(&mut fo, SD, &f2b);
480            for (a, c) in x.iter_mut().zip(&fo) { *a += *c; }
481            dbg_stats(&format!("text.l{l}"), &x);
482        }
483        let (lw, lb) = (load_vec(m, "text.encoder.ln_final.weight")?, load_vec(m, "text.encoder.ln_final.bias")?);
484        layer_norm_rows(&mut x, &lw, &lb, S_EPS);
485        dbg_stats("text.ln_final", &x);
486        if let Some(d) = std::env::var("SAM3_DUMP_DIR").ok() {
487            dump_f32(&d, "text_lh", &x);
488        }
489        // CLIP pools at the first eos token: (ids == eos).argmax()
490        let p = ids.iter().position(|&v| v == 49407).ok_or_else(|| anyhow!("no eos token"))?;
491        let pooled = &x[p * SD..(p + 1) * SD];
492        // text_embeds = pooled @ projection; projection is stored [1024, 512]
493        let proj = load_vec(m, "text.encoder.text_projection")?;
494        dbg_stats("text.proj", &proj);
495        let mut embeds = vec![0f32; PROJ];
496        for j in 0..PROJ {
497            let mut acc = 0f32;
498            for i in 0..SD { acc += pooled[i] * proj[i * PROJ + j]; }
499            embeds[j] = acc;
500        }
501        dbg_stats("text.pooled", pooled);
502        dbg_stats("text.embeds", &embeds);
503        // DETR text features: Linear(1024 -> 256) over the full sequence
504        let rz = lin(m, "text.resizer.weight")?;
505        let rb = load_vec(m, "text.resizer.bias")?;
506        let mut feats = vec![0f32; SS * FD];
507        rz.matmat(&x, SS, &mut feats, pool);
508        add_bias(&mut feats, FD, &rb);
509        dbg_stats("text.feats", &feats);
510        Ok((embeds, feats))
511    }
512
513    // ── trunk (ViT, pre-norm layers, windowed/global axial RoPE) ────
514
515    fn trunk(&self, pixels: &[f32], pool: Option<&Pool>) -> Result<Vec<f32>> {
516        let _patch = crate::cpuprof::spans::span("trunk.patch");
517        let m = &self.model;
518        let np = GRID * GRID;
519        let kc = 3 * PATCH * PATCH;
520        let dump = std::env::var("SAM3_DUMP_DIR").ok();
521        let dlayers: Option<Vec<usize>> = std::env::var("SAM3_DUMP_LAYERS")
522            .ok()
523            .map(|s| s.split(',').filter_map(|t| t.trim().parse().ok()).collect());
524        let w = load_vec(m, "trunk.patch_embed.proj.weight")?; // [1024,3,14,14]
525        let mut pv = vec![0f32; np * kc];
526        let pvb = pv.as_mut_ptr() as usize;
527        let fill = |s: usize, e: usize| {
528            let pv = unsafe { std::slice::from_raw_parts_mut(pvb as *mut f32, np * kc) };
529            for pt in s..e {
530                let (py, px) = (pt / GRID, pt % GRID);
531                let d = &mut pv[pt * kc..(pt + 1) * kc];
532                let mut o = 0;
533                for c in 0..3 {
534                    for ky in 0..PATCH {
535                        let y = py * PATCH + ky;
536                        for kx in 0..PATCH {
537                            d[o] = pixels[c * PIX * PIX + y * PIX + px * PATCH + kx];
538                            o += 1;
539                        }
540                    }
541                }
542            }
543        };
544        match pool { Some(p) => p.run_rows(np, &fill), None => fill(0, np) };
545        let mut x = vec![0f32; np * TD];
546        let xb = x.as_mut_ptr() as usize;
547        let gemm = |s: usize, e: usize| {
548            let x = unsafe { std::slice::from_raw_parts_mut(xb as *mut f32, np * TD) };
549            for ch in s..e {
550                let wr = &w[ch * kc..(ch + 1) * kc];
551                for pt in 0..np {
552                    let xv = &pv[pt * kc..(pt + 1) * kc];
553                    let mut acc = 0f32;
554                    for k in 0..kc { acc += wr[k] * xv[k]; }
555                    x[pt * TD + ch] = acc;
556                }
557            }
558        };
559        match pool { Some(p) => p.run_rows(TD, &gemm), None => gemm(0, TD) };
560        drop(_patch);
561        if let Some(d) = &dump { dump_f32(d, "trunk_patch", &x); }
562        // position embeddings [1,577,1024]: drop Meta's cls row, tile 24x24 -> 72x72
563        let pos = load_vec(m, "trunk.pos_embed")?;
564        let base = if self.pos_skip_cls { 1 } else { 0 };
565        if pos.len() < (base + 576) * TD { return Err(anyhow!("pos_embed too small")); }
566        for pt in 0..np {
567            let (py, px) = (pt / GRID, pt % GRID);
568            let src = (base + (py % 24) * 24 + px % 24) * TD;
569            for d in 0..TD { x[pt * TD + d] += pos[src + d]; }
570        }
571        let (lw, lb) = (load_vec(m, "trunk.ln_pre.weight")?, load_vec(m, "trunk.ln_pre.bias")?);
572        layer_norm_rows(&mut x, &lw, &lb, T_EPS);
573        if let Some(d) = &dump { dump_f32(d, "trunk_pre", &x); }
574
575        let mut tst = StageTimer::new();
576        for l in 0..TL {
577            let b = format!("trunk.blocks.{l}.");
578            let _attn = crate::cpuprof::spans::span("trunk.attn");
579            let (nw, nb) = (load_vec(m, &format!("{b}norm1.weight"))?, load_vec(m, &format!("{b}norm1.bias"))?);
580            let mut h = x.clone();
581            layer_norm_rows(&mut h, &nw, &nb, T_EPS);
582            let dbg0 = dump.is_some() && l == 0;
583            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_ln1", &h); }
584            drop(_attn);
585            let qkv = lin(m, &format!("{b}attn.qkv.weight"))?;
586            let qb = load_vec(m, &format!("{b}attn.qkv.bias"))?;
587            let proj = lin(m, &format!("{b}attn.proj.weight"))?;
588            let pb = load_vec(m, &format!("{b}attn.proj.bias"))?;
589            let fc = load_vec(m, &format!("{b}attn.freqs_cis"))?;
590            tst.lap("trk_attn_ln");
591            let attn_out = if GLOBAL.contains(&l) {
592                let mut o = vec![0f32; np * TD];
593                let _pa = crate::cpuprof::spans::span("trunk.attn.run");
594                run_attn(&qkv, &qb, &proj, &pb, &h, &h, &h, np, np, TD, TH, Some(&fc), Mask::None, pool, &mut o);
595                drop(_pa);
596                o
597            } else {
598                let _pa = crate::cpuprof::spans::span("trunk.attn.run");
599                let o = self.vit_windows(&h, &qkv, &qb, &proj, &pb, &fc, pool)?;
600                drop(_pa);
601                o
602            };
603            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_attn", &attn_out); }
604            for (a, c) in x.iter_mut().zip(&attn_out) { *a += *c; }
605            tst.lap("trk_attn_run");
606            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_x2", &x); }
607            // pre-norm MLP branch: norm a CLONE — x keeps the residual
608            // (norming x in place made the block add mlp(ln2(x)) instead
609            // of x + mlp(ln2(x))).
610            let (nw, nb) = (load_vec(m, &format!("{b}norm2.weight"))?, load_vec(m, &format!("{b}norm2.bias"))?);
611            let mut h = x.clone();
612            layer_norm_rows(&mut h, &nw, &nb, T_EPS);
613            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_ln2", &h); }
614            let f1 = lin(m, &format!("{b}mlp.fc1.weight"))?;
615            let f1b = load_vec(m, &format!("{b}mlp.fc1.bias"))?;
616            let f2 = lin(m, &format!("{b}mlp.fc2.weight"))?;
617            let f2b = load_vec(m, &format!("{b}mlp.fc2.bias"))?;
618            let mut ff = vec![0f32; np * TI];
619            f1.matmat(&h, np, &mut ff, pool);
620            add_bias(&mut ff, TI, &f1b);
621            gelu_slice(&mut ff);
622            let mut fo = vec![0f32; np * TD];
623            f2.matmat(&ff, np, &mut fo, pool);
624            add_bias(&mut fo, TD, &f2b);
625            if let (Some(d), true) = (&dump, dbg0) { dump_f32(d, "b0_mlp", &fo); }
626            for (a, c) in x.iter_mut().zip(&fo) { *a += *c; }
627            tst.lap("trk_mlp");
628            if let (Some(d), true) = (&dump, dlayers.as_ref().map_or(true, |ls| ls.contains(&l))) {
629                dump_f32(d, &format!("trunk_b{l}"), &x);
630            }
631        }
632        Ok(x)
633    }
634
635    /// Windowed trunk attention: 3x3 windows of 24x24 tokens (freqs [576]).
636    fn vit_windows(
637        &self,
638        h: &[f32],
639        qkv: &QTensor,
640        qb: &[f32],
641        proj: &QTensor,
642        pb: &[f32],
643        fc: &[f32],
644        pool: Option<&Pool>,
645    ) -> Result<Vec<f32>> {
646        let np = GRID * GRID;
647        let nw = GRID / WIN;
648        let wl = WIN * WIN;
649        let mut wh = vec![0f32; wl * TD];
650        let mut ao = vec![0f32; wl * TD];
651        let mut out = vec![0f32; np * TD];
652        for wy in 0..nw {
653            for wx in 0..nw {
654                for y in 0..WIN {
655                    for x2 in 0..WIN {
656                        let s = ((wy * WIN + y) * GRID + wx * WIN + x2) * TD;
657                        let d = (y * WIN + x2) * TD;
658                        wh[d..d + TD].copy_from_slice(&h[s..s + TD]);
659                    }
660                }
661                run_attn(qkv, qb, proj, pb, &wh, &wh, &wh, wl, wl, TD, TH, Some(fc), Mask::None, pool, &mut ao);
662                for y in 0..WIN {
663                    for x2 in 0..WIN {
664                        let d = ((wy * WIN + y) * GRID + wx * WIN + x2) * TD;
665                        let s = (y * WIN + x2) * TD;
666                        out[d..d + TD].copy_from_slice(&ao[s..s + TD]);
667                    }
668                }
669            }
670        }
671        Ok(out)
672    }
673
674    // ── FPN neck ────────────────────────────────────────────────────
675
676    /// Four pyramid levels from the trunk map: 288/144/72/36, each
677    /// [256, H, W] channels-first.
678    fn fpn(&self, trunk: &[f32], pool: Option<&Pool>) -> Result<Vec<Vec<f32>>> {
679        fpn_levels(&self.model, self.fpn_prefix, trunk, pool, 4)
680    }
681
682    // ── DETR encoder (pre-norm layers) ──────────────────────────────
683
684    #[allow(clippy::too_many_arguments)]
685    fn detr_encoder(
686        &self,
687        vision: &[f32],
688        pos: &[f32],
689        text: &[f32],
690        am: &[u8],
691        pool: Option<&Pool>,
692    ) -> Result<Vec<f32>> {
693        let m = &self.model;
694        let n = vision.len() / FD;
695        let mut xs = vision.to_vec();
696        let mut km = vec![0f32; SS];
697        for (k, &v) in am.iter().enumerate() { km[k] = if v == 0 { NEG } else { 0.0 }; }
698        let mut tse = StageTimer::new();
699        for l in 0..NL {
700            let b = format!("det.transformer.encoder.layers.{l}.");
701            // pre-norm self attention: q=k=normed+pos, v=normed
702            let (nw, nb) = (load_vec(m, &format!("{b}norm1.weight"))?, load_vec(m, &format!("{b}norm1.bias"))?);
703            let mut h = xs.clone();
704            layer_norm_rows(&mut h, &nw, &nb, D_EPS);
705            let hp: Vec<f32> = h
706                .chunks_exact(FD)
707                .zip(pos.chunks_exact(FD))
708                .flat_map(|(a, c)| a.iter().zip(c).map(|(x, y)| x + y))
709                .collect();
710            let qkv = lin(m, &format!("{b}self_attn.in_proj_weight"))?;
711            let ib = load_vec(m, &format!("{b}self_attn.in_proj_bias"))?;
712            let op = lin(m, &format!("{b}self_attn.out_proj.weight"))?;
713            let ob = load_vec(m, &format!("{b}self_attn.out_proj.bias"))?;
714            let mut ao = vec![0f32; n * FD];
715            run_attn(&qkv, &ib, &op, &ob, &hp, &hp, &h, n, n, FD, FH, None, Mask::None, pool, &mut ao);
716            for (a, c) in xs.iter_mut().zip(&ao) { *a += *c; }
717            tse.lap("enc_self_attn");
718            // cross attention to the text features (query has no pos add)
719            let (nw, nb) = (load_vec(m, &format!("{b}norm2.weight"))?, load_vec(m, &format!("{b}norm2.bias"))?);
720            let mut h = xs.clone();
721            layer_norm_rows(&mut h, &nw, &nb, D_EPS);
722            let qkv = lin(m, &format!("{b}cross_attn_image.in_proj_weight"))?;
723            let ib = load_vec(m, &format!("{b}cross_attn_image.in_proj_bias"))?;
724            let op = lin(m, &format!("{b}cross_attn_image.out_proj.weight"))?;
725            let ob = load_vec(m, &format!("{b}cross_attn_image.out_proj.bias"))?;
726            let mut ao = vec![0f32; n * FD];
727            run_attn(&qkv, &ib, &op, &ob, &h, text, text, n, SS, FD, FH, None, Mask::Key(&km), pool, &mut ao);
728            for (a, c) in xs.iter_mut().zip(&ao) { *a += *c; }
729            tse.lap("enc_cross_text");
730            // relu MLP
731            let (nw, nb) = (load_vec(m, &format!("{b}norm3.weight"))?, load_vec(m, &format!("{b}norm3.bias"))?);
732            let mut h = xs.clone();
733            layer_norm_rows(&mut h, &nw, &nb, D_EPS);
734            let l1 = lin(m, &format!("{b}linear1.weight"))?;
735            let l1b = load_vec(m, &format!("{b}linear1.bias"))?;
736            let l2 = lin(m, &format!("{b}linear2.weight"))?;
737            let l2b = load_vec(m, &format!("{b}linear2.bias"))?;
738            let mut mid = vec![0f32; n * FI];
739            l1.matmat(&h, n, &mut mid, pool);
740            add_bias(&mut mid, FI, &l1b);
741            mid.iter_mut().for_each(|v| *v = v.max(0.0));
742            let mut fo = vec![0f32; n * FD];
743            l2.matmat(&mid, n, &mut fo, pool);
744            add_bias(&mut fo, FD, &l2b);
745            for (a, c) in xs.iter_mut().zip(&fo) { *a += *c; }
746            tse.lap("enc_ffn");
747        }
748        Ok(xs)
749    }
750
751    // ── DETR decoder (POST-norm layers) ─────────────────────────────
752
753    /// Returns (stacked layer outputs [6*200*256], final reference boxes
754    /// in cxcywh — the box_head refinement of the last layer, which
755    /// equals Sam3Model's `all_pred_boxes[-1]` pre-xyxy conversion — and
756    /// the FINAL layer's presence logit: the presence row through
757    /// `presence_token_out_norm` + the 3-layer presence head, clamped
758    /// ±10 as HF clamps it).
759    fn detr_decoder(
760        &self,
761        enc: &[f32],
762        pos: &[f32],
763        text: &[f32],
764        am: &[u8],
765        pool: Option<&Pool>,
766    ) -> Result<(Vec<f32>, Vec<[f32; 4]>, f32)> {
767        let m = &self.model;
768        let n = enc.len() / FD;
769        let db = "det.transformer.decoder.";
770        let query = load_vec(m, &format!("{db}query_embed.weight"))?;
771        let presence = load_vec(m, &format!("{db}presence_token.weight"))?;
772        let ref0 = load_vec(m, &format!("{db}reference_points.weight"))?;
773        let mut hidden = vec![0f32; (NQ + 1) * FD];
774        hidden[..FD].copy_from_slice(&presence);
775        hidden[FD..].copy_from_slice(&query);
776        let mut refs: Vec<[f32; 4]> = ref0
777            .chunks_exact(4)
778            .map(|c| [sigm(c[0]), sigm(c[1]), sigm(c[2]), sigm(c[3])])
779            .collect();
780        let mut km = vec![0f32; SS];
781        for (k, &v) in am.iter().enumerate() { km[k] = if v == 0 { NEG } else { 0.0 }; }
782        let mut tsd = StageTimer::new();
783        // heads (all loaded once, biases included)
784        let rh0 = lin(m, &format!("{db}ref_point_head.layers.0.weight"))?;
785        let rh0b = load_vec(m, &format!("{db}ref_point_head.layers.0.bias"))?;
786        let rh1 = lin(m, &format!("{db}ref_point_head.layers.1.weight"))?;
787        let rh1b = load_vec(m, &format!("{db}ref_point_head.layers.1.bias"))?;
788        let rx = DecoderMlp::load(m, &format!("{db}boxRPB_embed_x."))?;
789        let ry = DecoderMlp::load(m, &format!("{db}boxRPB_embed_y."))?;
790        let bbox = DecoderMlp3::load3(m, &format!("{db}bbox_embed."))?;
791        let olnw = load_vec(m, &format!("{db}norm.weight"))?;
792        let olnb = load_vec(m, &format!("{db}norm.bias"))?;
793        let pres_w = load_vec(m, &format!("{db}presence_token_out_norm.weight"))?;
794        let pres_b = load_vec(m, &format!("{db}presence_token_out_norm.bias"))?;
795        let pres_head = DecoderMlp3::load3(m, &format!("{db}presence_token_head."))?;
796        let key = add_rows(enc, pos); // enc + pos, [n, FD]
797        let mut inter: Vec<Vec<f32>> = Vec::with_capacity(NL);
798        for l in 0..NL {
799            let b = format!("{db}layers.{l}.");
800            // query position from the sine encoding of the reference boxes
801            let sine = sine_boxes(&refs);
802            let mut qp = vec![0f32; NQ * FD];
803            rh0.matmat(&sine, NQ, &mut qp, pool);
804            add_bias(&mut qp, FD, &rh0b);
805            qp.iter_mut().for_each(|v| *v = v.max(0.0)); // DecoderMLP: relu between
806            let mut qp2 = vec![0f32; NQ * FD];
807            rh1.matmat(&qp, NQ, &mut qp2, pool);
808            add_bias(&mut qp2, FD, &rh1b);
809            let qp = qp2;
810            let mut qp_full = vec![0f32; (NQ + 1) * FD];
811            qp_full[FD..].copy_from_slice(&qp);
812            let rpb = rpb_bias(&refs, &rx, &ry, n, pool)?;
813            // self attention: q=k=h+qp (pad row stays qp=0), v=h ; POST-norm
814            let hqp = add_rows(&hidden, &qp_full);
815            let qkv = lin(m, &format!("{b}self_attn.in_proj_weight"))?;
816            let ib = load_vec(m, &format!("{b}self_attn.in_proj_bias"))?;
817            let op = lin(m, &format!("{b}self_attn.out_proj.weight"))?;
818            let ob = load_vec(m, &format!("{b}self_attn.out_proj.bias"))?;
819            let mut ao = vec![0f32; (NQ + 1) * FD];
820            run_attn(&qkv, &ib, &op, &ob, &hqp, &hqp, &hidden, NQ + 1, NQ + 1, FD, FH, None, Mask::None, pool, &mut ao);
821            for (a, c) in hidden.iter_mut().zip(&ao) { *a += *c; }
822            tsd.lap("dec_self_attn");
823            let (nw, nb) = (load_vec(m, &format!("{b}norm1.weight"))?, load_vec(m, &format!("{b}norm1.bias"))?);
824            layer_norm_rows(&mut hidden, &nw, &nb, D_EPS);
825            // text cross attention
826            let hqp = add_rows(&hidden, &qp_full);
827            let qkv = lin(m, &format!("{b}ca_text.in_proj_weight"))?;
828            let ib = load_vec(m, &format!("{b}ca_text.in_proj_bias"))?;
829            let op = lin(m, &format!("{b}ca_text.out_proj.weight"))?;
830            let ob = load_vec(m, &format!("{b}ca_text.out_proj.bias"))?;
831            let mut ao = vec![0f32; (NQ + 1) * FD];
832            run_attn(&qkv, &ib, &op, &ob, &hqp, text, text, NQ + 1, SS, FD, FH, None, Mask::Key(&km), pool, &mut ao);
833            for (a, c) in hidden.iter_mut().zip(&ao) { *a += *c; }
834            tsd.lap("dec_cross_text");
835            let (nw, nb) = (load_vec(m, &format!("{b}catext_norm.weight"))?, load_vec(m, &format!("{b}catext_norm.bias"))?);
836            layer_norm_rows(&mut hidden, &nw, &nb, D_EPS);
837            // vision cross attention (keys = enc + pos, values = enc, RPB bias)
838            let hqp = add_rows(&hidden, &qp_full);
839            let qkv = lin(m, &format!("{b}cross_attn.in_proj_weight"))?;
840            let ib = load_vec(m, &format!("{b}cross_attn.in_proj_bias"))?;
841            let op = lin(m, &format!("{b}cross_attn.out_proj.weight"))?;
842            let ob = load_vec(m, &format!("{b}cross_attn.out_proj.bias"))?;
843            let mut ao = vec![0f32; (NQ + 1) * FD];
844            run_attn(&qkv, &ib, &op, &ob, &hqp, &key, enc, NQ + 1, n, FD, FH, None, Mask::Head(&rpb), pool, &mut ao);
845            for (a, c) in hidden.iter_mut().zip(&ao) { *a += *c; }
846            tsd.lap("dec_cross_enc");
847            let (nw, nb) = (load_vec(m, &format!("{b}norm2.weight"))?, load_vec(m, &format!("{b}norm2.bias"))?);
848            layer_norm_rows(&mut hidden, &nw, &nb, D_EPS);
849            // relu MLP (Sam3MLP)
850            let l1 = lin(m, &format!("{b}linear1.weight"))?;
851            let l1b = load_vec(m, &format!("{b}linear1.bias"))?;
852            let l2 = lin(m, &format!("{b}linear2.weight"))?;
853            let l2b = load_vec(m, &format!("{b}linear2.bias"))?;
854            let mut mid = vec![0f32; (NQ + 1) * FI];
855            l1.matmat(&hidden, NQ + 1, &mut mid, pool);
856            add_bias(&mut mid, FI, &l1b);
857            mid.iter_mut().for_each(|v| *v = v.max(0.0));
858            let mut fo = vec![0f32; (NQ + 1) * FD];
859            l2.matmat(&mid, NQ + 1, &mut fo, pool);
860            add_bias(&mut fo, FD, &l2b);
861            for (a, c) in hidden.iter_mut().zip(&fo) { *a += *c; }
862            tsd.lap("dec_ffn");
863            let (nw, nb) = (load_vec(m, &format!("{b}norm3.weight"))?, load_vec(m, &format!("{b}norm3.bias"))?);
864            layer_norm_rows(&mut hidden, &nw, &nb, D_EPS);
865            // box refine + intermediate (query rows only, output_layer_norm)
866            let mut qh = hidden[FD..].to_vec();
867            layer_norm_rows(&mut qh, &olnw, &olnb, D_EPS);
868            inter.push(qh.clone());
869            let d = bbox.forward(&qh, NQ, pool);
870            for (r, dd) in refs.iter_mut().zip(d.chunks_exact(4)) {
871                for k in 0..4 { r[k] = sigm(inv_sigmoid(r[k]) + dd[k]); }
872            }
873            tsd.lap("dec_box_ref");
874        }
875        let dec = inter.iter().flat_map(|v| v.iter().copied()).collect();
876        // Presence logit of the FINAL layer: row 0 (the presence token)
877        // is post-normed by the layer stack like every other row — feed
878        // it through presence_layer_norm + presence_head (relu, relu,
879        // linear — 3 layers), clamp ±10 (HF `clamp_presence_logit_max_val`).
880        let mut ph = hidden[0..FD].to_vec();
881        layer_norm_rows(&mut ph, &pres_w, &pres_b, D_EPS);
882        let pl = pres_head.forward(&ph, 1, pool);
883        let presence_logit = pl[0].clamp(-10.0, 10.0);
884        Ok((dec, refs, presence_logit))
885    }
886
887    /// Dot-product concept scoring — HF `Sam3DotProductScoring` over
888    /// the `det.scoring.*` section: the prompt MLP (2 layers, relu
889    /// between) + residual + out-norm on the resizer features,
890    /// mean-pooled over the unpadded positions, projected by
891    /// `prompt_proj`; the output-normed queries of the FINAL decoder
892    /// layer projected by `hs_proj`; the dot product scaled by
893    /// 1/sqrt(256) and clamped to ±12 (HF `clamp_max_val`). `dec` is
894    /// one layer's `[nq * FD]` queries; returns `nq` logits.
895    fn score(
896        &self,
897        text: &[f32],
898        am: &[u8],
899        dec: &[f32],
900        pool: Option<&Pool>,
901    ) -> Result<Vec<f32>> {
902        let m = &self.model;
903        let mlp = DecoderMlp::load(m, "det.scoring.prompt_mlp.")?;
904        let mut h = mlp.forward(text, SS, pool, FD);
905        for (a, b) in h.iter_mut().zip(text) {
906            *a += *b;
907        }
908        let (ow, ob) = (
909            load_vec(m, "det.scoring.prompt_mlp.out_norm.weight")?,
910            load_vec(m, "det.scoring.prompt_mlp.out_norm.bias")?,
911        );
912        layer_norm_rows(&mut h, &ow, &ob, D_EPS);
913        // Mean pool over attention_mask == 1 (HF `_pool_text_features`).
914        let n_valid = am.iter().filter(|&&v| v == 1).count().max(1) as f32;
915        let mut pooled = vec![0f32; FD];
916        for (i, &v) in am.iter().enumerate().take(SS) {
917            if v == 1 {
918                for d in 0..FD {
919                    pooled[d] += h[i * FD + d];
920                }
921            }
922        }
923        for v in pooled.iter_mut() {
924            *v /= n_valid;
925        }
926        let pt = lin(m, "det.scoring.prompt_proj.weight")?;
927        let pb = load_vec(m, "det.scoring.prompt_proj.bias")?;
928        let mut tp = vec![0f32; FD];
929        pt.matmat(&pooled, 1, &mut tp, pool);
930        add_bias(&mut tp, FD, &pb);
931        let nq = dec.len() / FD;
932        let hs = lin(m, "det.scoring.hs_proj.weight")?;
933        let hb = load_vec(m, "det.scoring.hs_proj.bias")?;
934        let mut q = vec![0f32; nq * FD];
935        hs.matmat(dec, nq, &mut q, pool);
936        add_bias(&mut q, FD, &hb);
937        let scale = 1.0 / (FD as f32).sqrt();
938        Ok(q
939            .chunks(FD)
940            .map(|r| {
941                let mut acc = 0f32;
942                for d in 0..FD {
943                    acc += r[d] * tp[d];
944                }
945                (acc * scale).clamp(-12.0, 12.0)
946            })
947            .collect())
948    }
949
950    // ── mask decoder ────────────────────────────────────────────────
951
952    /// Prompt cross-attention over the encoder states, pixel decoder
953    /// (nearest upsample + sum + conv3x3 + GroupNorm + ReLU), then
954    /// pred_masks[q] = einsum(mask_embed[q], instance_embed).
955    #[allow(clippy::too_many_arguments)]
956    fn mask_decoder(
957        &self,
958        queries: &[f32],
959        enc: &[f32],
960        fpn: &[Vec<f32>],
961        text: &[f32],
962        am: &[u8],
963        pool: Option<&Pool>,
964    ) -> Result<Vec<f32>> {
965        let m = &self.model;
966        let db = "det.seghead.";
967        let n = enc.len() / FD;
968        let mut km = vec![0f32; SS];
969        for (k, &v) in am.iter().enumerate() { km[k] = if v == 0 { NEG } else { 0.0 }; }
970        let nw = load_vec(m, &format!("{db}cross_attn_norm.weight"))?;
971        let nb = load_vec(m, &format!("{db}cross_attn_norm.bias"))?;
972        let mut h = enc.to_vec();
973        layer_norm_rows(&mut h, &nw, &nb, D_EPS);
974        let qkv = lin(m, &format!("{db}cross_attend_prompt.in_proj_weight"))?;
975        let ib = load_vec(m, &format!("{db}cross_attend_prompt.in_proj_bias"))?;
976        let op = lin(m, &format!("{db}cross_attend_prompt.out_proj.weight"))?;
977        let ob = load_vec(m, &format!("{db}cross_attend_prompt.out_proj.bias"))?;
978        let mut tsm = StageTimer::new();
979        let mut e = enc.to_vec();
980        let mut ao = vec![0f32; n * FD];
981        run_attn(&qkv, &ib, &op, &ob, &h, text, text, n, SS, FD, FH, None, Mask::Key(&km), pool, &mut ao);
982        for (a, c) in e.iter_mut().zip(&ao) { *a += *c; }
983        tsm.lap("msk_cross_attn");
984        // pixel decoder: encoder states replace level 2 (72), then upsample
985        let l72 = rows_to_chw(&e, GRID, GRID);
986        let sizes = [(144usize, 144usize), (288, 288)];
987        let mut prev = l72;
988        for (li, &(sh, sw)) in sizes.iter().enumerate() {
989            let _pp = crate::cpuprof::spans::span("mask.pixdec");
990            prev = upsample_nearest(&prev, FD, sh / 2, sh / 2, sh, sw);
991            let fw = &fpn[1 - li]; // reversed(fpn[:-1]): 144 then 288
992            for (a, b) in prev.iter_mut().zip(fw.iter()) { *a += *b; }
993            let cw = load_vec(m, &format!("{db}pixel_decoder.conv_layers.{li}.weight"))?;
994            let cb = load_vec(m, &format!("{db}pixel_decoder.conv_layers.{li}.bias"))?;
995            prev = conv2d(&cw, &cb, &prev, FD, sh, sw, 3, 3, 1, 1, pool);
996            let gw = load_vec(m, &format!("{db}pixel_decoder.norms.{li}.weight"))?;
997            let gb = load_vec(m, &format!("{db}pixel_decoder.norms.{li}.bias"))?;
998            group_norm(&mut prev, 8, &gw, &gb, 1e-5);
999            prev.iter_mut().for_each(|v| *v = v.max(0.0));
1000            drop(_pp);
1001        }
1002        tsm.lap("msk_pixel_dec");
1003        let iw = load_vec(m, &format!("{db}instance_seg_head.weight"))?;
1004        let ib2 = load_vec(m, &format!("{db}instance_seg_head.bias"))?;
1005        let inst = conv2d(&iw, &ib2, &prev, FD, MH, MH, 1, 1, 1, 0, pool);
1006        // mask embeddings from the (already output-normed) queries
1007        let mut q = queries.to_vec();
1008        for i in 0..3 {
1009            let w = lin(m, &format!("{db}mask_predictor.mask_embed.layers.{i}.weight"))?;
1010            let b = load_vec(m, &format!("{db}mask_predictor.mask_embed.layers.{i}.bias"))?;
1011            let mut o = vec![0f32; NQ * FD];
1012            w.matmat(&q, NQ, &mut o, pool);
1013            add_bias(&mut o, FD, &b);
1014            if i < 2 { o.iter_mut().for_each(|v| *v = v.max(0.0)); }
1015            q = o;
1016        }
1017        tsm.lap("msk_inst_head");
1018        let hw = MH * MH;
1019        let mut out = vec![0f32; NQ * hw];
1020        let ob2 = out.as_mut_ptr() as usize;
1021        let _pe = crate::cpuprof::spans::span("mask.einsum");
1022        let run = |s: usize, e: usize| {
1023            let o = unsafe { std::slice::from_raw_parts_mut(ob2 as *mut f32, NQ * hw) };
1024            for qi in s..e {
1025                let qq = &q[qi * FD..(qi + 1) * FD];
1026                let dst = &mut o[qi * hw..(qi + 1) * hw];
1027                for c in 0..FD {
1028                    let wgt = qq[c];
1029                    if wgt == 0.0 { continue; }
1030                    let ic = &inst[c * hw..(c + 1) * hw];
1031                    for p in 0..hw { dst[p] += wgt * ic[p]; }
1032                }
1033            }
1034        };
1035        match pool { Some(p) => p.run_rows(NQ, &run), None => run(0, NQ) }
1036        drop(_pe);
1037        tsm.lap("msk_einsum");
1038        Ok(out)
1039    }
1040}
1041
1042/// `Sam3VisionNeck` (`Sam3FPNLayer` stack) over the trunk map: the first
1043/// `n_levels` pyramid levels at 288/144/72/36, each [256, H, W]
1044/// channels-first. The DETR path uses all four; the video tracker's neck
1045/// (`fpn.sam2_convs`) only consumes the first three.
1046pub(crate) fn fpn_levels(
1047    m: &Arc<CmfModel>,
1048    p: &str,
1049    trunk: &[f32],
1050    pool: Option<&Pool>,
1051    n_levels: usize,
1052) -> Result<Vec<Vec<f32>>> {
1053    let hidden = rows_to_chw(trunk, GRID, GRID); // [1024,72,72]
1054    let scales = [4.0f32, 2.0, 1.0, 0.5];
1055    let mut levels = Vec::with_capacity(n_levels);
1056    let _p = crate::cpuprof::spans::span("fpn.levels");
1057    for (i, &s) in scales.iter().enumerate().take(n_levels) {
1058        let (mut ci, mut ch, mut cw) = (TD, GRID, GRID);
1059        let mut cur = hidden.clone();
1060        match s {
1061            4.0 => {
1062                let w0 = load_vec(m, &format!("{p}.{i}.dconv_2x2_0.weight"))?;
1063                let b0 = load_vec(m, &format!("{p}.{i}.dconv_2x2_0.bias"))?;
1064                cur = convt2x2(&w0, &b0, &cur, ci, ch, cw, pool);
1065                (ch, cw, ci) = (ch * 2, cw * 2, b0.len());
1066                gelu_slice(&mut cur);
1067                let w1 = load_vec(m, &format!("{p}.{i}.dconv_2x2_1.weight"))?;
1068                let b1 = load_vec(m, &format!("{p}.{i}.dconv_2x2_1.bias"))?;
1069                cur = convt2x2(&w1, &b1, &cur, ci, ch, cw, pool);
1070                (ch, cw, ci) = (ch * 2, cw * 2, b1.len());
1071            }
1072            2.0 => {
1073                let w0 = load_vec(m, &format!("{p}.{i}.dconv_2x2.weight"))?;
1074                let b0 = load_vec(m, &format!("{p}.{i}.dconv_2x2.bias"))?;
1075                cur = convt2x2(&w0, &b0, &cur, ci, ch, cw, pool);
1076                (ch, cw, ci) = (ch * 2, cw * 2, b0.len());
1077            }
1078            1.0 => {}
1079            _ => {
1080                cur = maxpool2(&cur, ci, ch, cw);
1081                (ch, cw) = (ch / 2, cw / 2);
1082            }
1083        }
1084        let w1 = load_vec(m, &format!("{p}.{i}.conv_1x1.weight"))?;
1085        let b1 = load_vec(m, &format!("{p}.{i}.conv_1x1.bias"))?;
1086        cur = conv2d(&w1, &b1, &cur, ci, ch, cw, 1, 1, 1, 0, pool);
1087        ci = b1.len();
1088        let w3 = load_vec(m, &format!("{p}.{i}.conv_3x3.weight"))?;
1089        let b3 = load_vec(m, &format!("{p}.{i}.conv_3x3.bias"))?;
1090        cur = conv2d(&w3, &b3, &cur, ci, ch, cw, 3, 3, 1, 1, pool);
1091        levels.push(cur);
1092    }
1093    Ok(levels)
1094}
1095
1096// ───────────────────── model pieces ─────────────────────
1097
1098/// Sam3DecoderMLP, 2 layers: relu(l1), l2 (with biases).
1099struct DecoderMlp {
1100    w0: QTensor,
1101    b0: Vec<f32>,
1102    w1: QTensor,
1103    b1: Vec<f32>,
1104}
1105
1106impl DecoderMlp {
1107    fn load(m: &Arc<CmfModel>, prefix: &str) -> Result<Self> {
1108        Ok(Self {
1109            w0: lin(m, &format!("{prefix}layers.0.weight"))?,
1110            b0: load_vec(m, &format!("{prefix}layers.0.bias"))?,
1111            w1: lin(m, &format!("{prefix}layers.1.weight"))?,
1112            b1: load_vec(m, &format!("{prefix}layers.1.bias"))?,
1113        })
1114    }
1115    fn forward(&self, x: &[f32], rows: usize, pool: Option<&Pool>, out_w: usize) -> Vec<f32> {
1116        let mut h = vec![0f32; rows * self.b0.len()];
1117        self.w0.matmat(x, rows, &mut h, pool);
1118        add_bias(&mut h, self.b0.len(), &self.b0);
1119        h.iter_mut().for_each(|v| *v = v.max(0.0));
1120        let mut o = vec![0f32; rows * out_w];
1121        self.w1.matmat(&h, rows, &mut o, pool);
1122        add_bias(&mut o, out_w, &self.b1);
1123        o
1124    }
1125}
1126
1127/// Sam3DecoderMLP, 3 layers: relu(l1), relu(l2), l3 (box head).
1128struct DecoderMlp3 {
1129    w0: QTensor,
1130    b0: Vec<f32>,
1131    w1: QTensor,
1132    b1: Vec<f32>,
1133    w2: QTensor,
1134    b2: Vec<f32>,
1135}
1136
1137impl DecoderMlp3 {
1138    fn load3(m: &Arc<CmfModel>, prefix: &str) -> Result<Self> {
1139        Ok(Self {
1140            w0: lin(m, &format!("{prefix}layers.0.weight"))?,
1141            b0: load_vec(m, &format!("{prefix}layers.0.bias"))?,
1142            w1: lin(m, &format!("{prefix}layers.1.weight"))?,
1143            b1: load_vec(m, &format!("{prefix}layers.1.bias"))?,
1144            w2: lin(m, &format!("{prefix}layers.2.weight"))?,
1145            b2: load_vec(m, &format!("{prefix}layers.2.bias"))?,
1146        })
1147    }
1148    fn forward(&self, x: &[f32], rows: usize, pool: Option<&Pool>) -> Vec<f32> {
1149        let mut d = vec![0f32; rows * self.b0.len()];
1150        self.w0.matmat(x, rows, &mut d, pool);
1151        add_bias(&mut d, self.b0.len(), &self.b0);
1152        d.iter_mut().for_each(|v| *v = v.max(0.0));
1153        let mut d2 = vec![0f32; rows * self.b1.len()];
1154        self.w1.matmat(&d, rows, &mut d2, pool);
1155        add_bias(&mut d2, self.b1.len(), &self.b1);
1156        d2.iter_mut().for_each(|v| *v = v.max(0.0));
1157        let mut o = vec![0f32; rows * self.b2.len()];
1158        self.w2.matmat(&d2, rows, &mut o, pool);
1159        add_bias(&mut o, self.b2.len(), &self.b2);
1160        o
1161    }
1162}
1163
1164pub(crate) fn sigm(x: f32) -> f32 {
1165    1.0 / (1.0 + (-x).exp())
1166}
1167
1168fn inv_sigmoid(x: f32) -> f32 {
1169    let x = x.clamp(0.0, 1.0);
1170    ((x.max(1e-3)) / ((1.0 - x).max(1e-3))).ln()
1171}
1172
1173fn xyxy(boxes: &[[f32; 4]]) -> Vec<f32> {
1174    let mut out = vec![0f32; boxes.len() * 4];
1175    for (i, b) in boxes.iter().enumerate() {
1176        out[i * 4] = b[0] - 0.5 * b[2];
1177        out[i * 4 + 1] = b[1] - 0.5 * b[3];
1178        out[i * 4 + 2] = b[0] + 0.5 * b[2];
1179        out[i * 4 + 3] = b[1] + 0.5 * b[3];
1180    }
1181    out
1182}
1183
1184pub(crate) fn lin(m: &Arc<CmfModel>, name: &str) -> Result<QTensor> {
1185    let _p = crate::cpuprof::spans::span("qtensor.wrap");
1186    QTensor::from_model(m, name).map_err(|e| anyhow!(e))
1187}
1188
1189/// Dequantize a tensor to flat f32 (norms, biases, tables, convs).
1190pub(crate) fn load_vec(m: &Arc<CmfModel>, name: &str) -> Result<Vec<f32>> {
1191    let _p = crate::cpuprof::spans::span("load.decode");
1192    let e = m.tensor(name).ok_or_else(|| anyhow!("tensor '{name}' not found"))?;
1193    if matches!(e.dtype, TensorDtype::U8) { return Err(anyhow!("'{name}' is u8")); }
1194    let bytes = m.entry_bytes(e);
1195    let mut dst = vec![0f32; e.n_elems()];
1196    dequant_tensor(e, bytes, &mut dst).map_err(|er| anyhow!("{name}: {er}"))?;
1197    Ok(dst)
1198}
1199
1200/// `gelu_erf` over a whole buffer, one `SAM3_PROF` span for the loop.
1201pub(crate) fn gelu_slice(v: &mut [f32]) {
1202    let _p = crate::cpuprof::spans::span("op.gelu");
1203    v.iter_mut().for_each(|x| *x = gelu_erf(*x));
1204}
1205
1206/// Row-major [rows, cols] += bias (broadcast over rows).
1207pub(crate) fn add_bias(out: &mut [f32], cols: usize, bias: &[f32]) {
1208    debug_assert_eq!(bias.len(), cols);
1209    for r in out.chunks_exact_mut(cols) {
1210        for (v, b) in r.iter_mut().zip(bias) { *v += *b; }
1211    }
1212}
1213
1214/// LayerNorm over rows of `x` (row length = w.len()).
1215pub(crate) fn layer_norm_rows(x: &mut [f32], w: &[f32], b: &[f32], eps: f32) {
1216    let _p = crate::cpuprof::spans::span("op.layernorm");
1217    let d = w.len();
1218    for r in x.chunks_exact_mut(d) {
1219        let mean = r.iter().sum::<f32>() / d as f32;
1220        let var = r.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / d as f32;
1221        let inv = 1.0 / (var + eps).sqrt();
1222        for (v, (&ww, &bb)) in r.iter_mut().zip(w.iter().zip(b.iter())) {
1223            *v = (*v - mean) * inv * ww + bb;
1224        }
1225    }
1226}
1227
1228/// Axial 2D RoPE in place on a fused QKV block: rotates the q part
1229/// (dims [0, dim)) and the k part ([dim, 2*dim)); v untouched. freqs_cis
1230/// = [n, 32, 2] = (cos, sin) per pair; pairs 0..15 from w-position,
1231/// 16..31 from h-position; per head block of 64 dims (16 heads share the
1232/// same position-dependent table).
1233fn apply_rope(buf: &mut [f32], n: usize, stride: usize, width: usize, fc: &[f32]) {
1234    let npairs = 32;
1235    debug_assert_eq!(fc.len(), n * npairs * 2);
1236    let blocks = width / 64;
1237    for i in 0..n {
1238        let f = &fc[i * npairs * 2..(i + 1) * npairs * 2];
1239        let row = &mut buf[i * stride..i * stride + width];
1240        for b in 0..blocks {
1241            let base = b * 64;
1242            for j in 0..npairs {
1243                let (c, s) = (f[j * 2], f[j * 2 + 1]);
1244                let (a, d) = (row[base + j * 2], row[base + j * 2 + 1]);
1245                row[base + j * 2] = a * c - d * s;
1246                row[base + j * 2 + 1] = d * c + a * s;
1247            }
1248        }
1249    }
1250}
1251
1252/// RAII bypass of the f16-staged coop kernel for the calling thread —
1253/// the attention arm needs the pure-f32 scalar GEMM to stay inside the
1254/// parity gate (the coop kernel rounds operands to f16: measured ~1e-3
1255/// per-op, which compounds through a 32-layer trunk to rel 0.36).
1256struct CoopBypass;
1257
1258impl CoopBypass {
1259    fn new() -> Self {
1260        #[cfg(feature = "gpu")]
1261        crate::gpu_wgpu::COOP_BYPASS.with(|b| b.set(true));
1262        Self
1263    }
1264}
1265
1266impl Drop for CoopBypass {
1267    fn drop(&mut self) {
1268        #[cfg(feature = "gpu")]
1269        crate::gpu_wgpu::COOP_BYPASS.with(|b| b.set(false));
1270    }
1271}
1272
1273/// GPU arm for `run_attn`'s core (QKᵀ softmax A·V), owner GPU port.
1274///
1275/// Per head: Q/K/V rows are packed contiguous, QKᵀ and A·V run as f32
1276/// `gemm_nt_f32` GEMMs (transient both operands are activations), the
1277/// scale/mask/softmax step stays on the CPU in exactly the scalar arm's
1278/// order — only the f32 dot-product summation order differs (allowed:
1279/// cos >= 0.999 vs the CPU arm). Returns false on ANY refusal — backend
1280/// down, `CMF_GPU=0`, inside `cpu_scope`, job under the GEMM floor, a
1281/// device decline mid-loop — and leaves `out` untouched, so the caller
1282/// falls back to the scalar arm. `CMF_SAM3_GPU=0` disables the arm.
1283#[allow(clippy::too_many_arguments)]
1284fn attn_core_gpu(
1285    qkv_q: &[f32],
1286    k: &[f32],
1287    v: &[f32],
1288    n_q: usize,
1289    n_k: usize,
1290    dim: usize,
1291    heads: usize,
1292    mask: Mask<'_>,
1293    scale: f32,
1294    out: &mut [f32],
1295) -> bool {
1296    #[cfg(feature = "gpu")]
1297    {
1298        if std::env::var("CMF_SAM3_GPU").as_deref() == Ok("0") {
1299            return false;
1300        }
1301        if !crate::gpu::enabled_here() {
1302            return false;
1303        }
1304        let hd = dim / heads;
1305        if hd == 0 || out.len() < n_q * dim {
1306            return false;
1307        }
1308        // Same floor gemm_nt enforces, checked before any packing: both
1309        // GEMMs carry the identical n_q * n_k * hd MAC product.
1310        if n_q.saturating_mul(n_k).saturating_mul(hd) < (1 << 22) {
1311            return false;
1312        }
1313        let h3 = dim * 3;
1314        // Q/K packed [.., hd] rows; V packed transposed (rows = hd) so
1315        // A·V is the same NT GEMM: out[n_q, hd] = A[n_q, n_k]·Vᵀ[hd, n_k].
1316        let _byp = CoopBypass::new();
1317        let mut qpk = vec![0f32; n_q * hd];
1318        let mut kpk = vec![0f32; n_k * hd];
1319        let mut vtp = vec![0f32; hd * n_k];
1320        let mut raw = vec![0f32; n_q * n_k];
1321        let mut outt = vec![0f32; n_q * hd];
1322        let mut acc = vec![0f32; n_q * dim];
1323        // The pure-f32 scalar arm refuses grids over 2^22 workgroups (a
1324        // hang guard) — QKᵀ over a big square (n_q * n_k) is chunked
1325        // along query rows; output rows are independent, so chunking
1326        // changes nothing but the dispatch.
1327        let qk_chunk = ((1usize << 22) / n_k.max(1)).max(1);
1328        for t in 0..heads {
1329            let off = t * hd;
1330            for i in 0..n_q {
1331                qpk[i * hd..(i + 1) * hd]
1332                    .copy_from_slice(&qkv_q[i * h3 + off..i * h3 + off + hd]);
1333            }
1334            for j in 0..n_k {
1335                kpk[j * hd..(j + 1) * hd]
1336                    .copy_from_slice(&k[j * h3 + dim + off..j * h3 + dim + off + hd]);
1337            }
1338            for d in 0..hd {
1339                let rowd = &mut vtp[d * n_k..(d + 1) * n_k];
1340                for (j, rv) in rowd.iter_mut().enumerate() {
1341                    *rv = v[j * h3 + 2 * dim + off + d];
1342                }
1343            }
1344            // QKᵀ: raw[i, j] = qpk[i]·kpk[j]  (chunked over query rows)
1345            let mut i0 = 0usize;
1346            while i0 < n_q {
1347                let i1 = (i0 + qk_chunk).min(n_q);
1348                if !crate::gpu_wgpu::gemm_nt_f32_transient(
1349                    &qpk[i0 * hd..i1 * hd],
1350                    &kpk,
1351                    &mut raw[i0 * n_k..i1 * n_k],
1352                    i1 - i0,
1353                    hd,
1354                    n_k,
1355                ) {
1356                    return false;
1357                }
1358                i0 = i1;
1359            }
1360            // scale + mask + stable softmax — the scalar arm's order.
1361            let mut rowmax = vec![0f32; n_q];
1362            for i in 0..n_q {
1363                let base = i * n_k;
1364                let mut mx = f32::NEG_INFINITY;
1365                for j in 0..n_k {
1366                    let mut val = raw[base + j] * scale;
1367                    val += match mask {
1368                        Mask::None => 0.0,
1369                        Mask::Key(km) => km[j],
1370                        Mask::Add(am) => am[i * n_k + j],
1371                        Mask::Head(hm) => hm[(t * n_q + i) * n_k + j],
1372                    };
1373                    raw[base + j] = val;
1374                    if val > mx {
1375                        mx = val;
1376                    }
1377                }
1378                rowmax[i] = mx;
1379                let mut sum = 0.0f32;
1380                for j in 0..n_k {
1381                    let e = (raw[base + j] - mx).exp();
1382                    raw[base + j] = e;
1383                    sum += e;
1384                }
1385                let inv = 1.0 / sum;
1386                for j in 0..n_k {
1387                    raw[base + j] *= inv;
1388                }
1389            }
1390            // A·V: outt[i, d] = raw[i]·vtp[d]
1391            if !crate::gpu_wgpu::gemm_nt_f32_transient(&raw, &vtp, &mut outt, n_q, n_k, hd) {
1392                return false;
1393            }
1394            for i in 0..n_q {
1395                acc[i * dim + off..i * dim + off + hd]
1396                    .copy_from_slice(&outt[i * hd..(i + 1) * hd]);
1397            }
1398        }
1399        out.copy_from_slice(&acc);
1400        true
1401    }
1402    #[cfg(not(feature = "gpu"))]
1403    {
1404        let _ = (qkv_q, k, v, n_q, n_k, dim, heads, mask, scale, out);
1405        false
1406    }
1407}
1408
1409/// Multi-head attention for one sequence: fused QKV projection
1410/// (`w_qkv` rows = 3*dim laid out [q | k | v] per row, `qb` its bias),
1411/// optional axial RoPE on q/k, additive mask, softmax, output projection
1412/// (`w_o` + `ob`). `out` is [n_q, dim]; scale = head_dim^-0.5.
1413#[allow(clippy::too_many_arguments)]
1414pub(crate) fn run_attn(
1415    w_qkv: &QTensor,
1416    qb: &[f32],
1417    w_o: &QTensor,
1418    ob: &[f32],
1419    xs_q: &[f32],
1420    xs_k: &[f32],
1421    xs_v: &[f32],
1422    n_q: usize,
1423    n_k: usize,
1424    dim: usize,
1425    heads: usize,
1426    rope: Option<&[f32]>,
1427    mask: Mask,
1428    pool: Option<&Pool>,
1429    out: &mut [f32],
1430) {
1431    let h3 = dim * 3;
1432    let hd = dim / heads;
1433    let scale = 1.0 / (hd as f32).sqrt();
1434    let _tp = std::time::Instant::now();
1435    let mut qkv_q = vec![0f32; n_q * h3];
1436    let _pg = crate::cpuprof::spans::span("attn.gemm");
1437    w_qkv.matmat(xs_q, n_q, &mut qkv_q, pool);
1438    add_bias(&mut qkv_q, h3, qb);
1439    let same_k = xs_k.as_ptr() == xs_q.as_ptr();
1440    let mut kbuf = Vec::new();
1441    if !same_k {
1442        kbuf = vec![0f32; n_k * h3];
1443        w_qkv.matmat(xs_k, n_k, &mut kbuf, pool);
1444        add_bias(&mut kbuf, h3, qb);
1445    }
1446    let same_v = xs_v.as_ptr() == xs_q.as_ptr();
1447    let mut vbuf = Vec::new();
1448    if !same_v {
1449        vbuf = vec![0f32; n_k * h3];
1450        w_qkv.matmat(xs_v, n_k, &mut vbuf, pool);
1451        add_bias(&mut vbuf, h3, qb);
1452    }
1453    drop(_pg);
1454    if let Some(fc) = rope {
1455        // rotate q (and k when they share the buffer) over the first 2*dim dims
1456        apply_rope(&mut qkv_q, n_q, h3, dim * 2, fc);
1457        if !same_k {
1458            apply_rope(&mut kbuf, n_k, h3, dim * 2, fc);
1459        }
1460    }
1461    ATT_PROJ_NS.fetch_add(_tp.elapsed().as_nanos() as u64, Ordering::Relaxed);
1462    let _tc = std::time::Instant::now();
1463    let qkv_k: &[f32] = if same_k { &qkv_q } else { &kbuf };
1464    let qkv_v: &[f32] = if same_v { &qkv_q } else { &vbuf };
1465    let mut attn = vec![0f32; n_q * dim];
1466    if attn_core_gpu(&qkv_q, qkv_k, qkv_v, n_q, n_k, dim, heads, mask, scale, &mut attn) {
1467        ATT_CORE_NS.fetch_add(_tc.elapsed().as_nanos() as u64, Ordering::Relaxed);
1468        let _tw = std::time::Instant::now();
1469        w_o.matmat(&attn, n_q, out, pool);
1470        add_bias(out, dim, ob);
1471        ATT_PROJ_NS.fetch_add(_tw.elapsed().as_nanos() as u64, Ordering::Relaxed);
1472        return;
1473    }
1474    let ab = attn.as_mut_ptr() as usize;
1475    let _pc = crate::cpuprof::spans::span("attn.core");
1476    // Attention core as two packed GEMMs per head over each row chunk —
1477    // QKᵀ then softmax then AV — instead of the per-(row, head) scalar
1478    // loops with a dot()/axpy() call per key. matrixmultiply packs K/V
1479    // once per call (the old code rebuilt a hd-wide dot and re-sliced a
1480    // strided V row n_q×heads×n_k times: 1.0G+ tiny calls/frame at
1481    // ~115 µs per (row, head) group). Per-element mask/softmax values are
1482    // unchanged; only the reduction ORDER differs (gates are numeric).
1483    let run = |s: usize, e: usize| {
1484        if s >= e {
1485            return;
1486        }
1487        let a = unsafe { std::slice::from_raw_parts_mut(ab as *mut f32, n_q * dim) };
1488        let rows = e - s;
1489        let mut scores = vec![0f32; rows * n_k];
1490        let prof = crate::cpuprof::spans::on();
1491        for t in 0..heads {
1492            let tq = prof.then(std::time::Instant::now);
1493            // C[rows, n_k] = scale · Q[rows, hd] × Kᵀ (K head rows, stride h3)
1494            crate::qtensor::sgemm_public(
1495                rows,
1496                n_k,
1497                hd,
1498                scale,
1499                &qkv_q[s * h3 + t * hd..],
1500                h3,
1501                &qkv_k[dim + t * hd..],
1502                h3,
1503                true,
1504                &mut scores,
1505                n_k,
1506            );
1507            match mask {
1508                Mask::None => {}
1509                Mask::Key(km) => {
1510                    for r in 0..rows {
1511                        let sr = &mut scores[r * n_k..(r + 1) * n_k];
1512                        for (v, &m) in sr.iter_mut().zip(km) {
1513                            *v += m;
1514                        }
1515                    }
1516                }
1517                Mask::Add(am) => {
1518                    for (r, i) in (s..e).enumerate() {
1519                        let sr = &mut scores[r * n_k..(r + 1) * n_k];
1520                        for (v, &m) in sr.iter_mut().zip(&am[i * n_k..(i + 1) * n_k]) {
1521                            *v += m;
1522                        }
1523                    }
1524                }
1525                Mask::Head(hm) => {
1526                    for (r, i) in (s..e).enumerate() {
1527                        let base = (t * n_q + i) * n_k;
1528                        let sr = &mut scores[r * n_k..(r + 1) * n_k];
1529                        for (v, &m) in sr.iter_mut().zip(&hm[base..base + n_k]) {
1530                            *v += m;
1531                        }
1532                    }
1533                }
1534            }
1535            let tm = prof.then(std::time::Instant::now);
1536            // Stable softmax: the trunk's ViT scores reach ~155, so a
1537            // raw exp() overflows to inf for the row-max and the sum
1538            // turns inf → inv = 0 → the whole attention row silently
1539            // collapses to zeros. Subtract the row max (`mx`).
1540            for r in 0..rows {
1541                let sr = &mut scores[r * n_k..(r + 1) * n_k];
1542                let mut mx = f32::NEG_INFINITY;
1543                for &v in sr.iter() {
1544                    if v > mx {
1545                        mx = v;
1546                    }
1547                }
1548                let mut sum = 0.0f32;
1549                for v in sr.iter_mut() {
1550                    *v = (*v - mx).exp();
1551                    sum += *v;
1552                }
1553                let inv = 1.0 / sum;
1554                for v in sr.iter_mut() {
1555                    *v *= inv;
1556                }
1557            }
1558            let ta = prof.then(std::time::Instant::now);
1559            // C[rows, hd] = S[rows, n_k] × V_head (rows stride h3) → out
1560            crate::qtensor::sgemm_public(
1561                rows,
1562                hd,
1563                n_k,
1564                1.0,
1565                &scores,
1566                n_k,
1567                &qkv_v[2 * dim + t * hd..],
1568                h3,
1569                false,
1570                &mut a[s * dim + t * hd..],
1571                dim,
1572            );
1573            if let (Some(t0), Some(t1), Some(t2)) = (tq, tm, ta) {
1574                let t3 = std::time::Instant::now();
1575                crate::cpuprof::spans::add("attn.qk", t1 - t0);
1576                crate::cpuprof::spans::add("attn.sm", t2 - t1);
1577                crate::cpuprof::spans::add("attn.av", t3 - t2);
1578            }
1579        }
1580    };
1581    match pool { Some(p) => p.run_rows(n_q, &run), None => run(0, n_q) }
1582    drop(_pc);
1583    let _pg = crate::cpuprof::spans::span("attn.gemm");
1584    ATT_CORE_NS.fetch_add(_tc.elapsed().as_nanos() as u64, Ordering::Relaxed);
1585    let _tw = std::time::Instant::now();
1586    w_o.matmat(&attn, n_q, out, pool);
1587    add_bias(out, dim, ob);
1588    ATT_PROJ_NS.fetch_add(_tw.elapsed().as_nanos() as u64, Ordering::Relaxed);
1589}
1590
1591// ─────────── convolutions (NCHW f32, explicit shapes) ───────────
1592
1593pub(crate) fn conv2d(
1594    w: &[f32],
1595    b: &[f32],
1596    x: &[f32],
1597    ci: usize,
1598    h: usize,
1599    wid: usize,
1600    kh: usize,
1601    kw: usize,
1602    stride: usize,
1603    pad: usize,
1604    pool: Option<&Pool>,
1605) -> Vec<f32> {
1606    let co = b.len();
1607    debug_assert_eq!(w.len(), co * ci * kh * kw);
1608    debug_assert_eq!(x.len(), ci * h * wid);
1609    let oh = (h + 2 * pad - kh) / stride + 1;
1610    let ow = (wid + 2 * pad - kw) / stride + 1;
1611    let _p = crate::cpuprof::spans::span("op.conv2d");
1612    let mut out = vec![0f32; co * oh * ow];
1613    let ob = out.as_mut_ptr() as usize;
1614    // One output ROW per unit instead of one pixel: the (c, dy, dx)
1615    // accumulation order per element is unchanged (results stay bit-exact),
1616    // but the innermost loop becomes a contiguous `orow[lo..hi] += w *
1617    // xrow[..]` slice that auto-vectorizes — the pixel loop fed LLVM a
1618    // strided load, a div/mod for (oy, ox) and two bounds checks per tap,
1619    // and ran ~50% of the SAM3 image forward scalar.
1620    let run = |s: usize, e: usize| {
1621        let o = unsafe { std::slice::from_raw_parts_mut(ob as *mut f32, co * oh * ow) };
1622        for r in s..e {
1623            let c_o = r / oh;
1624            let oy = r % oh;
1625            let obase = c_o * oh * ow + oy * ow;
1626            let orow = &mut o[obase..obase + ow];
1627            orow.fill(b[c_o]);
1628            for c in 0..ci {
1629                for dy in 0..kh {
1630                    let iy = oy * stride + dy;
1631                    if iy < pad || iy - pad >= h { continue; }
1632                    let xrow = &x[(c * h + iy - pad) * wid..][..wid];
1633                    for dx in 0..kw {
1634                        let wv = w[((c_o * ci + c) * kh + dy) * kw + dx];
1635                        // ox with 0 <= ox*stride + dx - pad < wid, 0 <= ox < ow
1636                        let lo = if pad > dx {
1637                            (pad - dx).div_ceil(stride)
1638                        } else {
1639                            0
1640                        };
1641                        if lo >= ow { continue; }
1642                        let hi_excl = (pad + wid - dx).div_ceil(stride).min(ow);
1643                        if lo >= hi_excl { continue; }
1644                        if stride == 1 {
1645                            // vectorizable: both slices are contiguous
1646                            let xo = lo + dx - pad;
1647                            let hi = hi_excl;
1648                            for (a, b) in orow[lo..hi].iter_mut().zip(&xrow[xo..xo + (hi - lo)]) {
1649                                *a += wv * *b;
1650                            }
1651                        } else {
1652                            for ox in lo..hi_excl {
1653                                orow[ox] += wv * xrow[ox * stride + dx - pad];
1654                            }
1655                        }
1656                    }
1657                }
1658            }
1659        }
1660    };
1661    match pool { Some(p) => p.run_rows(co * oh, &run), None => run(0, co * oh) }
1662    out
1663}
1664
1665/// ConvTranspose2d(k=2, s=2): w is [ci, co, 2, 2]; x is [ci, h, wid].
1666pub(crate) fn convt2x2(w: &[f32], b: &[f32], x: &[f32], ci: usize, h: usize, wid: usize, pool: Option<&Pool>) -> Vec<f32> {
1667    let co = b.len();
1668    debug_assert_eq!(w.len(), ci * co * 4);
1669    let (oh, ow) = (h * 2, wid * 2);
1670    let _p = crate::cpuprof::spans::span("op.convt");
1671    let mut out = vec![0f32; co * oh * ow];
1672    let ob = out.as_mut_ptr() as usize;
1673    // Per output row: (kh, kx) = (oy & 1, ox & 1), so even columns take
1674    // w[.., 0] and odd columns w[.., 1] — accumulate into two contiguous
1675    // scratch rows (vectorizable axpy over x, per-channel order kept →
1676    // bit-exact) and interleave once at the end.
1677    let run = |s: usize, e: usize| {
1678        let o = unsafe { std::slice::from_raw_parts_mut(ob as *mut f32, co * oh * ow) };
1679        let mut t0 = vec![0f32; wid];
1680        let mut t1 = vec![0f32; wid];
1681        for r in s..e {
1682            let c_o = r / oh;
1683            let oy = r % oh;
1684            let (slot, iy) = (oy & 1, oy >> 1);
1685            t0.fill(b[c_o]);
1686            t1.fill(b[c_o]);
1687            for c in 0..ci {
1688                let xrow = &x[(c * h + iy) * wid..][..wid];
1689                let wb = &w[(c * co + c_o) * 4 + slot * 2..][..2];
1690                let (w0, w1) = (wb[0], wb[1]);
1691                for (t, &xv) in t0.iter_mut().zip(xrow) {
1692                    *t += w0 * xv;
1693                }
1694                for (t, &xv) in t1.iter_mut().zip(xrow) {
1695                    *t += w1 * xv;
1696                }
1697            }
1698            let obase = c_o * oh * ow + oy * ow;
1699            for j in 0..wid {
1700                o[obase + 2 * j] = t0[j];
1701                o[obase + 2 * j + 1] = t1[j];
1702            }
1703        }
1704    };
1705    match pool { Some(p) => p.run_rows(co * oh, &run), None => run(0, co * oh) }
1706    out
1707}
1708
1709pub(crate) fn maxpool2(x: &[f32], ci: usize, h: usize, wid: usize) -> Vec<f32> {
1710    let (oh, ow) = (h / 2, wid / 2);
1711    let mut out = vec![0f32; ci * oh * ow];
1712    for c in 0..ci {
1713        for oy in 0..oh {
1714            for ox in 0..ow {
1715                let mut mx = f32::NEG_INFINITY;
1716                for dy in 0..2 {
1717                    for dx in 0..2 {
1718                        let v = x[(c * h + oy * 2 + dy) * wid + ox * 2 + dx];
1719                        if v > mx { mx = v; }
1720                    }
1721                }
1722                out[(c * oh + oy) * ow + ox] = mx;
1723            }
1724        }
1725    }
1726    out
1727}
1728
1729/// Nearest-neighbour upsample of [ci, h, wid] to [ci, sh, sw].
1730pub(crate) fn upsample_nearest(x: &[f32], ci: usize, h: usize, wid: usize, sh: usize, sw: usize) -> Vec<f32> {
1731    let mut out = vec![0f32; ci * sh * sw];
1732    for c in 0..ci {
1733        for oy in 0..sh {
1734            let iy = oy * h / sh;
1735            for ox in 0..sw {
1736                let ix = ox * wid / sw;
1737                out[(c * sh + oy) * sw + ox] = x[(c * h + iy) * wid + ix];
1738            }
1739        }
1740    }
1741    out
1742}
1743
1744/// GroupNorm(eps=1e-5) over channels of [c, h, w].
1745pub(crate) fn group_norm(x: &mut [f32], groups: usize, w: &[f32], b: &[f32], eps: f32) {
1746    let co = w.len();
1747    let gsz = co / groups;
1748    let rest = x.len() / co;
1749    for g in 0..groups {
1750        let s = g * gsz * rest;
1751        let e = s + gsz * rest;
1752        let mean = x[s..e].iter().sum::<f32>() / (e - s) as f32;
1753        let var = x[s..e].iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / (e - s) as f32;
1754        let inv = 1.0 / (var + eps).sqrt();
1755        for (i, v) in x[s..e].iter_mut().enumerate() {
1756            let ch = g * gsz + (i / rest);
1757            *v = (*v - mean) * inv * w[ch] + b[ch];
1758        }
1759    }
1760}
1761
1762// ─────────── layout helpers ───────────
1763
1764pub(crate) fn rows_to_chw(rows: &[f32], h: usize, w: usize) -> Vec<f32> {
1765    let _p = crate::cpuprof::spans::span("op.layout");
1766    let n = h * w;
1767    let c = rows.len() / n;
1768    let mut out = vec![0f32; c * h * w];
1769    for ch in 0..c {
1770        for y in 0..h {
1771            for x in 0..w { out[(ch * h + y) * w + x] = rows[(y * w + x) * c + ch]; }
1772        }
1773    }
1774    out
1775}
1776
1777pub(crate) fn chw_to_rows(chw: &[f32], h: usize, w: usize) -> Vec<f32> {
1778    let _p = crate::cpuprof::spans::span("op.layout");
1779    let c = chw.len() / (h * w);
1780    let mut out = vec![0f32; h * w * c];
1781    for ch in 0..c {
1782        for y in 0..h {
1783            for x in 0..w { out[(y * w + x) * c + ch] = chw[(ch * h + y) * w + x]; }
1784        }
1785    }
1786    out
1787}
1788
1789/// Normalized sine position encoding for an FPN level: rows [h*w, 2n]
1790/// (pos_y then pos_x), scale 2π, temperature 10000.
1791pub(crate) fn sine_pos(h: usize, w: usize, n: usize) -> Vec<f32> {
1792    let _p = crate::cpuprof::spans::span("op.sine_pos");
1793    let scale = std::f32::consts::TAU;
1794    let mut dim_t = vec![0f32; n];
1795    for i in 0..n { dim_t[i] = 10000f32.powf((2 * (i / 2)) as f32 / n as f32); }
1796    let mut out = vec![0f32; h * w * n * 2];
1797    let (hn, wn) = (h as f32, w as f32);
1798    for y in 0..h {
1799        let yy = (y + 1) as f32 / (hn + 1e-6) * scale;
1800        for x in 0..w {
1801            let xx = (x + 1) as f32 / (wn + 1e-6) * scale;
1802            let row = (y * w + x) * n * 2;
1803            for i in 0..n {
1804                let (py, px) = (yy / dim_t[i], xx / dim_t[i]);
1805                if i % 2 == 0 {
1806                    out[row + i] = py.sin();
1807                    out[row + n + i] = px.sin();
1808                } else {
1809                    out[row + i] = py.cos();
1810                    out[row + n + i] = px.cos();
1811                }
1812            }
1813        }
1814    }
1815    out
1816}
1817
1818/// Sine box embedding (decoder query_pos input): [N, 4*128] =
1819/// cat(pos_y, pos_x, pos_w, pos_h) for boxes in (cx, cy, w, h),
1820/// scale 2π (Sam3SinePositionEmbedding.encode_boxes).
1821fn sine_boxes(boxes: &[[f32; 4]]) -> Vec<f32> {
1822    let nf = 128;
1823    let scale = std::f32::consts::TAU;
1824    let mut dim_t = vec![0f32; nf];
1825    for i in 0..nf { dim_t[i] = 10000f32.powf((2 * (i / 2)) as f32 / nf as f32); }
1826    let mut out = vec![0f32; boxes.len() * 4 * nf];
1827    for (q, b) in boxes.iter().enumerate() {
1828        let base = q * 4 * nf;
1829        for (ci, v) in [b[1], b[0], b[2], b[3]].iter().enumerate() {
1830            for i in 0..nf {
1831                let val = v * scale / dim_t[i];
1832                out[base + ci * nf + i] = if i % 2 == 0 { val.sin() } else { val.cos() };
1833            }
1834        }
1835    }
1836    out
1837}
1838
1839/// Per-head log box-RPB additive bias: [heads, NQ+1, n]. Row 0 (the
1840/// presence token) is zero — HF pads the query dim from NQ to NQ+1.
1841fn rpb_bias(refs: &[[f32; 4]], rx: &DecoderMlp, ry: &DecoderMlp, n: usize, pool: Option<&Pool>) -> Result<Vec<f32>> {
1842    let gh = GRID;
1843    debug_assert_eq!(n, gh * gh);
1844    let l2_8 = 8.0f32.ln();
1845    let mut dy = vec![0f32; refs.len() * gh * 2];
1846    let mut dx = vec![0f32; refs.len() * gh * 2];
1847    for (q, r) in refs.iter().enumerate() {
1848        let x1 = r[0] - 0.5 * r[2];
1849        let y1 = r[1] - 0.5 * r[3];
1850        let x2 = r[0] + 0.5 * r[2];
1851        let y2 = r[1] + 0.5 * r[3];
1852        for hi in 0..gh {
1853            let c = hi as f32 / gh as f32;
1854            dy[(q * gh + hi) * 2] = log_compress(c - y1, l2_8);
1855            dy[(q * gh + hi) * 2 + 1] = log_compress(c - y2, l2_8);
1856        }
1857        for wi in 0..gh {
1858            let c = wi as f32 / gh as f32;
1859            dx[(q * gh + wi) * 2] = log_compress(c - x1, l2_8);
1860            dx[(q * gh + wi) * 2 + 1] = log_compress(c - x2, l2_8);
1861        }
1862    }
1863    let py = ry.forward(&dy, refs.len() * gh, pool, FH);
1864    let px = rx.forward(&dx, refs.len() * gh, pool, FH);
1865    let mut out = vec![0f32; FH * (NQ + 1) * n];
1866    for t in 0..FH {
1867        for q in 0..refs.len() {
1868            for hi in 0..gh {
1869                for wi in 0..gh {
1870                    out[(t * (NQ + 1) + (q + 1)) * n + hi * gh + wi] =
1871                        py[(q * gh + hi) * FH + t] + px[(q * gh + wi) * FH + t];
1872                }
1873            }
1874        }
1875    }
1876    Ok(out)
1877}
1878
1879/// sign(d) * log2(|8d| + 1) / log2(8)  (box_rpb_mode = "log").
1880fn log_compress(d: f32, l2_8: f32) -> f32 {
1881    d.signum() * ((8.0 * d).abs() + 1.0).log2() / l2_8
1882}
1883
1884/// elementwise a+b over equal-length buffers
1885fn add_rows(a: &[f32], b: &[f32]) -> Vec<f32> {
1886    a.iter().zip(b).map(|(x, y)| x + y).collect()
1887}
1888
1889/// Finiteness/range stats for one debug stage (env `SAM3_TEXT_DEBUG`).
1890#[allow(dead_code)]
1891fn dbg_stats(tag: &str, v: &[f32]) {
1892    if std::env::var("SAM3_TEXT_DEBUG").is_err() {
1893        return;
1894    }
1895    let (mut bad, mut mn, mut mx) = (0usize, f32::INFINITY, f32::NEG_INFINITY);
1896    for &x in v {
1897        if !x.is_finite() {
1898            bad += 1;
1899            continue;
1900        }
1901        if x < mn { mn = x; }
1902        if x > mx { mx = x; }
1903    }
1904    eprintln!(
1905        "[sam3] {tag} n={} nonfinite={} min={:.4e} max={:.4e}",
1906        v.len(), bad, mn, mx
1907    );
1908}
1909
1910/// Raw f32 dump for reference bisection (env `SAM3_DUMP_DIR`).
1911#[allow(dead_code)]
1912fn dump_f32(dir: &str, name: &str, v: &[f32]) {
1913    use std::io::Write;
1914    let p = std::path::Path::new(dir).join(format!("{name}.f32"));
1915    let mut b = Vec::with_capacity(v.len() * 4);
1916    for x in v {
1917        b.extend_from_slice(&x.to_le_bytes());
1918    }
1919    match std::fs::File::create(&p).and_then(|mut f| f.write_all(&b)) {
1920        Ok(()) => eprintln!("[sam3] dumped {} ({} elems)", p.display(), v.len()),
1921        Err(e) => eprintln!("[sam3] dump {} failed: {e}", p.display()),
1922    }
1923}
1924
1925// ───────────────────────── tests (gpu feature) ─────────────────────
1926
1927/// Isolated GPU-vs-CPU check of `run_attn` itself on deterministic
1928/// pseudo-random inputs — no model, no fixtures: if the owner gate
1929/// (rel/cos) fails at the Sam3Out level, this says whether the
1930/// attention GPU arm or another block is the diverger.
1931#[cfg(all(test, feature = "gpu"))]
1932mod attn_gpu_tests {
1933    use super::*;
1934
1935    fn lcg(state: &mut u64) -> f32 {
1936        *state = state
1937            .wrapping_mul(6364136223846793005)
1938            .wrapping_add(1442695040888963407);
1939        ((*state >> 40) as f32 / (1u64 << 24) as f32) - 0.5
1940    }
1941
1942    fn rel_cos(g: &[f32], w: &[f32]) -> (f32, f32) {
1943        let mut md = 0f32;
1944        let mut mx = 0f32;
1945        let (mut dot, mut ng, mut nw) = (0f64, 0f64, 0f64);
1946        for (&g, &w) in g.iter().zip(w) {
1947            let d = (g - w).abs();
1948            if d > md { md = d; }
1949            if g.abs() > mx { mx = g.abs(); }
1950            dot += g as f64 * w as f64;
1951            ng += g as f64 * g as f64;
1952            nw += w as f64 * w as f64;
1953        }
1954        let rel = if mx > 0.0 { md / mx } else { md };
1955        let cos = if ng > 0.0 && nw > 0.0 {
1956            (dot / (ng.sqrt() * nw.sqrt())) as f32
1957        } else {
1958            1.0
1959        };
1960        (rel, cos)
1961    }
1962
1963    #[test]
1964    fn attn_gpu_matches_cpu() {
1965        if std::env::var("CMF_GPU").as_deref() != Ok("1") {
1966            eprintln!("skip attn_gpu_matches_cpu: CMF_GPU != 1");
1967            return;
1968        }
1969        if !crate::gpu::backend_available() {
1970            eprintln!("skip attn_gpu_matches_cpu: no wgpu backend");
1971            return;
1972        }
1973        // Shapes that mirror the real trunk: 1024x1024 (small square),
1974        // 576x576x1024/16 (windowed layer) and 5184x5184x1024/16 (global
1975        // layer — the only shape that exercises QK query-chunking).
1976        let cases: [(usize, usize, usize, usize, bool); 3] = [
1977            (1024, 1024, 256, 4, true),
1978            (576, 576, 1024, 16, false),
1979            (5184, 5184, 1024, 16, false),
1980        ];
1981        for &(n_q, n_k, dim, heads, with_head) in &cases {
1982            let h3 = dim * 3;
1983            let mut st = 0x9e37_79b9_7f4a_7c15u64;
1984            let xs: Vec<f32> = (0..n_k * h3).map(|_| lcg(&mut st) * 2.0).collect();
1985            let mut st2 = 0xdead_beefu64.wrapping_mul(0x5bd1_e995);
1986            let w_qkv = QTensor::F32 {
1987                data: (0..h3 * dim).map(|_| lcg(&mut st2)).collect(),
1988                rows: h3,
1989                cols: dim,
1990            };
1991            let w_o = QTensor::F32 {
1992                data: (0..dim * dim).map(|_| lcg(&mut st2)).collect(),
1993                rows: dim,
1994                cols: dim,
1995            };
1996            let qb = vec![0.01f32; h3];
1997            let ob = vec![0.02f32; dim];
1998            let head_mask: Vec<f32>;
1999            let mut head_owned: Vec<f32> = Vec::new();
2000            if with_head {
2001                head_owned = vec![0f32; heads * n_q * n_k];
2002                for (i, v) in head_owned.iter_mut().enumerate() {
2003                    *v = (i % 97) as f32 * 0.001 - 0.048;
2004            }
2005            }
2006            head_mask = head_owned;
2007            let mask_kinds: [(Option<usize>, &str); 2] =
2008                [(None, "none"), (with_head.then_some(0usize), "head")];
2009            for (kind, label) in mask_kinds {
2010                if kind.is_none() && !matches!(label, "none") {
2011                    continue;
2012                }
2013                let mask = match kind {
2014                    Some(_) if with_head => Mask::Head(&head_mask),
2015                    Some(_) => continue,
2016                    None => Mask::None,
2017                };
2018                if label == "head" && !with_head {
2019                    continue;
2020                }
2021                let mut out_cpu = vec![0f32; n_q * dim];
2022                let mut out_gpu = vec![0f32; n_q * dim];
2023                crate::gpu::cpu_scope(|| {
2024                    run_attn(
2025                        &w_qkv, &qb, &w_o, &ob, &xs, &xs, &xs,
2026                        n_q, n_k, dim, heads, None, mask, None, &mut out_cpu,
2027                    )
2028                });
2029                run_attn(
2030                    &w_qkv, &qb, &w_o, &ob, &xs, &xs, &xs,
2031                    n_q, n_k, dim, heads, None, mask, None, &mut out_gpu,
2032                );
2033                let (rel, cos) = rel_cos(&out_gpu, &out_cpu);
2034                eprintln!(
2035                    "attn_gpu_vs_cpu[{n_q}x{n_k}x{dim}/{heads} {label}] rel={rel:.3e} cos={cos:.6}"
2036                );
2037                assert!(
2038                    rel <= 1e-2 && cos >= 0.999,
2039                    "attn {n_q}x{n_k} mask={label}: rel={rel:.3e} cos={cos:.6} outside gate"
2040                );
2041            }
2042        }
2043    }
2044}