1use 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 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 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 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 pub fn compile_profile(&self) -> &CompileProfile {
198 &self.compile_profile
199 }
200
201 pub fn config(&self) -> &Sam3Config {
202 &self.cfg
203 }
204
205 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 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 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 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 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 let src_level = &kept[2];
317 let h = src_level.h;
318 let w = src_level.w;
319 let batch = 1;
320
321 let text_out = encode_tokens(
323 &self.text,
324 tokens,
325 batch,
326 seq_len,
327 self.gguf_packed.as_ref(),
328 )?;
329
330 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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 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; 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 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}