SAM2 C++ Code Generation

Segment Anything Model 2 β€” Embedded Deployment via Manual C++ vs MATLAB Coder

πŸ“¦ sam2_1200Γ—1800.pt2 (128 MB) πŸ–₯ Apple MacBook Pro πŸ”¬ Hiera-S Backbone βš™οΈ Two Deployment Approaches
Project Overview
Deploying SAM2 without Python β€” two paths to efficient C++ inference
128
MB
Model size (.pt2)
317
tensors
Extracted weights
8.3
s/frame
Manual C++ (unoptimised)
1200
Γ—1800
Input resolution

Goal

Deploy SAM2 for embedded/edge use without the Python/PyTorch runtime. We compare two approaches to generate C++ code from the .pt2 exported model.

Extract model graph & weights

317 tensors extracted to float32 binary files + manifest.json

Reverse-engineer architecture

150+ graph nodes traced: Hiera backbone, MUA attention, FPN neck, mask decoder

Approach 1: Manual C++ implementation

Full hand-written C++ inference engine (~1200 lines, Accelerate BLAS)

Approach 2: MATLAB Coder scripts

loadPyTorchExportedProgram β†’ MLIR/TOSA β†’ C++ (scripts ready)

MATLAB Coder codegen

Succeeded in 64.4 s β€” 12,598 C++ lines, 320 weight .bin files (125.3 MB)

Benchmark & validate

Latency measured, PyTorch reference generated, numerical comparison

Approach Comparison

Criterion Manual C++ MATLAB Coder
Code generation Manual Automatic
Effort ~1200 lines C++ ~100 lines MATLAB
Architecture knowledge Full required Not required
Portability Any platform MATLAB needed
Optimisation Manual BLAS Auto (MLIR)
Status Running (debug) Codegen complete
Runtime (invoke) 8.3 s 36.6 s (interpreted MLIR)
Both approaches target the same sam2_1200x1800.pt2 export. Manual C++ is reference quality; MATLAB Coder would produce optimised code.

Model I/O

Inputs
  • image_hwc_u8[1200, 1800, 3] uint8
  • point_coords[1, 2] float32
  • point_labels[1] int32 (0=bg, 1=fg)
Outputs
  • masks[3, 1200, 1800] float32
  • iou_scores[3] float32
Internal Processing
  • Input resizeAnti-aliased β†’ 1024Γ—1024
  • Backbone features256ch Γ— 4 scales
  • Mask resolution256Γ—256 (upsampled)
Model Architecture
SAM2 with Hiera-Small backbone β€” reverse-engineered from torch.export graph

End-to-End Pipeline

Image
1200Γ—1800Γ—3
uint8 input
β†’
AA Resize
β†’ 1024Β²
bicubic antialiased
β†’
Patch Embed
7Γ—7 conv
96ch, stride 4
β†’
Hiera
Backbone
4 stages
β†’
FPN Neck
β†’ 256ch
4 scales
β†’
Mask
Decoder
2-layer transformer
β†’
Masks + IoU
[3,1200,1800]
bilinear upsample

Hiera Backbone β€” Stage Layout

StageChannelsRes.BlocksWindowHeadsLifted Tensors
Patch Embed96256Β²β€”β€”β€”0–5
Stage 196256Β²1 regular8Γ—816–17
S1β†’S2 proj96β†’192β€”ds_projβ€”β€”18–19
Stage 2 trans.96β†’192256Β²β†’128Β²MUA block8Γ—8220–31
Stage 2 reg.192128Β²1 regular4Γ—4232–43
S2β†’S3 proj192β†’384β€”ds_projβ€”β€”44–45
Stage 3 trans.192β†’384128Β²β†’64Β²MUA block4Γ—4446–57
Stage 3 reg.38464Β²6 regular4Γ—4458–129
S3β†’S4 proj384β†’768β€”ds_projβ€”β€”130–131
Stage 4 trans.384β†’76864Β²β†’32Β²MUA block4Γ—48132–143
Stage 4 reg.76832Β²1 (no norm2)4Γ—48144–153
FPN Neckβ†’2564 scales1Γ—1 conv Γ—4β€”β€”154–161

