Skip to main content

rlx_sam3/
vision_encoder.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Native SAM3 ViT trunk.
17//!
18//! Full numerical port of `sam3.model.vitdet.ViT` (the base 1008² model):
19//!
20//!   - Patch embed (Conv2d k=14, s=14, no bias) → [B, H, W, C] in NHWC.
21//!   - Tiled absolute positional embedding (24×24 → 72×72).
22//!   - 32 transformer blocks. Blocks 0..32 are window-attention with
23//!     `window_size=24` except for `global_att_blocks=[7,15,23,31]` which
24//!     run full-resolution attention.
25//!   - 2D RoPE applied to Q/K, interpolated when input differs from
26//!     `rope_pt_size=(24, 24)`.
27//!   - LayerNorm `ln_pre`, identity `ln_post`, `mlp_ratio=4.625`.
28//!
29//! Output: final block features in NHWC `[1, 72, 72, 1024]`, flattened
30//! row-major as `[grid*grid, embed_dim]`.
31
32use super::config::{SAM3_PATCH_GRID, Sam3VitConfig};
33use super::preprocess::{Sam3PreprocessWeights, assemble_patch_tokens, extract_preprocess_weights};
34use super::tensor::{gelu_tanh, layer_norm, linear, matmul, matmul_bt, softmax_rows};
35use anyhow::{Result, ensure};
36use rlx_core::weight_map::WeightMap;
37use rlx_flow::{GgufPackedLinear, GgufPackedParams};
38
39#[derive(Clone)]
40pub struct Sam3VitBlockWeights {
41    pub norm1_w: Vec<f32>,
42    pub norm1_b: Vec<f32>,
43    pub qkv_w_t: Vec<f32>,
44    pub qkv_b: Vec<f32>,
45    /// GGUF prefix for `attn.qkv` (no `.weight` suffix) when `qkv_w_t` is empty.
46    pub qkv_gguf_prefix: Option<String>,
47    pub proj_w_t: Vec<f32>,
48    pub proj_b: Vec<f32>,
49    pub proj_gguf_prefix: Option<String>,
50    pub norm2_w: Vec<f32>,
51    pub norm2_b: Vec<f32>,
52    pub mlp_fc1_w_t: Vec<f32>,
53    pub mlp_fc1_b: Vec<f32>,
54    pub mlp_fc1_gguf_prefix: Option<String>,
55    pub mlp_fc2_w_t: Vec<f32>,
56    pub mlp_fc2_b: Vec<f32>,
57    pub mlp_fc2_gguf_prefix: Option<String>,
58}
59
60#[derive(Clone)]
61pub struct Sam3VisionEncoderWeights {
62    pub pre: Sam3PreprocessWeights,
63    pub ln_pre_w: Vec<f32>,
64    pub ln_pre_b: Vec<f32>,
65    pub blocks: Vec<Sam3VitBlockWeights>,
66}
67
68pub struct Sam3VisionOutput {
69    pub tokens: Vec<f32>,
70    pub grid: usize,
71    pub dim: usize,
72}
73
74pub fn extract_vision_encoder_weights(
75    weights: &mut WeightMap,
76    cfg: &Sam3VitConfig,
77    gguf_packed: Option<&GgufPackedParams>,
78) -> Result<Sam3VisionEncoderWeights> {
79    let pre = extract_preprocess_weights(weights, cfg)?;
80    let e = cfg.embed_dim;
81    let (ln_pre_w, ln_pre_b) = take_layer_norm(weights, &prefixes("ln_pre"), e)?;
82    let hidden = (e as f64 * cfg.mlp_ratio) as usize;
83    let mut blocks = Vec::with_capacity(cfg.depth);
84    for i in 0..cfg.depth {
85        let p = format!("blocks.{i}");
86        let pref = prefixes(&p);
87        let (norm1_w, norm1_b) = take_layer_norm(weights, &prefixed(&pref, "norm1"), e)?;
88        let (qkv_w_t, qkv_gguf_prefix) =
89            take_linear_w_or_gguf(weights, gguf_packed, &prefixed(&pref, "attn.qkv"), e, 3 * e)?;
90        let qkv_b = take_linear_b(weights, &prefixed(&pref, "attn.qkv"), 3 * e)?;
91        let (proj_w_t, proj_gguf_prefix) =
92            take_linear_w_or_gguf(weights, gguf_packed, &prefixed(&pref, "attn.proj"), e, e)?;
93        let proj_b = take_linear_b(weights, &prefixed(&pref, "attn.proj"), e)?;
94        let (norm2_w, norm2_b) = take_layer_norm(weights, &prefixed(&pref, "norm2"), e)?;
95        let (mlp_fc1_w_t, mlp_fc1_gguf_prefix) = take_linear_w_any_or_gguf(
96            weights,
97            gguf_packed,
98            &pref,
99            &["mlp.fc1", "mlp.lin1"],
100            e,
101            hidden,
102        )?;
103        let mlp_fc1_b = take_linear_b_any(weights, &pref, &["mlp.fc1", "mlp.lin1"], hidden)?;
104        let (mlp_fc2_w_t, mlp_fc2_gguf_prefix) = take_linear_w_any_or_gguf(
105            weights,
106            gguf_packed,
107            &pref,
108            &["mlp.fc2", "mlp.lin2"],
109            hidden,
110            e,
111        )?;
112        let mlp_fc2_b = take_linear_b_any(weights, &pref, &["mlp.fc2", "mlp.lin2"], e)?;
113        blocks.push(Sam3VitBlockWeights {
114            norm1_w,
115            norm1_b,
116            qkv_w_t,
117            qkv_b,
118            qkv_gguf_prefix,
119            proj_w_t,
120            proj_b,
121            proj_gguf_prefix,
122            norm2_w,
123            norm2_b,
124            mlp_fc1_w_t,
125            mlp_fc1_b,
126            mlp_fc1_gguf_prefix,
127            mlp_fc2_w_t,
128            mlp_fc2_b,
129            mlp_fc2_gguf_prefix,
130        });
131    }
132    Ok(Sam3VisionEncoderWeights {
133        pre,
134        ln_pre_w,
135        ln_pre_b,
136        blocks,
137    })
138}
139
140/// Run the 32 ViT-L transformer blocks on already-tokenized input
141/// `[grid*grid, embed_dim]`. Intended for parity tests against the IR path,
142/// which also operates on post-`ln_pre` tokens.
143pub fn forward_blocks_native(
144    weights: &Sam3VisionEncoderWeights,
145    gguf_packed: Option<&GgufPackedParams>,
146    cfg: &Sam3VitConfig,
147    tokens_in: &[f32],
148) -> Result<Vec<f32>> {
149    let e = cfg.embed_dim;
150    let grid = cfg.patch_grid();
151    let head_dim = e / cfg.num_heads;
152    ensure!(
153        head_dim * cfg.num_heads == e,
154        "embed_dim {e} not divisible by num_heads {}",
155        cfg.num_heads
156    );
157    ensure!(
158        tokens_in.len() == grid * grid * e,
159        "tokens_in expected {} got {}",
160        grid * grid * e,
161        tokens_in.len()
162    );
163    let rope_pt = if cfg.window_size > 0 {
164        cfg.window_size
165    } else {
166        grid
167    };
168    let global_set: std::collections::HashSet<usize> =
169        cfg.global_att_blocks.iter().copied().collect();
170    let rope_global = build_rope_freqs(head_dim, grid, grid, 10000.0, rope_pt as f32 / grid as f32);
171    let rope_window = build_rope_freqs(head_dim, cfg.window_size, cfg.window_size, 10000.0, 1.0);
172    let mut x = tokens_in.to_vec();
173    for (i, block) in weights.blocks.iter().enumerate() {
174        let is_global = global_set.contains(&i);
175        block_forward(
176            &mut x,
177            block,
178            gguf_packed,
179            cfg,
180            grid,
181            if is_global { 0 } else { cfg.window_size },
182            if is_global {
183                &rope_global
184            } else {
185                &rope_window
186            },
187            head_dim,
188            cfg.num_heads,
189        )?;
190    }
191    Ok(x)
192}
193
194pub fn encode_image_native(
195    weights: &Sam3VisionEncoderWeights,
196    gguf_packed: Option<&GgufPackedParams>,
197    cfg: &Sam3VitConfig,
198    image_nchw: &[f32],
199) -> Result<Sam3VisionOutput> {
200    let e = cfg.embed_dim;
201    let grid = cfg.patch_grid();
202    ensure!(
203        grid == SAM3_PATCH_GRID,
204        "SAM3 base grid must be {SAM3_PATCH_GRID}"
205    );
206    let head_dim = e / cfg.num_heads;
207    ensure!(
208        head_dim * cfg.num_heads == e,
209        "embed_dim {e} not divisible by num_heads {}",
210        cfg.num_heads
211    );
212    let rope_pt = if cfg.window_size > 0 {
213        cfg.window_size
214    } else {
215        grid
216    };
217
218    // Patch embed (+ tiled abs pos), flat [grid*grid, embed_dim] NHWC.
219    let mut x = assemble_patch_tokens(&weights.pre, image_nchw)?;
220    x = layer_norm(
221        &x,
222        &weights.ln_pre_w,
223        &weights.ln_pre_b,
224        e,
225        cfg.layer_norm_eps as f32,
226    )?;
227
228    let global_set: std::collections::HashSet<usize> =
229        cfg.global_att_blocks.iter().copied().collect();
230    let rope_global = build_rope_freqs(head_dim, grid, grid, 10000.0, rope_pt as f32 / grid as f32);
231    let rope_window = build_rope_freqs(head_dim, cfg.window_size, cfg.window_size, 10000.0, 1.0);
232
233    for (i, block) in weights.blocks.iter().enumerate() {
234        let is_global = global_set.contains(&i);
235        block_forward(
236            &mut x,
237            block,
238            gguf_packed,
239            cfg,
240            grid,
241            if is_global { 0 } else { cfg.window_size },
242            if is_global {
243                &rope_global
244            } else {
245                &rope_window
246            },
247            head_dim,
248            cfg.num_heads,
249        )?;
250    }
251    // ln_post is Identity for SAM3 base, no-op.
252
253    Ok(Sam3VisionOutput {
254        tokens: x,
255        grid,
256        dim: e,
257    })
258}
259
260/// Compute the 2D RoPE frequency table. The layout is `[L, head_dim]` flat,
261/// with each `head_dim`-long stride storing `head_dim/2` interleaved
262/// `(cos, sin)` pairs — first half from the x axis, second from the y axis.
263fn build_rope_freqs(
264    head_dim: usize,
265    end_x: usize,
266    end_y: usize,
267    theta: f32,
268    scale_pos: f32,
269) -> Vec<f32> {
270    let half = head_dim / 2;
271    assert!(
272        head_dim.is_multiple_of(4),
273        "RoPE head_dim must be divisible by 4"
274    );
275    let pair_per_axis = head_dim / 4;
276    let mut freqs_per_pair = Vec::with_capacity(pair_per_axis);
277    for k in 0..pair_per_axis {
278        let exp = (4 * k) as f32 / head_dim as f32;
279        freqs_per_pair.push(1.0 / theta.powf(exp));
280    }
281    let l = end_x * end_y;
282    let mut out = vec![0f32; l * head_dim];
283    for pos in 0..l {
284        let t_x = (pos % end_x) as f32 * scale_pos;
285        let t_y = (pos / end_x) as f32 * scale_pos;
286        for k in 0..pair_per_axis {
287            let ang_x = t_x * freqs_per_pair[k];
288            let ang_y = t_y * freqs_per_pair[k];
289            out[pos * head_dim + 2 * k] = ang_x.cos();
290            out[pos * head_dim + 2 * k + 1] = ang_x.sin();
291            out[pos * head_dim + 2 * (k + pair_per_axis)] = ang_y.cos();
292            out[pos * head_dim + 2 * (k + pair_per_axis) + 1] = ang_y.sin();
293        }
294    }
295    let _ = half;
296    out
297}
298
299/// Apply RoPE in-place to `qk` of shape `[batch_eff * num_heads * L, head_dim]`.
300/// `freqs_cis` is `[L, head_dim]` (real, imag pairs) and broadcasts over the
301/// outer batch×head axis.
302fn rope_apply_inplace(
303    qk: &mut [f32],
304    freqs_cis: &[f32],
305    rows: usize,
306    seq_len: usize,
307    head_dim: usize,
308) {
309    let pairs = head_dim / 2;
310    for r in 0..rows {
311        let l = r % seq_len;
312        let f = &freqs_cis[l * head_dim..(l + 1) * head_dim];
313        let v = &mut qk[r * head_dim..(r + 1) * head_dim];
314        for k in 0..pairs {
315            let vr = v[2 * k];
316            let vi = v[2 * k + 1];
317            let fr = f[2 * k];
318            let fi = f[2 * k + 1];
319            v[2 * k] = vr * fr - vi * fi;
320            v[2 * k + 1] = vr * fi + vi * fr;
321        }
322    }
323}
324
325/// One transformer block: norm → (windowed) attention → residual →
326/// norm → MLP → residual. `x` is `[grid*grid, embed_dim]` NHWC flat.
327fn linear_maybe_gguf(
328    x: &[f32],
329    m: usize,
330    k: usize,
331    w_t: &[f32],
332    gguf: Option<&GgufPackedLinear>,
333    n: usize,
334    b: &[f32],
335) -> Result<Vec<f32>> {
336    let mut out = vec![0f32; m * n];
337    if let Some(p) = gguf {
338        ensure!(
339            p.in_dim == k && p.out_dim == n,
340            "packed linear shape {k}x{n} vs gguf {}x{}",
341            p.in_dim,
342            p.out_dim
343        );
344        rlx_cpu::gguf_matmul::gguf_matmul_bt(x, &p.w_q, &mut out, m, k, n, p.scheme);
345    } else {
346        ensure!(
347            !w_t.is_empty(),
348            "linear: missing F32 weights and no GGUF packed entry"
349        );
350        return linear(x, m, k, w_t, n, b);
351    }
352    for row in 0..m {
353        for col in 0..n {
354            out[row * n + col] += b[col];
355        }
356    }
357    Ok(out)
358}
359
360fn packed_for_prefix<'a>(
361    packed: Option<&'a GgufPackedParams>,
362    prefix: Option<&String>,
363) -> Option<&'a GgufPackedLinear> {
364    let key = format!("{}.weight", prefix.as_ref()?);
365    packed?.get_linear(&key)
366}
367
368fn block_forward(
369    x: &mut [f32],
370    block: &Sam3VitBlockWeights,
371    gguf_packed: Option<&GgufPackedParams>,
372    cfg: &Sam3VitConfig,
373    grid: usize,
374    window_size: usize,
375    freqs_cis: &[f32],
376    head_dim: usize,
377    num_heads: usize,
378) -> Result<()> {
379    let e = cfg.embed_dim;
380    let n = grid * grid;
381    let eps = cfg.layer_norm_eps as f32;
382
383    // shortcut: x as-is. Compute attention(norm1(x)) in attn_out.
384    let n1 = layer_norm(x, &block.norm1_w, &block.norm1_b, e, eps)?;
385    let qkv_gguf = packed_for_prefix(gguf_packed, block.qkv_gguf_prefix.as_ref());
386    let proj_gguf = packed_for_prefix(gguf_packed, block.proj_gguf_prefix.as_ref());
387    let attn_out = if window_size == 0 {
388        attention_native(
389            &n1,
390            1,
391            n,
392            &block.qkv_w_t,
393            qkv_gguf,
394            &block.qkv_b,
395            &block.proj_w_t,
396            proj_gguf,
397            &block.proj_b,
398            freqs_cis,
399            num_heads,
400            head_dim,
401        )?
402    } else {
403        attention_windowed(
404            &n1,
405            grid,
406            grid,
407            window_size,
408            e,
409            &block.qkv_w_t,
410            qkv_gguf,
411            &block.qkv_b,
412            &block.proj_w_t,
413            proj_gguf,
414            &block.proj_b,
415            freqs_cis,
416            num_heads,
417            head_dim,
418        )?
419    };
420    for i in 0..x.len() {
421        x[i] += attn_out[i];
422    }
423
424    let n2 = layer_norm(x, &block.norm2_w, &block.norm2_b, e, eps)?;
425    let hidden = block.mlp_fc1_b.len();
426    let fc1_gguf = packed_for_prefix(gguf_packed, block.mlp_fc1_gguf_prefix.as_ref());
427    let fc2_gguf = packed_for_prefix(gguf_packed, block.mlp_fc2_gguf_prefix.as_ref());
428    let mut mlp = linear_maybe_gguf(
429        &n2,
430        n,
431        e,
432        &block.mlp_fc1_w_t,
433        fc1_gguf,
434        hidden,
435        &block.mlp_fc1_b,
436    )?;
437    gelu_tanh(&mut mlp);
438    let ffn = linear_maybe_gguf(
439        &mlp,
440        n,
441        hidden,
442        &block.mlp_fc2_w_t,
443        fc2_gguf,
444        e,
445        &block.mlp_fc2_b,
446    )?;
447    for i in 0..x.len() {
448        x[i] += ffn[i];
449    }
450    Ok(())
451}
452
453fn attention_windowed(
454    x: &[f32],
455    h: usize,
456    w: usize,
457    ws: usize,
458    e: usize,
459    qkv_w_t: &[f32],
460    qkv_gguf: Option<&GgufPackedLinear>,
461    qkv_b: &[f32],
462    proj_w_t: &[f32],
463    proj_gguf: Option<&GgufPackedLinear>,
464    proj_b: &[f32],
465    freqs_cis: &[f32],
466    num_heads: usize,
467    head_dim: usize,
468) -> Result<Vec<f32>> {
469    let pad_h = (ws - h % ws) % ws;
470    let pad_w = (ws - w % ws) % ws;
471    let hp = h + pad_h;
472    let wp = w + pad_w;
473    let nh = hp / ws;
474    let nw = wp / ws;
475    let num_windows = nh * nw;
476    let win_len = ws * ws;
477
478    // Partition: produce [num_windows, ws, ws, e].
479    let mut win = vec![0f32; num_windows * win_len * e];
480    for y in 0..hp {
481        for xc in 0..wp {
482            let wy = y / ws;
483            let wx = xc / ws;
484            let ry = y % ws;
485            let rx = xc % ws;
486            let widx = wy * nw + wx;
487            let dst = ((widx * ws + ry) * ws + rx) * e;
488            if y < h && xc < w {
489                let src = (y * w + xc) * e;
490                win[dst..dst + e].copy_from_slice(&x[src..src + e]);
491            }
492            // else: padding stays zero (matches F.pad with 0).
493        }
494    }
495
496    let attn = attention_native(
497        &win,
498        num_windows,
499        win_len,
500        qkv_w_t,
501        qkv_gguf,
502        qkv_b,
503        proj_w_t,
504        proj_gguf,
505        proj_b,
506        freqs_cis,
507        num_heads,
508        head_dim,
509    )?;
510
511    // Unpartition: stitch [num_windows, ws, ws, e] back into [h, w, e],
512    // dropping padding.
513    let mut out = vec![0f32; h * w * e];
514    for y in 0..h {
515        for xc in 0..w {
516            let wy = y / ws;
517            let wx = xc / ws;
518            let ry = y % ws;
519            let rx = xc % ws;
520            let widx = wy * nw + wx;
521            let src = ((widx * ws + ry) * ws + rx) * e;
522            let dst = (y * w + xc) * e;
523            out[dst..dst + e].copy_from_slice(&attn[src..src + e]);
524        }
525    }
526    Ok(out)
527}
528
529/// Multi-head self-attention with 2D RoPE for `b` independent sequences of
530/// length `l`. `x` is `[b, l, e]`; output is `[b, l, e]`.
531fn attention_native(
532    x: &[f32],
533    b: usize,
534    l: usize,
535    qkv_w_t: &[f32],
536    qkv_gguf: Option<&GgufPackedLinear>,
537    qkv_b: &[f32],
538    proj_w_t: &[f32],
539    proj_gguf: Option<&GgufPackedLinear>,
540    proj_b: &[f32],
541    freqs_cis: &[f32],
542    num_heads: usize,
543    head_dim: usize,
544) -> Result<Vec<f32>> {
545    let e = num_heads * head_dim;
546    let rows = b * l;
547    let qkv = linear_maybe_gguf(x, rows, e, qkv_w_t, qkv_gguf, 3 * e, qkv_b)?;
548
549    // Split into [b, num_heads, l, head_dim] for q, k, v. We keep them as
550    // [b*num_heads, l, head_dim] = [bh, l, head_dim] to feed sgemm.
551    let bh = b * num_heads;
552    let mut q = vec![0f32; bh * l * head_dim];
553    let mut k = vec![0f32; bh * l * head_dim];
554    let mut v = vec![0f32; bh * l * head_dim];
555    for bi in 0..b {
556        for li in 0..l {
557            let src = (bi * l + li) * 3 * e;
558            for hd in 0..num_heads {
559                let qd_src = src + hd * head_dim;
560                let kd_src = src + e + hd * head_dim;
561                let vd_src = src + 2 * e + hd * head_dim;
562                let dst = ((bi * num_heads + hd) * l + li) * head_dim;
563                q[dst..dst + head_dim].copy_from_slice(&qkv[qd_src..qd_src + head_dim]);
564                k[dst..dst + head_dim].copy_from_slice(&qkv[kd_src..kd_src + head_dim]);
565                v[dst..dst + head_dim].copy_from_slice(&qkv[vd_src..vd_src + head_dim]);
566            }
567        }
568    }
569
570    rope_apply_inplace(&mut q, freqs_cis, bh * l, l, head_dim);
571    rope_apply_inplace(&mut k, freqs_cis, bh * l, l, head_dim);
572
573    let scale = 1.0f32 / (head_dim as f32).sqrt();
574    let mut attn_out = vec![0f32; bh * l * head_dim];
575    let mut scores = vec![0f32; l * l];
576
577    for bhi in 0..bh {
578        let q_h = &q[bhi * l * head_dim..(bhi + 1) * l * head_dim];
579        let k_h = &k[bhi * l * head_dim..(bhi + 1) * l * head_dim];
580        let v_h = &v[bhi * l * head_dim..(bhi + 1) * l * head_dim];
581        // scores[l, l] = scale * Q[l, hd] @ K[l, hd]^T
582        matmul_bt(q_h, k_h, &mut scores, l, head_dim, l, scale);
583        softmax_rows(&mut scores, l, l);
584        // out[l, hd] = scores[l, l] @ V[l, hd]
585        let out_h = &mut attn_out[bhi * l * head_dim..(bhi + 1) * l * head_dim];
586        matmul(&scores, v_h, out_h, l, l, head_dim);
587    }
588
589    // Repack [b, num_heads, l, head_dim] → [b, l, num_heads*head_dim] for proj.
590    let mut packed = vec![0f32; rows * e];
591    for bi in 0..b {
592        for li in 0..l {
593            for hd in 0..num_heads {
594                let src = ((bi * num_heads + hd) * l + li) * head_dim;
595                let dst = (bi * l + li) * e + hd * head_dim;
596                packed[dst..dst + head_dim].copy_from_slice(&attn_out[src..src + head_dim]);
597            }
598        }
599    }
600    linear_maybe_gguf(&packed, rows, e, proj_w_t, proj_gguf, e, proj_b)
601}
602
603fn prefixes(suffix: &str) -> Vec<String> {
604    [
605        "detector.backbone.vision_backbone.trunk",
606        "detector.backbone.visual.trunk",
607        "backbone.vision_backbone.trunk",
608        "backbone.visual.trunk",
609        "visual.trunk",
610        "trunk",
611    ]
612    .iter()
613    .map(|p| format!("{p}.{suffix}"))
614    .collect()
615}
616
617fn prefixed(prefixes: &[String], suffix: &str) -> Vec<String> {
618    prefixes.iter().map(|p| format!("{p}.{suffix}")).collect()
619}
620
621fn take_layer_norm(
622    weights: &mut WeightMap,
623    bases: &[String],
624    dim: usize,
625) -> Result<(Vec<f32>, Vec<f32>)> {
626    let w = take_shape(weights, &suffixes(bases, "weight"), &[dim])?;
627    let b = take_shape(weights, &suffixes(bases, "bias"), &[dim])?;
628    Ok((w, b))
629}
630
631fn take_linear_w_or_gguf(
632    weights: &mut WeightMap,
633    gguf_packed: Option<&GgufPackedParams>,
634    bases: &[String],
635    in_dim: usize,
636    out_dim: usize,
637) -> Result<(Vec<f32>, Option<String>)> {
638    let keys = suffixes(bases, "weight");
639    for key in &keys {
640        if weights.has(key) {
641            let w = take_linear_w(weights, bases, in_dim, out_dim)?;
642            return Ok((w, None));
643        }
644        if let Some(packed) = gguf_packed {
645            if let Some(prefix) = key.strip_suffix(".weight") {
646                if packed.get_linear(key).is_some() {
647                    return Ok((Vec::new(), Some(prefix.to_string())));
648                }
649            }
650        }
651    }
652    anyhow::bail!("none of the SAM3 linear weight keys were found: {keys:?}")
653}
654
655fn take_linear_w_any_or_gguf(
656    weights: &mut WeightMap,
657    gguf_packed: Option<&GgufPackedParams>,
658    block_prefixes: &[String],
659    names: &[&str],
660    in_dim: usize,
661    out_dim: usize,
662) -> Result<(Vec<f32>, Option<String>)> {
663    let bases: Vec<String> = block_prefixes
664        .iter()
665        .flat_map(|p| names.iter().map(move |name| format!("{p}.{name}")))
666        .collect();
667    take_linear_w_or_gguf(weights, gguf_packed, &bases, in_dim, out_dim)
668}
669
670fn take_linear_w(
671    weights: &mut WeightMap,
672    bases: &[String],
673    in_dim: usize,
674    out_dim: usize,
675) -> Result<Vec<f32>> {
676    let keys = suffixes(bases, "weight");
677    for key in &keys {
678        if weights.has(key) {
679            let (data, shape) = weights.take_transposed(key)?;
680            ensure!(
681                shape == vec![in_dim, out_dim],
682                "{key} expected [{in_dim}, {out_dim}], got {shape:?}"
683            );
684            return Ok(data);
685        }
686    }
687    anyhow::bail!("none of the SAM3 linear weight keys were found: {keys:?}")
688}
689
690#[allow(dead_code)]
691fn take_linear_w_any(
692    weights: &mut WeightMap,
693    block_prefixes: &[String],
694    names: &[&str],
695    in_dim: usize,
696    out_dim: usize,
697) -> Result<Vec<f32>> {
698    let bases: Vec<String> = block_prefixes
699        .iter()
700        .flat_map(|p| names.iter().map(move |name| format!("{p}.{name}")))
701        .collect();
702    take_linear_w(weights, &bases, in_dim, out_dim)
703}
704
705fn take_linear_b(weights: &mut WeightMap, bases: &[String], dim: usize) -> Result<Vec<f32>> {
706    take_shape(weights, &suffixes(bases, "bias"), &[dim])
707}
708
709fn take_linear_b_any(
710    weights: &mut WeightMap,
711    block_prefixes: &[String],
712    names: &[&str],
713    dim: usize,
714) -> Result<Vec<f32>> {
715    let bases: Vec<String> = block_prefixes
716        .iter()
717        .flat_map(|p| names.iter().map(move |name| format!("{p}.{name}")))
718        .collect();
719    take_linear_b(weights, &bases, dim)
720}
721
722fn suffixes(bases: &[String], suffix: &str) -> Vec<String> {
723    bases.iter().map(|b| format!("{b}.{suffix}")).collect()
724}
725
726fn take_shape(weights: &mut WeightMap, keys: &[String], expected: &[usize]) -> Result<Vec<f32>> {
727    for key in keys {
728        if weights.has(key) {
729            let (data, shape) = weights.take(key)?;
730            ensure!(
731                shape == expected,
732                "{key} expected {expected:?}, got {shape:?}"
733            );
734            return Ok(data);
735        }
736    }
737    anyhow::bail!("none of the SAM3 weight keys were found: {keys:?}")
738}