Skip to main content

rlx_sam3/
sam3.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//! SAM3 top-level API.
17//!
18//! This module owns the shipped checkpoint-facing surface for base SAM3.
19//! The native surface exposes SAM3 preprocessing and ViT patch embeddings.
20//! Full image/video inference runs through native Rust modules. The Python
21//! helper in this directory is kept only as a parity reference.
22
23use super::config::Sam3Config;
24use super::detector::{Sam3DetectorWeights, detector_forward_native};
25use super::detector_decoder::{
26    Sam3DecoderOutput, Sam3DecoderWeights, extract_decoder_weights, forward_decoder,
27};
28use super::detector_encoder::{Sam3EncoderWeights, extract_encoder_weights, forward_encoder};
29use super::detector_encoder_ir::{forward_encoder_ir, forward_encoder_ir_on_with_profile};
30use super::geometry::{Sam3GeometryWeights, encode_geometry_native};
31use super::neck::{
32    Sam3NeckWeights, apply_neck_native, compile_neck_branches, extract_neck_weights,
33};
34use super::preprocess::{assemble_patch_tokens, preprocess_image};
35use super::segmentation_head::{
36    Sam3DotProductScoringWeights, Sam3SegmentationHeadWeights, Sam3SegmentationOutput,
37    compile_segmentation_ir, extract_dot_product_scoring_weights,
38    extract_segmentation_head_weights, forward_dot_prod_scoring, forward_segmentation,
39    segmentation_forward_native,
40};
41use super::text_encoder::{
42    Sam3TextEncoded, Sam3TextEncoderWeights, encode_text_native, encode_tokens,
43    extract_text_encoder_weights,
44};
45use super::tracker::{Sam3TrackerWeights, extract_tracker_weights, tracker_forward_native};
46use super::vision_encoder::{
47    Sam3VisionEncoderWeights, Sam3VisionOutput, encode_image_native, extract_vision_encoder_weights,
48};
49use super::vision_encoder_ir::encode_image_ir_on_with_profile;
50use anyhow::{Context, Result, ensure};
51use rlx_flow::CompileProfile;
52use rlx_runtime::Device;
53use rlx_sam::profile::sam3_profile_near_weights;
54use std::path::Path;
55
56#[derive(Debug, Clone)]
57pub struct Sam3EncodedImage {
58    /// `[grid * grid, embed_dim]` flattened row-major patch tokens.
59    pub patch_tokens: Vec<f32>,
60    pub grid: usize,
61    pub embed_dim: usize,
62    pub resized_hw: (usize, usize),
63}
64
65#[derive(Debug, Clone)]
66pub struct Sam3ImagePrediction {
67    /// Mask logits/probabilities flattened in row-major order. The shape
68    /// is available in `mask_shape`.
69    pub masks: Vec<f32>,
70    pub mask_shape: Vec<usize>,
71    pub boxes: Vec<f32>,
72    pub boxes_shape: Vec<usize>,
73    pub scores: Vec<f32>,
74    pub scores_shape: Vec<usize>,
75    pub num_instances: usize,
76    pub h_out: usize,
77    pub w_out: usize,
78}
79
80#[derive(Debug, Clone, Default)]
81pub struct Sam3VideoState {
82    pub frame_index: usize,
83    pub memory_tokens: Vec<Vec<f32>>,
84    pub last_prediction: Option<Sam3ImagePrediction>,
85}
86
87#[derive(Debug, Clone)]
88pub struct Sam3VideoFramePrediction {
89    pub frame_index: usize,
90    pub image: Sam3ImagePrediction,
91    pub memory_len: usize,
92}
93
94pub struct Sam3 {
95    cfg: Sam3Config,
96    vision: Option<Sam3VisionEncoderWeights>,
97    neck: Sam3NeckWeights,
98    text: Sam3TextEncoderWeights,
99    geometry: Sam3GeometryWeights,
100    detector: Sam3DetectorWeights,
101    encoder: Sam3EncoderWeights,
102    decoder: Sam3DecoderWeights,
103    seg_head: Sam3SegmentationHeadWeights,
104    scoring: Sam3DotProductScoringWeights,
105    seg: Sam3SegmentationHeadWeights,
106    tracker: Sam3TrackerWeights,
107    device: Device,
108    compile_profile: CompileProfile,
109    gguf_packed: Option<rlx_flow::GgufPackedParams>,
110}
111
112impl Sam3 {
113    /// Load a SAM3 checkpoint for native inference.
114    ///
115    /// Load from `.safetensors` or `.gguf` (`general.architecture` = `sam3`).
116    ///
117    /// For PyTorch checkpoints, convert with `tests/sam3_parity_helpers/pt_to_safetensors.py`
118    /// or use community GGUF (e.g. rob-laz/sam3-gguf).
119    pub fn from_checkpoint(weights_path: &str, cfg: Sam3Config) -> Result<Self> {
120        Self::from_checkpoint_on(weights_path, cfg, Device::Cpu)
121    }
122
123    pub fn from_checkpoint_on(weights_path: &str, cfg: Sam3Config, device: Device) -> Result<Self> {
124        Self::from_safetensors_on(weights_path, cfg, device)
125    }
126
127    pub fn from_safetensors(weights_path: &str, cfg: Sam3Config) -> Result<Self> {
128        Self::from_safetensors_on(weights_path, cfg, Device::Cpu)
129    }
130
131    pub fn from_safetensors_on(
132        weights_path: &str,
133        cfg: Sam3Config,
134        device: Device,
135    ) -> Result<Self> {
136        rlx_core::validate_sam_device("sam3", device)?;
137
138        let path = Path::new(weights_path);
139        let is_gguf = path.extension().is_some_and(|e| e == "gguf");
140        if is_gguf {
141            rlx_core::gguf_validate_arch(path, rlx_core::SAM3_GGUF_ARCHES)?;
142        }
143        let (mut wm, gguf_packed) = if is_gguf && crate::packed_gguf::gguf_has_packed_linears(path)?
144        {
145            eprintln!("[sam3] loading GGUF with packed ViT matmul {path:?}");
146            let (wm, packed) = crate::packed_gguf::load_sam3_from_gguf(path)?;
147            (wm, Some(packed))
148        } else {
149            (
150                rlx_core::load_weight_map(path, rlx_core::SAM3_GGUF_ARCHES)?,
151                None,
152            )
153        };
154        let compile_profile = sam3_profile_near_weights(path);
155        let vision = extract_vision_encoder_weights(&mut wm, &cfg.vit, gguf_packed.as_ref())?;
156        let mut neck = extract_neck_weights(&mut wm)?;
157        compile_neck_branches(
158            &mut neck,
159            cfg.vit.embed_dim,
160            cfg.vit.patch_grid(),
161            device,
162            &compile_profile,
163        )?;
164        let text = extract_text_encoder_weights(&mut wm, &cfg.text, gguf_packed.as_ref())?;
165        let encoder = extract_encoder_weights(&mut wm, gguf_packed.as_ref())?;
166        let decoder = extract_decoder_weights(&mut wm, gguf_packed.as_ref())?;
167        let mut seg_head = extract_segmentation_head_weights(&mut wm, gguf_packed.as_ref())?;
168        compile_segmentation_ir(
169            &mut seg_head,
170            gguf_packed.as_ref(),
171            cfg.vit.patch_grid(),
172            device,
173            &compile_profile,
174        )?;
175        let scoring = extract_dot_product_scoring_weights(&mut wm, gguf_packed.as_ref())?;
176        let tracker = extract_tracker_weights(&mut wm)?;
177        Ok(Self {
178            cfg,
179            vision: Some(vision),
180            neck,
181            text,
182            geometry: Sam3GeometryWeights::default(),
183            detector: Sam3DetectorWeights::default(),
184            encoder,
185            seg: Sam3SegmentationHeadWeights::default(),
186            tracker,
187            decoder,
188            seg_head,
189            scoring,
190            device,
191            compile_profile,
192            gguf_packed,
193        })
194    }
195
196    /// Tier-1 compile profile (`sam.rlx.toml` next to weights when present).
197    pub fn compile_profile(&self) -> &CompileProfile {
198        &self.compile_profile
199    }
200
201    pub fn config(&self) -> &Sam3Config {
202        &self.cfg
203    }
204
205    /// Returns the loaded tracker weights (used by the video basic test
206    /// to confirm checkpoint coverage).
207    pub fn tracker_weights(&self) -> &Sam3TrackerWeights {
208        &self.tracker
209    }
210
211    pub fn encoder_weights(&self) -> &Sam3EncoderWeights {
212        &self.encoder
213    }
214
215    pub fn decoder_weights(&self) -> &Sam3DecoderWeights {
216        &self.decoder
217    }
218
219    pub fn device(&self) -> Device {
220        self.device
221    }
222
223    /// Run the ViT-L trunk on a single preprocessed image. Routes through the
224    /// HIR graph when `RLX_SAM3_VISION_DEVICE=metal|mlx|cuda|cpu` is set; falls
225    /// back to the CPU-native path otherwise (the default — byte-identical to
226    /// pre-IR behavior). `RLX_SAM3_VISION_HOST=1` forces the host path even if
227    /// the device env var is also present.
228    fn encode_image_dispatch(&self, image_nchw: &[f32]) -> Result<Sam3VisionOutput> {
229        let vision = self
230            .vision
231            .as_ref()
232            .context("SAM3 encode_image requires native vision weights")?;
233        if rlx_ir::env::flag("RLX_SAM3_VISION_HOST") {
234            return encode_image_native(
235                vision,
236                self.gguf_packed.as_ref(),
237                &self.cfg.vit,
238                image_nchw,
239            );
240        }
241        let dev = match rlx_ir::env::var("RLX_SAM3_VISION_DEVICE").as_deref() {
242            Some("metal") => Device::Metal,
243            Some("mlx") => Device::Mlx,
244            Some("cuda") => Device::Cuda,
245            Some("cpu") => Device::Cpu,
246            _ => {
247                return encode_image_native(
248                    vision,
249                    self.gguf_packed.as_ref(),
250                    &self.cfg.vit,
251                    image_nchw,
252                );
253            }
254        };
255        encode_image_ir_on_with_profile(
256            vision,
257            self.gguf_packed.as_ref(),
258            &self.cfg.vit,
259            image_nchw,
260            dev,
261            &self.compile_profile,
262        )
263    }
264
265    pub fn encode_image(
266        &self,
267        image_u8: &[u8],
268        h_in: usize,
269        w_in: usize,
270    ) -> Result<Sam3EncodedImage> {
271        let _vision_check = self
272            .vision
273            .as_ref()
274            .context("SAM3 encode_image requires native vision weights")?;
275        let (image_nchw, resized_hw) = preprocess_image(image_u8, h_in, w_in);
276        let encoded = self.encode_image_dispatch(&image_nchw)?;
277        Ok(Sam3EncodedImage {
278            patch_tokens: encoded.tokens,
279            grid: encoded.grid,
280            embed_dim: encoded.dim,
281            resized_hw,
282        })
283    }
284
285    /// Run the vision trunk + 4-scale neck and return per-level
286    /// `[channels, h, w]` feature maps with matching sinusoidal positional
287    /// encodings. Used by the detector and as a parity gate.
288    /// End-to-end image inference with a pre-tokenized text prompt
289    /// (`tokens` has length `seq_len` == decoder context length, usually
290    /// 32). Returns the same 3-tuple the public `Sam3Processor.set_text_
291    /// prompt` exposes — without NMS / score thresholding, which we leave
292    /// to callers so parity tests can compare raw model outputs.
293    pub fn predict_image_text(
294        &mut self,
295        image_u8: &[u8],
296        h_in: usize,
297        w_in: usize,
298        tokens: &[u32],
299    ) -> Result<Sam3ImagePrediction> {
300        let nq = 200;
301        let seq_len = tokens.len();
302
303        // Vision + neck.
304        let _vision_check = self
305            .vision
306            .as_ref()
307            .context("predict_image_text requires native vision weights")?;
308        let (image_nchw, resized_hw) = preprocess_image(image_u8, h_in, w_in);
309        let vision_out = self.encode_image_dispatch(&image_nchw)?;
310        let levels = apply_neck_native(&mut self.neck, &vision_out)?;
311        // Drop the last (scale=0.5) level per scalp=1.
312        let kept = &levels[..3];
313        let backbone_fpn: Vec<Vec<f32>> = kept.iter().map(|l| l.features.clone()).collect();
314        let backbone_shapes: Vec<(usize, usize)> = kept.iter().map(|l| (l.h, l.w)).collect();
315        // Encoder input = level scale=1.0 (index 2).
316        let src_level = &kept[2];
317        let h = src_level.h;
318        let w = src_level.w;
319        let batch = 1;
320
321        // Text encoder.
322        let text_out = encode_tokens(
323            &self.text,
324            tokens,
325            batch,
326            seq_len,
327            self.gguf_packed.as_ref(),
328        )?;
329
330        // Detector encoder (single-level fusion).
331        let memory_bf = forward_encoder(
332            &self.encoder,
333            &src_level.features,
334            &src_level.pos,
335            &text_out.text_memory_resized,
336            &text_out.attention_mask,
337            batch,
338            h,
339            w,
340            seq_len,
341            self.gguf_packed.as_ref(),
342        )?;
343        // Convert FPN pos to batch-first for the decoder.
344        let mut memory_pos = vec![0f32; batch * h * w * 256];
345        for b in 0..batch {
346            for y in 0..h {
347                for xc in 0..w {
348                    for c in 0..256 {
349                        memory_pos[(b * h * w + y * w + xc) * 256 + c] =
350                            src_level.pos[((b * 256 + c) * h + y) * w + xc];
351                    }
352                }
353            }
354        }
355
356        // Detector decoder.
357        let dec = forward_decoder(
358            &self.decoder,
359            &memory_bf,
360            &memory_pos,
361            &text_out.text_memory_resized,
362            &text_out.attention_mask,
363            batch,
364            h,
365            w,
366            seq_len,
367            self.gguf_packed.as_ref(),
368        )?;
369
370        // Last-layer queries batch-first.
371        let num_layers = dec.num_layers;
372        let mut queries_last_bf = vec![0f32; batch * nq * 256];
373        let li = num_layers - 1;
374        for q in 0..nq {
375            for b in 0..batch {
376                let src = ((li * nq + q) * batch + b) * 256;
377                let dst = (b * nq + q) * 256;
378                queries_last_bf[dst..dst + 256].copy_from_slice(&dec.intermediate[src..src + 256]);
379            }
380        }
381
382        // Last layer's refined reference boxes.
383        let mut ref_last_bf = vec![0f32; batch * nq * 4];
384        for q in 0..nq {
385            for b in 0..batch {
386                let src = ((li * nq + q) * batch + b) * 4;
387                let dst = (b * nq + q) * 4;
388                ref_last_bf[dst..dst + 4]
389                    .copy_from_slice(&dec.intermediate_ref_boxes[src..src + 4]);
390            }
391        }
392
393        // Final boxes: sigmoid(inv_sigmoid(ref_last) + bbox_embed(queries_last)).
394        let delta = super::detector_decoder::bbox_embed_forward(
395            &self.decoder,
396            &queries_last_bf,
397            batch * nq,
398            self.gguf_packed.as_ref(),
399        )?;
400        let mut final_boxes_cxcywh = vec![0f32; batch * nq * 4];
401        for q in 0..nq {
402            for b in 0..batch {
403                let rb = &ref_last_bf[(b * nq + q) * 4..(b * nq + q + 1) * 4];
404                let d = &delta[(b * nq + q) * 4..(b * nq + q + 1) * 4];
405                let out_off = (b * nq + q) * 4;
406                for k in 0..4 {
407                    let inv = if rb[k] <= 0.0 {
408                        (1e-3f32 / (1.0 - 1e-3)).ln()
409                    } else if rb[k] >= 1.0 {
410                        ((1.0 - 1e-3) / 1e-3f32).ln()
411                    } else {
412                        (rb[k].max(1e-3) / (1.0 - rb[k]).max(1e-3)).ln()
413                    };
414                    let s = inv + d[k];
415                    final_boxes_cxcywh[out_off + k] = 1.0 / (1.0 + (-s).exp());
416                }
417            }
418        }
419        // Convert to xyxy.
420        let mut boxes_xyxy = vec![0f32; batch * nq * 4];
421        for i in 0..(batch * nq) {
422            let cx = final_boxes_cxcywh[i * 4];
423            let cy = final_boxes_cxcywh[i * 4 + 1];
424            let bw = final_boxes_cxcywh[i * 4 + 2];
425            let bh = final_boxes_cxcywh[i * 4 + 3];
426            boxes_xyxy[i * 4] = cx - 0.5 * bw;
427            boxes_xyxy[i * 4 + 1] = cy - 0.5 * bh;
428            boxes_xyxy[i * 4 + 2] = cx + 0.5 * bw;
429            boxes_xyxy[i * 4 + 3] = cy + 0.5 * bh;
430        }
431
432        // Scores: dot product scoring, last layer.
433        let mut hs_bf = vec![0f32; num_layers * batch * nq * 256];
434        for l in 0..num_layers {
435            for q in 0..nq {
436                for b in 0..batch {
437                    let src = ((l * nq + q) * batch + b) * 256;
438                    let dst = ((l * batch + b) * nq + q) * 256;
439                    hs_bf[dst..dst + 256].copy_from_slice(&dec.intermediate[src..src + 256]);
440                }
441            }
442        }
443        let all_scores = forward_dot_prod_scoring(
444            &self.scoring,
445            &hs_bf,
446            &text_out.text_memory_resized,
447            &text_out.attention_mask,
448            num_layers,
449            batch,
450            nq,
451            seq_len,
452            self.gguf_packed.as_ref(),
453        )?;
454        let last_scores =
455            all_scores[(num_layers - 1) * batch * nq..num_layers * batch * nq].to_vec();
456
457        // Segmentation.
458        let seg = forward_segmentation(
459            &mut self.seg_head,
460            &memory_bf,
461            &backbone_fpn,
462            &backbone_shapes,
463            &queries_last_bf,
464            &text_out.text_memory_resized,
465            &text_out.attention_mask,
466            batch,
467            h,
468            w,
469            nq,
470            seq_len,
471            self.gguf_packed.as_ref(),
472        )?;
473
474        Ok(Sam3ImagePrediction {
475            masks: seg.mask_pred,
476            mask_shape: vec![batch, nq, seg.h_out, seg.w_out],
477            boxes: boxes_xyxy,
478            boxes_shape: vec![batch, nq, 4],
479            scores: last_scores,
480            scores_shape: vec![batch, nq],
481            num_instances: nq,
482            h_out: resized_hw.0,
483            w_out: resized_hw.1,
484        })
485    }
486
487    /// Forward the segmentation head: cross-attend encoder memory to the
488    /// text prompt, run the pixel decoder, and emit per-query mask logits
489    /// plus the semantic mask.
490    #[allow(clippy::too_many_arguments)]
491    pub fn run_segmentation(
492        &mut self,
493        enc_memory_bf: &[f32],
494        backbone_fpn: &[Vec<f32>],
495        backbone_shapes: &[(usize, usize)],
496        obj_queries_last_bf: &[f32],
497        prompt_seq_first: &[f32],
498        prompt_kpm: &[u8],
499        batch: usize,
500        enc_h: usize,
501        enc_w: usize,
502        num_queries: usize,
503        seq_len: usize,
504    ) -> Result<Sam3SegmentationOutput> {
505        forward_segmentation(
506            &mut self.seg_head,
507            enc_memory_bf,
508            backbone_fpn,
509            backbone_shapes,
510            obj_queries_last_bf,
511            prompt_seq_first,
512            prompt_kpm,
513            batch,
514            enc_h,
515            enc_w,
516            num_queries,
517            seq_len,
518            self.gguf_packed.as_ref(),
519        )
520    }
521
522    /// Compute per-query, per-layer scores via mean-pooled text + linear
523    /// projections + dot product.
524    #[allow(clippy::too_many_arguments)]
525    pub fn run_dot_prod_scoring(
526        &self,
527        hs_bf: &[f32],
528        prompt_seq_first: &[f32],
529        prompt_kpm: &[u8],
530        num_layers: usize,
531        batch: usize,
532        num_queries: usize,
533        seq_len: usize,
534    ) -> Result<Vec<f32>> {
535        forward_dot_prod_scoring(
536            &self.scoring,
537            hs_bf,
538            prompt_seq_first,
539            prompt_kpm,
540            num_layers,
541            batch,
542            num_queries,
543            seq_len,
544            self.gguf_packed.as_ref(),
545        )
546    }
547
548    /// Run the detector decoder. Inputs are the encoder memory in
549    /// batch-first flat `[batch, h*w, 256]` plus matching positional
550    /// encoding, and the text memory in seq-first `[seq, batch, 256]`.
551    /// Returns intermediate layer outputs, refined boxes, and presence
552    /// logits — the same triple the upstream model uses to derive scores
553    /// and final box predictions.
554    #[allow(clippy::too_many_arguments)]
555    pub fn run_decoder(
556        &self,
557        memory: &[f32],
558        memory_pos: &[f32],
559        memory_text: &[f32],
560        text_attention_mask: &[u8],
561        batch: usize,
562        h: usize,
563        w: usize,
564        seq_len: usize,
565    ) -> Result<Sam3DecoderOutput> {
566        if rlx_ir::env::flag("RLX_SAM3_DECODER_HOST") {
567            return forward_decoder(
568                &self.decoder,
569                memory,
570                memory_pos,
571                memory_text,
572                text_attention_mask,
573                batch,
574                h,
575                w,
576                seq_len,
577                self.gguf_packed.as_ref(),
578            );
579        }
580        let dev = match rlx_ir::env::var("RLX_SAM3_DECODER_DEVICE").as_deref() {
581            Some("metal") => Device::Metal,
582            Some("mlx") => Device::Mlx,
583            Some("cuda") => Device::Cuda,
584            _ => self.device,
585        };
586        super::detector_decoder_ir::forward_decoder_ir_on_with_profile(
587            &self.decoder,
588            memory,
589            memory_pos,
590            memory_text,
591            text_attention_mask,
592            batch,
593            h,
594            w,
595            seq_len,
596            dev,
597            &self.compile_profile,
598            self.gguf_packed.as_ref(),
599        )
600    }
601
602    /// Run the detector encoder fusion on a single FPN level + text
603    /// prompt. Returns the encoded image memory in batch-first flat
604    /// `[batch, h*w, 256]`.
605    #[allow(clippy::too_many_arguments)]
606    pub fn run_encoder(
607        &self,
608        src_bchw: &[f32],
609        src_pos_bchw: &[f32],
610        prompt_seq_first: &[f32],
611        prompt_kpm: &[u8],
612        batch: usize,
613        src_h: usize,
614        src_w: usize,
615        prompt_len: usize,
616    ) -> Result<Vec<f32>> {
617        // Backend selection:
618        //   RLX_SAM3_ENCODER_HOST=1 → host-side per-head sgemm (legacy).
619        //   RLX_SAM3_ENCODER_DEVICE=metal → IR on Metal (default Cpu).
620        if rlx_ir::env::flag("RLX_SAM3_ENCODER_HOST") {
621            return forward_encoder(
622                &self.encoder,
623                src_bchw,
624                src_pos_bchw,
625                prompt_seq_first,
626                prompt_kpm,
627                batch,
628                src_h,
629                src_w,
630                prompt_len,
631                self.gguf_packed.as_ref(),
632            );
633        }
634        let dev = match rlx_ir::env::var("RLX_SAM3_ENCODER_DEVICE").as_deref() {
635            Some("metal") => Device::Metal,
636            Some("mlx") => Device::Mlx,
637            _ => Device::Cpu,
638        };
639        let _ = forward_encoder_ir; // silence unused if always _on
640        forward_encoder_ir_on_with_profile(
641            &self.encoder,
642            src_bchw,
643            src_pos_bchw,
644            prompt_seq_first,
645            prompt_kpm,
646            batch,
647            src_h,
648            src_w,
649            prompt_len,
650            dev,
651            &self.compile_profile,
652            self.gguf_packed.as_ref(),
653        )
654    }
655
656    /// Run the text encoder on already-tokenized inputs. Returns the
657    /// resized memory the detector consumes.
658    pub fn encode_text_tokens(
659        &self,
660        tokens: &[u32],
661        batch: usize,
662        seq_len: usize,
663    ) -> Result<Sam3TextEncoded> {
664        encode_tokens(
665            &self.text,
666            tokens,
667            batch,
668            seq_len,
669            self.gguf_packed.as_ref(),
670        )
671    }
672
673    pub fn predict_neck(
674        &mut self,
675        image_u8: &[u8],
676        h_in: usize,
677        w_in: usize,
678    ) -> Result<Vec<super::neck::Sam3FeatureLevel>> {
679        let _vision_check = self
680            .vision
681            .as_ref()
682            .context("SAM3 predict_neck requires native vision weights")?;
683        let (image_nchw, _) = preprocess_image(image_u8, h_in, w_in);
684        let vision_out = self.encode_image_dispatch(&image_nchw)?;
685        apply_neck_native(&mut self.neck, &vision_out)
686    }
687
688    pub fn patch_embed_image(
689        &self,
690        image_u8: &[u8],
691        h_in: usize,
692        w_in: usize,
693    ) -> Result<Sam3EncodedImage> {
694        let vision = self
695            .vision
696            .as_ref()
697            .context("SAM3 patch_embed_image requires native vision weights")?;
698        let (image_nchw, resized_hw) = preprocess_image(image_u8, h_in, w_in);
699        let patch_tokens = assemble_patch_tokens(&vision.pre, &image_nchw)?;
700        Ok(Sam3EncodedImage {
701            patch_tokens,
702            grid: vision.pre.grid,
703            embed_dim: vision.pre.embed_dim,
704            resized_hw,
705        })
706    }
707
708    pub fn predict_image(
709        &mut self,
710        image_u8: &[u8],
711        h_in: usize,
712        w_in: usize,
713        text_prompt: Option<&str>,
714        boxes: Option<&[f32]>,
715        points: Option<(&[f32], &[f32])>,
716    ) -> Result<Sam3ImagePrediction> {
717        self.predict_image_native(image_u8, h_in, w_in, text_prompt, boxes, points)
718    }
719
720    pub fn predict_video_frame(
721        &mut self,
722        state: &mut Sam3VideoState,
723        image_u8: &[u8],
724        h_in: usize,
725        w_in: usize,
726        text_prompt: Option<&str>,
727    ) -> Result<Sam3VideoFramePrediction> {
728        let pred = self.predict_image_native(image_u8, h_in, w_in, text_prompt, None, None)?;
729        Ok(tracker_forward_native(&self.tracker, state, pred))
730    }
731
732    fn predict_image_native(
733        &mut self,
734        image_u8: &[u8],
735        h_in: usize,
736        w_in: usize,
737        text_prompt: Option<&str>,
738        boxes: Option<&[f32]>,
739        points: Option<(&[f32], &[f32])>,
740    ) -> Result<Sam3ImagePrediction> {
741        ensure!(
742            image_u8.len() == h_in * w_in * 3,
743            "SAM3 image must be RGB u8 with len {} (got {})",
744            h_in * w_in * 3,
745            image_u8.len()
746        );
747        let _vision_check = self
748            .vision
749            .as_ref()
750            .context("SAM3 predict_image requires native vision weights")?;
751        let (image_nchw, resized_hw) = preprocess_image(image_u8, h_in, w_in);
752        let vision_out = self.encode_image_dispatch(&image_nchw)?;
753        let levels = apply_neck_native(&mut self.neck, &vision_out)?;
754        let text = encode_text_native(
755            &self.text,
756            &self.cfg.text,
757            text_prompt,
758            self.gguf_packed.as_ref(),
759        )?;
760        let geometry = encode_geometry_native(&self.geometry, boxes, points);
761        let det = detector_forward_native(
762            &self.detector,
763            &self.cfg.detector,
764            &levels,
765            &text,
766            &geometry,
767        )?;
768        Ok(segmentation_forward_native(
769            &self.seg,
770            &det,
771            resized_hw.0,
772            resized_hw.1,
773        ))
774    }
775}