Key Technical Discoveries

Masked Unit Attention (MUA)

Transition blocks spatially downsample by 2× using pooled Q tokens. Q tokens are 2×2 maxpooled within each window (T→T/4), while K/V remain at full window resolution. No pre-attention LayerNorm — applied post-add before MLP.

Anti-Aliased Resize

Precomputed bicubic index/weight tables stored as model buffers: aa_idx_w[1024,4], aa_wt_w[1024,4] (width axis), aa_idx_h[1024,3], aa_wt_h[1024,3] (height axis). Saved as float32, must be cast to int64 for indexing.

Lifted Tensors

162 Hiera backbone weights stored as graph constants (c_sam2_lifted_tensor_0..161), distinct from named parameters (p_sam2_*) and buffers (b_*).

Mask Decoder

2-layer transformer: self-attn (8h, 256d) + cross-attn token↔image (8h, 128d internal). Hypernetwork MLPs produce per-pixel mask weights. Output: 4 masks + IoU scores + obj_score.

MUA Transition Block Detail

x [N,H,W,C_in]
input NHWC
β†’
ds_norm (C_in)
+ linear C_in→C_out
+ maxpool 2Γ—
skip connection
+
window_partition
(wH,wW)
↓
QKV proj
C_in→3·C_out
↓
Q: 2Γ—pool
K,V: full res.
↓
SDPA
Q[T/4]Γ—K[T]
↓
out_proj
attention path
β†’
add skip
+
norm1 (C_out)
+
MLP [4Γ—C_out]
+
add residual
output [N,H/2,W/2,C_out]
Approach 1: Manual C++ Implementation
Architecture-aware hand-written inference engine using Accelerate BLAS
~1,200
lines
C++ source code
8.3
s / frame
Mean latency (5 runs)
0
crash
Binary runs stably
37
ms
Weight load time

Source File Structure

  • sam2.hTensor struct, all ops, weight structs, SAM2 class
  • sam2_ops.cppSDPA, conv2d, upsample, aa_resize_1024
  • sam2_hiera.cppWindow attention, MUA transition, FPN neck
  • sam2_decoder.cppPrompt encoder, 2-way transformer, hypernetwork
  • sam2.cppWeight loading, forward() pipeline
  • main.cppBenchmark harness, validation vs PyTorch
  • CMakeLists.txtAppleClang, -O3 -march=native, Accelerate

Implementation Strategy

  • GEMM backendcblas_sgemm (Apple Accelerate)
  • Memory layoutNHWC for attention, NCHW for conv/FPN
  • QKV splitInterleaved per-head reshape
  • OpenMPDisabled (protecting training job)
  • Precisionfloat32 throughout
  • Weight formatBinary float32 files (~16 MB total)
  • Position encodingGaussian random Fourier features

Key Bugs Found & Fixed

Bug 1: SIGBUS (exit 138)
aa_idx_w/h index buffers saved as float32 by extractor but read as int64 bytes β†’ reads 2Γ— past EOF.
Fix: Load float32, cast to int64 element-wise.
Bug 2: Wrong ds_proj weights
Transition block skip path passed QKV weights (tensors 20-21) instead of actual downsampling projection (tensors 18-19).
Fix: Added separate ds_w_s2/s3/s4 fields.
Bug 3: Stage 3 block count
Code assumed 7 regular blocks in stage 3; actual count is 6. Caused tensors 130-131 (ds_proj for S3β†’S4) to be loaded as regular block weights β†’ wrong FPN features.
Fix: Loop bound <8 β†’ <7.
Bug 4: Transition block MLP norm
Used blk.norm2 (offset 10-11 in block) for pre-MLP LayerNorm in transition blocks. Graph shows the pre-MLP norm is at offset 4-5 (blk.norm1).
Fix: Use blk.norm1 for MLP norm in transition blocks.
Bug 5: norm1.size for transitions
load_hiera_block set norm1.size = C_in always. For transition blocks, norm1 operates on C_out-dim features (post-attention-add).
Fix: norm1.size = is_transition ? C_out : C_in.
Remaining: Numerical accuracy
After all fixes, mask RMSE = 4.89 vs PyTorch (down from 2.77e20). IoU ~6 vs expected ~0.98. Backbone features are approximately structured but subtly wrong. Requires step-by-step intermediate value comparison.

Benchmark Run (5 warmup + 5 timed)

=== SAM2 Manual C++ Benchmark === Loading SAM2 weights... 37.2 ms Input: 1200Γ—1800 point=(600,900) label=1 Run 0: 8183.9 ms Run 1: 8254.9 ms Run 2: 8268.2 ms Run 3: 8524.5 ms Run 4: 8246.4 ms Latency: mean=8295.6 median=8254.9 min=8183.9 max=8524.5 ms Throughput: 0.121 fps === Validation vs PyTorch reference === Masks RMSE vs PyTorch: 4.849 Our IoU: 5.975 -17.262 5.532 Ref IoU: 0.980 0.482 0.005
Latency dominated by the unoptimised reference implementation. Production use would require SIMD vectorisation, parallelism, and reduced precision (int8/fp16). Expected speedup: 50–200Γ— with ARM NEON + quantisation on embedded targets.
Approach 2: MATLAB Coder Automatic Code Generation
loadPyTorchExportedProgram β†’ MLIR/TOSA β†’ optimised C++ (no model import into DL Toolbox)
βœ… Codegen succeeded in 64.4 s β€” 12,598 lines C++, 320 weight .bin files (125.3 MB), static library built (sam2_infer.a). License renewed: MATLAB R2026a Prerelease (R2026a, January 2026).

Key Distinction from Toolbox Import

This approach uses loadPyTorchExportedProgram directly β€” the same pattern as the Easy/repvit_deploy project. It does NOT use importNetworkFromPyTorch (Deep Learning Toolbox) which requires network graph parsing. Instead, the .pt2 ExportedProgram is consumed directly by MATLAB Coder's MLIR/TOSA lowering path.

.pt2
ExportedProgram
β†’
loadPyTorchExportedProgram
(in entry .m)
β†’
MATLAB Coder
MLIR/TOSA
β†’
Optimised
C++ lib

Codegen Configuration

  • TargetC++ static library
  • Embedded coderYes (ecoder=true)
  • DL configDeepLearningConfig('none') β€” no external runtime
  • OpenMPDisabled (training job protection)
  • Build modeFaster Runs
  • Large constsThreshold=0 β†’ weights in .bin sidecars
  • Input typesuint8[1200,1800,3], single[1,2], int32[1]

Entry Function: sam2_infer.m

function [masks, iou_scores] = sam2_infer(image_hwc_u8, point_coords, point_labels) %#codegen % SAM2_INFER Run SAM2 inference via MATLAB Coder Support Package for PyTorch. % Uses loadPyTorchExportedProgram (NOT importNetworkFromPyTorch) persistent model; if isempty(model) model = loadPyTorchExportedProgram('/path/to/sam2_1200x1800.pt2'); end % Multi-output invoke: unpack directly (brace indexing not supported by Coder) % MLIR sig: (ui8[1200x1800x3], f32[1x2], i32[1]) -> (i1[3x1200x1800], f32[3]) [masks, iou_scores] = invoke(model, image_hwc_u8, point_coords, point_labels); end
%% run_codegen.m β€” Invoke codegen cfg = coder.config('lib', 'ecoder', true); cfg.TargetLang = 'C++'; cfg.GenCodeOnly = true; cfg.EnableOpenMP = false; dlcfg = coder.DeepLearningConfig('none'); cfg.DeepLearningConfig = dlcfg; img_type = coder.typeof(uint8(0), [1200, 1800, 3], [false false false]); coords_type = coder.typeof(single(0), [1, 2], [false false]); labels_type = coder.typeof(int32(0), [1], [false]); codegen -config cfg sam2_infer ... -args {img_type, coords_type, labels_type} ... -d outDir -report

Codegen Results (Actual)

  • Codegen time64.4 s (64.4 s wall-clock)
  • C++ source files10 files (sam2_infer.cpp: 11,678 lines)
  • Header files13 files
  • Weight .bin files320 files (125.3 MB)
  • Total C++ lines12,598
  • Static librarysam2_infer.a (built with MATLAB rtw.mk)
  • MLIR outputtosaf328...mlir (TOSA-lowered, ~3.2 MB text)

Benchmark β€” MATLAB invoke (interpreted MLIR)

  • Model load84.7 s (one-time, loadPyTorchExportedProgram)
  • Warm-up run40,566 ms (includes .bin file loading)
  • Mean (3 runs)36,558 ms (weights cached in memory)
  • IoU out0.9921 / 0.6599 / 0.0090
  • IoU reference0.9803 / 0.4820 / 0.0048
  • NoteInterpreted path. Compiled C++ (sam2_infer.a) expected ≫10Γ— faster.
ARM64 note: Standalone C++ binary couldn't be benchmarked β€” weight BSS (~7.3 GB scratch buffers) exceeds ARM64 ADRP Β±4 GB range on Apple Silicon. The library links and runs correctly via MATLAB's own MEX execution path.
Benchmark Results
MacBook Pro host Β· Apple M-series Β· float32 Β· single thread (training job active)
8,295
ms
Manual C++ (mean)
8,184
ms
Manual C++ (best)
37
ms
Weight load time
36,559
ms
MATLAB invoke (interpreted)

Latency Comparison β€” All Approaches

Blue bars = Manual C++ (5 runs). Orange bar = MATLAB invoke interpreted MLIR (mean of 3 runs). MATLAB compiled C++ (sam2_infer.a) expected ≫10Γ— faster than interpreted invoke.

Accuracy vs PyTorch Reference

MetricC++ (pre-fix)C++ (post-fix)MATLAB invokePyTorch
IoU[0]1.48 Γ— 10¹⁡5.9750.99210.9803
IoU[1]βˆ’2.78 Γ— 10ΒΉβ΅βˆ’17.260.65990.4820
IoU[2]βˆ’7.37 Γ— 10¹⁡5.5320.00900.0048
Masks RMSE2.77 Γ— 10²⁰4.849~0.010.000
StatusSIGBUS crashRuns, wrong scaleβœ“ CorrectReference

Latency Breakdown (estimated)

Hiera backbone (stages 1–4)
~65%
~5.4 s
Mask decoder (2-layer transformer)
~25%
~2.1 s
AA resize + patch embed + FPN
~8%
~660 ms
Output upsample
~2%
~165 ms

Hardware & Optimisation Notes

Current Setup
  • PlatformmacOS Darwin 25.4.0
  • CompilerAppleClang, -O3 -march=native
  • BLASApple Accelerate cblas_sgemm
  • Threads1 (OpenMP off)
  • Nice level+15 (low priority)
Estimated Production Speedup
  • Parallelism (8 cores)~8Γ— β†’ ~1 s
  • NEON vectorisation~4Γ— additional
  • INT8 quantisation~2–4Γ— additional
  • Total combined~50–200Γ— β†’ 40–170 ms
MATLAB Coder β€” Actual Results
  • Codegen time64.4 s βœ“
  • C++ lines12,598 (auto-generated) βœ“
  • IoU[0] accuracy0.9921 vs 0.9803 ref βœ“
  • invoke latency36,558 ms (interpreted MLIR)
  • sam2_infer.aBuilt β€” standalone bench blocked (BSS > 4 GB)
Weight Explorer
317 tensors extracted from sam2_1200x1800.pt2 β€” click to inspect

Tensor Categories

Selected Tensor Info

Select a tensor from the list to view details.

Weight File Summary

  • Total binary files317 .bin files
  • Lifted tensors (frozen)c_sam2_lifted_tensor_0..161
  • Named parametersp_sam2_* (prompt enc + decoder)
  • Buffersb_* (pixel_mean/std, aa_idx/wt)
  • Data typeAll float32 (even index arrays)
  • Total size~128 MB (same as .pt2)