From d729d35b867e5567c2d9742c8ef6fe65691b3224 Mon Sep 17 00:00:00 2001 From: Julien Waechter Date: Sat, 18 Jul 2026 16:24:09 +0200 Subject: [PATCH 1/8] feat: add cuda backend support - enable cuda inference via cmake flag - add cuda backend initialization logic - implement flash attention with manual fallback - replace pool_1d with manual mean pooling - add cuda inference test suite - update build and readme documentation --- CLAUDE.md | 2 +- CMakeLists.txt | 7 ++ README.md | 25 +++- sam3.cpp | 207 ++++++++++++++++++++++------------ tests/CMakeLists.txt | 3 + tests/test_cuda_inference.cpp | 85 ++++++++++++++ 6 files changed, 256 insertions(+), 73 deletions(-) create mode 100644 tests/test_cuda_inference.cpp diff --git a/CLAUDE.md b/CLAUDE.md index c471d85..23108ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Project -sam3.cpp — a C++14 port of Meta's SAM 3 (Segment Anything Model 3) using ggml for inference on CPU and Metal. +sam3.cpp — a C++14 port of Meta's SAM 3 (Segment Anything Model 3) using ggml for inference on CPU, Metal, and CUDA. ## Architecture diff --git a/CMakeLists.txt b/CMakeLists.txt index 0006638..32c8dd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,13 @@ if(APPLE AND SAM3_METAL) set(GGML_METAL_EMBED_LIBRARY ON CACHE BOOL "" FORCE) endif() +# Check if CUDA should be enabled +option(SAM3_CUDA "Enable CUDA backend" OFF) +# ggml options — enable CUDA +if(SAM3_CUDA) + set(GGML_CUDA ON CACHE BOOL "" FORCE) +endif() + add_subdirectory(ggml) # sam3 static library diff --git a/README.md b/README.md index ab6c7ca..1e6766c 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,8 @@ All models are available in GGML format on Hugging Face: | Video tracking (memory bank) | Yes | Yes | Yes | Yes | | Interactive refinement | Yes | Yes | Yes | Yes | | Quantization (Q4/Q8) | Yes | Yes | Yes | Yes | -| Metal GPU | Yes | Yes | Yes | Yes | +| Metal GPU (macOS) | Yes | Yes | Yes | Yes | +| CUDA GPU (NVIDIA) | Yes | Yes | Yes | Yes | ## Building from Source @@ -202,6 +203,28 @@ Metal is enabled automatically on macOS. To disable it: cmake .. -DSAM3_METAL=OFF ``` +**CUDA support must be explicitly enabled** with the `-DSAM3_CUDA=ON` flag: + +```bash +cmake .. -DSAM3_CUDA=ON +make -j +``` + +**Prerequisites for CUDA:** +- NVIDIA GPU with CUDA support (Compute Capability 7.5+) +- NVIDIA CUDA Toolkit installed (version 11.0+) +- The `nvcc` compiler must be in your PATH +- Up-to-date NVIDIA drivers + +Check that CUDA is properly configured: +```bash +which nvcc # Should show the path to nvcc +nvcc --version # Should show CUDA version +nvidia-smi # Should show your GPU +``` + +When both CUDA and Metal backends are available, CUDA takes priority. + To build tests: ```bash diff --git a/sam3.cpp b/sam3.cpp index 43d88a8..e3a3f06 100644 --- a/sam3.cpp +++ b/sam3.cpp @@ -12,6 +12,10 @@ #include "ggml-metal.h" #endif +#ifdef GGML_USE_CUDA +#include "ggml-cuda.h" +#endif + /* stb (implementation compiled here -- order is pinned) */ #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" @@ -3276,8 +3280,14 @@ std::shared_ptr sam3_load_model(const sam3_params& params) { } // ── Init backend ───────────────────────────────────────────────────── -#ifdef GGML_USE_METAL +#ifdef GGML_USE_CUDA if (params.use_gpu) { + fprintf(stderr, "%s: using CUDA backend\n", __func__); + model->backend = ggml_backend_cuda_init(0); // device 0 + } +#endif +#ifdef GGML_USE_METAL + if (!model->backend && params.use_gpu) { fprintf(stderr, "%s: using Metal backend\n", __func__); model->backend = ggml_backend_metal_init(); } @@ -3709,6 +3719,65 @@ static struct ggml_tensor* sam3_apply_rope(struct ggml_context* ctx, return ggml_reshape_3d(ctx, ggml_cont(ctx, out), head_dim, N, nheads_B); } +// Helper: flash attention with fallback to manual SDPA for unsupported dimensions +// CUDA flash attention only supports: 40, 64, 72, 80, 96, 112, 128, 192, 256, 320, 512, 576 +// Many SAM3 components use HD=32 which requires manual SDPA fallback +static struct ggml_tensor* sam3_flash_attn_with_fallback( + struct ggml_context* ctx, + struct ggml_tensor* Q, // [HD, N_q, NH, B] + struct ggml_tensor* K, // [HD, N_kv, NH, B] + struct ggml_tensor* V, // [HD, N_kv, NH, B] (can be non-contiguous) + struct ggml_tensor* mask, // optional mask for flash_attn_ext + float scale, + int64_t HD, // head dimension + int64_t n_heads) { + // Check if flash attention is supported for this configuration + const bool use_flash_attn = (HD == 40 || HD == 64 || HD == 72 || HD == 80 || + HD == 96 || HD == 112 || HD == 128 || HD == 192 || + HD == 256 || HD == 320 || HD == 512 || HD == 576); + + if (use_flash_attn) { + return ggml_flash_attn_ext(ctx, Q, K, V, mask, scale, 0.0f, 0.0f); + } else { + // Manual SDPA for unsupported dimensions (e.g., HD=32) + const int64_t N_q = Q->ne[1]; + const int64_t N_kv = K->ne[1]; + const int64_t B = Q->ne[3]; + + // Ensure tensors are contiguous before reshaping + Q = ggml_cont(ctx, Q); + K = ggml_cont(ctx, K); + V = ggml_cont(ctx, V); + + auto* Q3 = ggml_reshape_3d(ctx, Q, HD, N_q, n_heads * B); + auto* K3 = ggml_reshape_3d(ctx, K, HD, N_kv, n_heads * B); + auto* V3 = ggml_reshape_3d(ctx, V, HD, N_kv, n_heads * B); + + // QK^T: ggml_mul_mat(K, Q) → K^T @ Q → [N_kv, N_q, NH*B] + auto* attn_scores = ggml_mul_mat(ctx, K3, Q3); + + // Apply mask and scale in one step if mask provided + if (mask) { + // For masked attention, use soft_max_ext which handles scale and mask together + attn_scores = ggml_soft_max_ext(ctx, attn_scores, mask, scale, 0.0f); + } else { + attn_scores = ggml_scale(ctx, attn_scores, scale); + attn_scores = ggml_soft_max(ctx, attn_scores); + } + + // attn @ V: transpose V then multiply + auto* VT = ggml_permute(ctx, V3, 1, 0, 2, 3); // [N_kv, HD, NH*B] + VT = ggml_cont(ctx, VT); + auto* out3 = ggml_mul_mat(ctx, VT, attn_scores); // [HD, N_q, NH*B] + + // Reshape back to 4D and permute to match flash_attn_ext output + auto* out = ggml_reshape_4d(ctx, out3, HD, N_q, n_heads, B); + out = ggml_cont(ctx, ggml_permute(ctx, out, 0, 2, 1, 3)); // [HD, NH, N_q, B] + + return out; + } +} + // Single ViT block forward: pre-norm → attn (window or global, with RoPE) → residual → pre-norm → MLP → residual // x: [E, W, H, B] in ggml layout (following sam.cpp convention) static struct ggml_tensor* sam3_vit_block_forward(struct ggml_context* ctx, @@ -4396,7 +4465,7 @@ static struct ggml_tensor* sam2_hiera_block_forward(struct ggml_context* ctx, V = ggml_permute(ctx, V, 0, 2, 1, 3); // non-contiguous OK for flash_attn float scale = 1.0f / sqrtf((float)head_dim); - auto* attn_out = ggml_flash_attn_ext(ctx, Q, K, V, nullptr, scale, 0, 0); + auto* attn_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, nullptr, scale, head_dim, NH); // Recombine: flash_attn output is [HD, N_q, NH, B_win] // → reshape to [C_out, N_q, B_win] @@ -5519,7 +5588,7 @@ static struct ggml_tensor* edgetam_perceiver_layer_forward( k = ggml_reshape_4d(ctx, k, D, N_lat, 1, batch); v = ggml_reshape_4d(ctx, v, D, N_lat, 1, batch); - auto* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, scale, 0.0f, 0.0f); + auto* attn = sam3_flash_attn_with_fallback(ctx, q, k, v, nullptr, scale, D, 1); attn = ggml_reshape_3d(ctx, attn, D, N_lat, batch); auto* sa_out = ggml_mul_mat(ctx, layer.sa_out_w, attn); @@ -6821,7 +6890,7 @@ static struct ggml_tensor * sam3_build_vit_attn_core_from_qkv(struct ggml_contex K = ggml_reshape_4d(ctx, K, HD, W_cur * H_cur, NH, B_cur); const float scale = 1.0f / sqrtf((float) HD); - struct ggml_tensor * attn_out = ggml_flash_attn_ext(ctx, Q, K, V, nullptr, scale, 0.0f, 0.0f); + struct ggml_tensor * attn_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, nullptr, scale, HD, NH); return ggml_cont(ctx, ggml_reshape_4d(ctx, attn_out, E, W_cur, H_cur, B_cur)); } @@ -7538,7 +7607,7 @@ static struct ggml_tensor* sam3_multihead_attn_fused( V = ggml_permute(ctx, V, 0, 2, 1, 3); // [HD, N_kv, NH, B] non-contiguous; flash_attn uses strides float scale = 1.0f / sqrtf((float)HD); - auto* attn_out = ggml_flash_attn_ext(ctx, Q, K, V, attn_mask, scale, 0.0f, 0.0f); + auto* attn_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, attn_mask, scale, HD, n_heads); auto* merged = ggml_reshape_3d(ctx, attn_out, D, N_q, B); merged = ggml_mul_mat(ctx, out_proj_w, merged); @@ -7721,7 +7790,7 @@ static sam3_geom_result sam3_build_geom_enc_graph( V = ggml_permute(ctx, V, 0, 2, 1, 3); float scale = 1.0f / sqrtf((float)HD); - auto* sa_out = ggml_flash_attn_ext(ctx, Q, K, V, nullptr, scale, 0.0f, 0.0f); + auto* sa_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, nullptr, scale, HD, n_heads); sa_out = ggml_reshape_3d(ctx, sa_out, D, S, 1); sa_out = ggml_mul_mat(ctx, ly.sa_out_proj_w, sa_out); @@ -7763,7 +7832,7 @@ static sam3_geom_result sam3_build_geom_enc_graph( V = ggml_permute(ctx, V, 0, 2, 1, 3); float scale = 1.0f / sqrtf((float)HD); - auto* ca_out = ggml_flash_attn_ext(ctx, Q, K, V, nullptr, scale, 0.0f, 0.0f); + auto* ca_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, nullptr, scale, HD, n_heads); ca_out = ggml_reshape_3d(ctx, ca_out, D, S_q, 1); ca_out = ggml_mul_mat(ctx, ly.ca_out_w, ca_out); @@ -8071,7 +8140,7 @@ static struct ggml_tensor* sam3_fenc_layer_forward( V = ggml_permute(ctx, V, 0, 2, 1, 3); float scale = 1.0f / sqrtf((float)HD); - auto* sa_out = ggml_flash_attn_ext(ctx, Q, K, V, nullptr, scale, 0.0f, 0.0f); + auto* sa_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, nullptr, scale, HD, n_heads); sa_out = ggml_reshape_3d(ctx, sa_out, D, N, B); sa_out = ggml_mul_mat(ctx, ly.sa_out_proj_w, sa_out); @@ -8113,7 +8182,7 @@ static struct ggml_tensor* sam3_fenc_layer_forward( auto* ca_mask = sam3_expand_token_attn_bias(ctx, prompt_attn_bias, N_q, n_heads, B); float scale = 1.0f / sqrtf((float)HD); - auto* ca_out = ggml_flash_attn_ext(ctx, Q, K, V, ca_mask, scale, 0.0f, 0.0f); + auto* ca_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, ca_mask, scale, HD, n_heads); ca_out = ggml_reshape_3d(ctx, ca_out, D, N_q, B); ca_out = ggml_mul_mat(ctx, ly.ca_out_w, ca_out); @@ -8489,7 +8558,7 @@ static struct ggml_tensor* sam3_ddec_layer_forward( V = ggml_permute(ctx, V, 0, 2, 1, 3); float scale = 1.0f / sqrtf((float)HD); - auto* sa_out = ggml_flash_attn_ext(ctx, Q, K, V, nullptr, scale, 0.0f, 0.0f); + auto* sa_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, nullptr, scale, HD, n_heads); sa_out = ggml_reshape_3d(ctx, sa_out, D, N, B); sa_out = ggml_mul_mat(ctx, ly.sa_out_proj_w, sa_out); sa_out = ggml_add(ctx, sa_out, ly.sa_out_proj_b); @@ -8532,7 +8601,7 @@ static struct ggml_tensor* sam3_ddec_layer_forward( auto* text_mask = sam3_expand_token_attn_bias(ctx, text_attn_bias, N_q, n_heads, B); float scale = 1.0f / sqrtf((float)HD); - auto* ca_out = ggml_flash_attn_ext(ctx, Q, K, V, text_mask, scale, 0.0f, 0.0f); + auto* ca_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, text_mask, scale, HD, n_heads); ca_out = ggml_reshape_3d(ctx, ca_out, D, N_q, B); ca_out = ggml_mul_mat(ctx, ly.ca_text_out_w, ca_out); ca_out = ggml_add(ctx, ca_out, ly.ca_text_out_b); @@ -8588,7 +8657,7 @@ static struct ggml_tensor* sam3_ddec_layer_forward( ca_out = ggml_mul_mat(ctx, v_t, kq); // [HD, N_q, NH, B] ca_out = ggml_cont(ctx, ggml_permute(ctx, ca_out, 0, 2, 1, 3)); } else { - ca_out = ggml_flash_attn_ext(ctx, Q, K, V, nullptr, scale, 0.0f, 0.0f); + ca_out = sam3_flash_attn_with_fallback(ctx, Q, K, V, nullptr, scale, HD, n_heads); } ca_out = ggml_reshape_3d(ctx, ca_out, D, N_q, B); @@ -8672,26 +8741,56 @@ static struct ggml_tensor* sam3_dot_product_scoring( auto* tp = ggml_cont(ctx, ggml_permute(ctx, text_mlp, 1, 0, 2, 3)); // [T, D, B] // Mask: text_valid_mask is [T, 1, B] — broadcast multiply zeros out padding tp = ggml_mul(ctx, tp, text_valid_mask); // [T, D, B] with padding zeroed - // Sum over T dimension: pool_1d with SUM kernel=T - // ggml_pool_1d AVG divides by T; we want SUM then divide by n_valid. - // Use AVG and then scale by T/n_valid? Or use a manual approach. - // Simpler: sum via pool_1d with AVG, then scale by T/n_valid. - // But n_valid is dynamic. Instead: sum = mean * T, then divide by n_valid. - // We pass n_valid as part of the mask: text_valid_mask sums to n_valid. - // pool_1d(masked, AVG, T, T, 0) = sum(masked) / T. Multiply by T → sum(masked). - // Then divide by n_valid. But n_valid is a scalar we know CPU-side. - // For simplicity: compute AVG over ALL T positions (with padding zeroed out). - // This gives sum(valid) / T. To get sum(valid) / n_valid, scale by T / n_valid. - // We embed the scale factor into the mask: mask = (T / n_valid) for valid, 0 for pad. - // Then AVG(mask * features) = sum(valid * T/n_valid) / T = sum(valid) / n_valid. ✓ - // Caller should set mask values to T/n_valid for valid tokens, 0 for padding. - auto* pooled_t = ggml_pool_1d(ctx, tp, GGML_OP_POOL_AVG, (int)T, (int)T, 0); - text_pooled = ggml_cont(ctx, ggml_permute(ctx, pooled_t, 1, 0, 2, 3)); // [D, 1, B] + + // Mean pooling without ggml_pool_1d (not supported on CUDA) + // Reshape to [D, T*B] then use mul_mat with ones vector to sum over T + // Then divide by T to get mean + auto* tp_flat = ggml_reshape_2d(ctx, tp, D, T * B); // [D, T*B] + + // Create a ones vector [T*B, 1] and multiply to sum over second dim + // Actually simpler: use ggml_sum_rows which sums over rows + // But we need to average. Let's use a scale factor instead. + // Reshape back and use ggml_mean if available, or manual approach + + // Manual mean: reshape to [D*B, T], sum each row, divide by T + auto* reshaped = ggml_reshape_3d(ctx, tp, T, D, B); // [T, D, B] + reshaped = ggml_cont(ctx, ggml_permute(ctx, reshaped, 1, 0, 2, 3)); // [D, T, B] + + // Sum over dimension 1 (T) then divide: create [D, 1, B] + // Use sum_rows which sums dim1, but result needs proper shape + // Actually use ggml_view to extract each position and sum manually + // Simpler: average by scaling after summing + struct ggml_tensor* text_pooled_temp = nullptr; + for (int t = 0; t < (int)T; ++t) { + auto* slice = ggml_view_3d(ctx, reshaped, D, 1, B, + reshaped->nb[1], reshaped->nb[2], + t * reshaped->nb[1]); + if (text_pooled_temp == nullptr) { + text_pooled_temp = ggml_cont(ctx, slice); + } else { + text_pooled_temp = ggml_add(ctx, text_pooled_temp, slice); + } + } + text_pooled_temp = ggml_scale(ctx, text_pooled_temp, 1.0f / (float)T); + text_pooled = text_pooled_temp; // [D, 1, B] } else { - // All tokens valid — simple mean - auto* tp = ggml_cont(ctx, ggml_permute(ctx, text_mlp, 1, 0, 2, 3)); - auto* pooled_t = ggml_pool_1d(ctx, tp, GGML_OP_POOL_AVG, (int)T, (int)T, 0); - text_pooled = ggml_cont(ctx, ggml_permute(ctx, pooled_t, 1, 0, 2, 3)); + // All tokens valid — simple mean using same approach + auto* tp = ggml_cont(ctx, ggml_permute(ctx, text_mlp, 1, 0, 2, 3)); // [T, D, B] + auto* reshaped = ggml_cont(ctx, ggml_permute(ctx, tp, 1, 0, 2, 3)); // [D, T, B] + + struct ggml_tensor* text_pooled_temp = nullptr; + for (int t = 0; t < (int)T; ++t) { + auto* slice = ggml_view_3d(ctx, reshaped, D, 1, B, + reshaped->nb[1], reshaped->nb[2], + t * reshaped->nb[1]); + if (text_pooled_temp == nullptr) { + text_pooled_temp = ggml_cont(ctx, slice); + } else { + text_pooled_temp = ggml_add(ctx, text_pooled_temp, slice); + } + } + text_pooled_temp = ggml_scale(ctx, text_pooled_temp, 1.0f / (float)T); + text_pooled = text_pooled_temp; // [D, 1, B] } ggml_set_name(text_pooled, "scoring_pooled"); @@ -9268,7 +9367,7 @@ static struct ggml_tensor* sam3_build_mem_attn_graph( v = ggml_permute(ctx, v, 0, 2, 1, 3); float scale = 1.0f / sqrtf((float)D); - auto* sa_out = ggml_flash_attn_ext(ctx, q, k, v, nullptr, scale, 0.0f, 0.0f); + auto* sa_out = sam3_flash_attn_with_fallback(ctx, q, k, v, nullptr, scale, D, 1); sa_out = ggml_reshape_3d(ctx, sa_out, D, N, 1); sa_out = ggml_add(ctx, ggml_mul_mat(ctx, ly.sa_out_w, sa_out), ly.sa_out_b); x = ggml_add(ctx, x, sa_out); @@ -9313,7 +9412,7 @@ static struct ggml_tensor* sam3_build_mem_attn_graph( v = ggml_permute(ctx, v, 0, 2, 1, 3); float scale = 1.0f / sqrtf((float)D); - auto* ca_out = ggml_flash_attn_ext(ctx, q, k, v, nullptr, scale, 0.0f, 0.0f); + auto* ca_out = sam3_flash_attn_with_fallback(ctx, q, k, v, nullptr, scale, D, 1); ca_out = ggml_reshape_3d(ctx, ca_out, D, N, 1); ca_out = ggml_add(ctx, ggml_mul_mat(ctx, ly.ca_out_w, ca_out), ly.ca_out_b); x = ggml_add(ctx, x, ca_out); @@ -9671,6 +9770,7 @@ sam3_result sam3_segment_pcs(sam3_state& state, return sam3_result{}; } + #if SAM3_LOG_LEVEL >= 1 auto t_start = std::chrono::high_resolution_clock::now(); #endif @@ -10205,45 +10305,10 @@ static struct ggml_tensor* sam3_sam_attention( V = ggml_reshape_4d(ctx, V, HD, n_heads, N_kv, B); V = ggml_cont(ctx, ggml_permute(ctx, V, 0, 2, 1, 3)); // [HD, N_kv, NH, B] contiguous - // Attention + // Attention with automatic fallback for unsupported dimensions float scale = 1.0f / sqrtf((float)HD); - auto* out = ggml_flash_attn_ext(ctx, Q, K, V, nullptr, scale, 0.0f, 0.0f); - // out: [HD, NH, N_q, B] (flash_attn_ext swaps dims 1,2 vs input) - -#if 0 // Manual SDPA (for debugging only) - auto* Q3 = ggml_reshape_3d(ctx, Q, HD, N_q, n_heads * B); - auto* K3 = ggml_reshape_3d(ctx, K, HD, N_kv, n_heads * B); - auto* V3 = ggml_reshape_3d(ctx, V, HD, N_kv, n_heads * B); - // QK^T: ggml_mul_mat(K, Q) → K^T @ Q → [N_kv, N_q, NH*B] - auto* attn_scores = ggml_mul_mat(ctx, K3, Q3); - attn_scores = ggml_scale(ctx, attn_scores, scale); - attn_scores = ggml_soft_max(ctx, attn_scores); - - // attn @ V: need attn^T [N_q, N_kv] and V^T [HD, N_kv] - // ggml_mul_mat(attn^T, V) = (attn^T)^T @ V = attn @ V = [N_q, HD]... no. - // ggml_mul_mat(A, B) = A^T @ B where A=[K, M], B=[K, N] → [M, N] - // Want: output[q, d] = sum_k attn[q, k] * V[k, d] - // = (V^T @ attn^T)^T... let me think differently. - // attn_scores is [N_kv, N_q, NH*B]. For each head: - // attn[k, q] = attn_scores[k, q] (col q has the weights for query q) - // V3 is [HD, N_kv, NH*B]. - // Want: out[d, q] = sum_k V[d, k] * attn[k, q] = V @ attn - // = ggml_mul_mat? mul_mat(A, B) = A^T B with A=[K, M], B=[K, N] → [M, N] - // V has ne=[HD, N_kv, ...]. attn has ne=[N_kv, N_q, ...]. - // If A=V3 (ne0=HD, ne1=N_kv) and B=attn_scores (ne0=N_kv, ne1=N_q): - // Shared dim ne0: V3 ne0=HD ≠ attn ne0=N_kv. Mismatch! - // - // Need to transpose V: V^T is [N_kv, HD]. Then A=V^T, B=attn_scores. - // A ne0=N_kv, B ne0=N_kv → shared. A^T B = V @ attn → [HD, N_q]. ✓ - auto* VT = ggml_permute(ctx, V3, 1, 0, 2, 3); // [N_kv, HD, NH*B] - VT = ggml_cont(ctx, VT); - auto* out3 = ggml_mul_mat(ctx, VT, attn_scores); // [HD, N_q, NH*B] - - // Reshape back to 4D: [HD, N_q, NH, B] - auto* out = ggml_reshape_4d(ctx, out3, HD, N_q, n_heads, B); - // Permute to [HD, NH, N_q, B] to match flash_attn_ext output convention - out = ggml_cont(ctx, ggml_permute(ctx, out, 0, 2, 1, 3)); -#endif + auto* out = sam3_flash_attn_with_fallback(ctx, Q, K, V, nullptr, scale, HD, n_heads); + // out: [HD, NH, N_q, B] // Merge heads: [ID=HD*NH, N_q, B] auto* merged = ggml_reshape_3d(ctx, out, ID, N_q, B); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8e27787..2d0f2ee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -127,3 +127,6 @@ target_link_libraries(test_sam2_pe_cache_size PRIVATE sam3) add_executable(test_sam2_video_debug test_sam2_video_debug.cpp) target_link_libraries(test_sam2_video_debug PRIVATE sam3) +add_executable(test_cuda_inference test_cuda_inference.cpp) +target_link_libraries(test_cuda_inference PRIVATE sam3) + diff --git a/tests/test_cuda_inference.cpp b/tests/test_cuda_inference.cpp new file mode 100644 index 0000000..1cf32b2 --- /dev/null +++ b/tests/test_cuda_inference.cpp @@ -0,0 +1,85 @@ +/** + * Simple test to validate that CUDA works during inference + */ + +#include "sam3.h" +#include +#include + +int main(int argc, char** argv) { + if (argc < 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + const char* model_path = argv[1]; + const char* image_path = argv[2]; + + printf("=== CUDA inference test for sam3.cpp ===\n\n"); + + // Configuration + sam3_params params; + params.model_path = model_path; + params.use_gpu = true; + params.n_threads = 4; + + printf("1. Loading model...\n"); + auto model = sam3_load_model(params); + if (!model) { + fprintf(stderr, "Error: failed to load model\n"); + return 1; + } + printf(" ✓ Model loaded\n\n"); + + // Create state + printf("2. Creating inference state...\n"); + auto state = sam3_create_state(*model, params); + if (!state) { + fprintf(stderr, "Error: failed to create state\n"); + return 1; + } + printf(" ✓ State created\n\n"); + + // Load image + printf("3. Loading image: %s\n", image_path); + auto image = sam3_load_image(image_path); + if (image.data.empty()) { + fprintf(stderr, "Error: failed to load image\n"); + return 1; + } + printf(" ✓ Image loaded: %dx%d\n\n", image.width, image.height); + + // Encode image (uses GPU if CUDA is active) + printf("4. Encoding image (uses CUDA if available)...\n"); + bool success = sam3_encode_image(*state, *model, image); + if (!success) { + fprintf(stderr, "Error: encoding failed\n"); + return 1; + } + printf(" ✓ Encoding successful\n\n"); + + // Segmentation with a point + printf("5. Testing segmentation with a point...\n"); + sam3_pvs_params pvs; + pvs.pos_points.push_back({image.width / 2.0f, image.height / 2.0f}); + + auto result = sam3_segment_pvs(*state, *model, pvs); + if (result.detections.empty()) { + fprintf(stderr, "Warning: no detections\n"); + } else { + printf(" ✓ Segmentation successful: %zu detection(s)\n", result.detections.size()); + for (size_t i = 0; i < result.detections.size(); ++i) { + printf(" - Detection %zu: IoU=%.3f, size=%dx%d\n", + i, result.detections[i].iou_score, + result.detections[i].mask.width, + result.detections[i].mask.height); + } + } + + printf("\n=== ✓ CUDA inference test passed! ===\n"); + printf("\nIf you saw 'using CUDA backend' at the beginning,\n"); + printf("then CUDA is working correctly for inference.\n"); + + return 0; +} + From 37181595793dfa13e3df4e9dda3a974276bd49f1 Mon Sep 17 00:00:00 2001 From: Julien Waechter Date: Wed, 5 Aug 2026 08:54:02 +0200 Subject: [PATCH 2/8] feat: add encode-img-size argument for dynamic grid sizing - add --encode-img-size cli argument to override default grid dimensions - update help text to include the new flag - crop global attention RoPE frequencies to effective grid size - derive feature map sizes from actual encoder token count - support variable spatial grids in position embeddings and FPN - add timing logs for pcs stages (text, geo, fusion, detr, seghead) --- examples/main_image.cpp | 4 +- sam3.cpp | 87 +++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/examples/main_image.cpp b/examples/main_image.cpp index 3af8b7a..535e1a4 100644 --- a/examples/main_image.cpp +++ b/examples/main_image.cpp @@ -364,12 +364,14 @@ int main(int argc, char** argv) { image_path = argv[++i]; } else if (strcmp(argv[i], "--threads") == 0 && i+1 < argc) { app.params.n_threads = atoi(argv[++i]); + } else if (strcmp(argv[i], "--encode-img-size") == 0 && i+1 < argc) { + app.params.encode_img_size = atoi(argv[++i]); } else if (strcmp(argv[i], "--no-gpu") == 0) { app.params.use_gpu = false; } else if (strcmp(argv[i], "--help") == 0) { fprintf(stderr, "Usage: %s --model [--image ]\n" - " [--threads N] [--no-gpu]\n", argv[0]); + " [--threads N] [--encode-img-size N] [--no-gpu]\n", argv[0]); return 0; } } diff --git a/sam3.cpp b/sam3.cpp index e3a3f06..c19b6b2 100644 --- a/sam3.cpp +++ b/sam3.cpp @@ -3667,6 +3667,25 @@ static void sam3_get_1d_sine_pe(float* out, float pos_ind, int dim, // All ViT graph functions use the sam.cpp convention: // ne[0] = embed_dim (E=1024), ne[1] = spatial W, ne[2] = spatial H, ne[3] = batch +// Crop the per-position RoPE freqs of a global-attention block to the effective +// grid (supports encode_img_size overrides). freqs_cis is [2, half, N] with +// N = grid_native^2 in row-major token order (p = w + grid*h). Returns a strided +// view covering the top-left grid_eff x grid_eff positions of the native grid. +static struct ggml_tensor* sam3_crop_global_freqs(struct ggml_context* ctx, + struct ggml_tensor* freqs, + int64_t grid_native, + int64_t grid_eff) { + if (grid_eff >= grid_native) { + return freqs; + } + const int64_t half = freqs->ne[1]; + auto* fr4 = ggml_reshape_4d(ctx, freqs, 2, half, grid_native, grid_native); + auto* view = ggml_view_4d(ctx, fr4, 2, half, grid_eff, grid_eff, + fr4->nb[0], fr4->nb[1], fr4->nb[2], fr4->nb[3]); + auto* cont = ggml_cont(ctx, view); + return ggml_reshape_3d(ctx, cont, 2, half, grid_eff * grid_eff); +} + // Apply RoPE to Q and K tensors using complex multiplication. // x shape: [head_dim, N, num_heads*B] in ggml layout // freqs_cis shape: [2, 32, N] in ggml layout — stored as (cos,sin) interleaved pairs @@ -3720,8 +3739,7 @@ static struct ggml_tensor* sam3_apply_rope(struct ggml_context* ctx, } // Helper: flash attention with fallback to manual SDPA for unsupported dimensions -// CUDA flash attention only supports: 40, 64, 72, 80, 96, 112, 128, 192, 256, 320, 512, 576 -// Many SAM3 components use HD=32 which requires manual SDPA fallback +// CUDA flash attention only supports: 32, 40, 64, 72, 80, 96, 112, 128, 192, 256, 320, 512, 576 static struct ggml_tensor* sam3_flash_attn_with_fallback( struct ggml_context* ctx, struct ggml_tensor* Q, // [HD, N_q, NH, B] @@ -3732,14 +3750,25 @@ static struct ggml_tensor* sam3_flash_attn_with_fallback( int64_t HD, // head dimension int64_t n_heads) { // Check if flash attention is supported for this configuration - const bool use_flash_attn = (HD == 40 || HD == 64 || HD == 72 || HD == 80 || + const bool use_flash_attn = (HD == 32 || HD == 40 || HD == 64 || HD == 72 || HD == 80 || HD == 96 || HD == 112 || HD == 128 || HD == 192 || HD == 256 || HD == 320 || HD == 512 || HD == 576); if (use_flash_attn) { - return ggml_flash_attn_ext(ctx, Q, K, V, mask, scale, 0.0f, 0.0f); + // Backend flash attention kernels require a contiguous F16 mask shared across + // all heads (ne[2] == 1). Masks built by sam3_expand_token_attn_bias broadcast + // the same bias to every head, so a view of the first head slice is numerically + // identical and unlocks the fused kernel (previously all masked attention fell + // back to manual SDPA). + struct ggml_tensor* flash_mask = mask; + if (mask != nullptr) { + flash_mask = ggml_view_4d(ctx, mask, mask->ne[0], mask->ne[1], 1, mask->ne[3], + mask->nb[1], mask->nb[2], mask->nb[3], 0); + flash_mask = ggml_cont(ctx, flash_mask); + } + return ggml_flash_attn_ext(ctx, Q, K, V, flash_mask, scale, 0.0f, 0.0f); } else { - // Manual SDPA for unsupported dimensions (e.g., HD=32) + // Manual SDPA for masked attention or unsupported head dimensions const int64_t N_q = Q->ne[1]; const int64_t N_kv = K->ne[1]; const int64_t B = Q->ne[3]; @@ -3836,8 +3865,12 @@ static struct ggml_tensor* sam3_vit_block_forward(struct ggml_context* ctx, V = ggml_permute(ctx, V, 0, 2, 1, 3); // [HD, N, NH, B_cur] non-contiguous view; flash_attn uses strides if (blk.freqs_cis) { - Q = sam3_apply_rope(ctx, Q, blk.freqs_cis); - K = sam3_apply_rope(ctx, K, blk.freqs_cis); + struct ggml_tensor* freqs = blk.freqs_cis; + if (is_global) { + freqs = sam3_crop_global_freqs(ctx, freqs, hp.n_img_embd(), x->ne[1]); + } + Q = sam3_apply_rope(ctx, Q, freqs); + K = sam3_apply_rope(ctx, K, freqs); } Q = ggml_reshape_4d(ctx, Q, HD, W_cur * H_cur, NH, B_cur); @@ -3881,16 +3914,17 @@ static struct ggml_tensor* sam3_build_vit_prefix_graph(struct ggml_context* ctx, const sam3_model& model) { const auto& hp = model.hparams; const int E = hp.vit_embed_dim; // 1024 - const int H = hp.n_img_embd(); // 72 - const int W = hp.n_img_embd(); // 72 // Patch embedding: ggml conv outputs [W, H, E, 1], permute to [E, W, H, B] auto* x = ggml_conv_2d_sk_p0(ctx, model.vit.patch_embed_w, input); x = ggml_cont(ctx, ggml_permute(ctx, x, 1, 2, 0, 3)); - // pos_embed [E, 24, 24] is Hiera pretrained resolution — tile 3x3 to [E, 72, 72] + // pos_embed [E, 24, 24] is Hiera pretrained resolution — tile to [E, W, H] + // using the effective spatial grid of the conv output (supports encode_img_size override). + const int64_t W_eff = x->ne[1]; + const int64_t H_eff = x->ne[2]; auto* pos_2d = model.vit.pos_embed; - auto* pos_target = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, E, W, H, 1); + auto* pos_target = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, E, W_eff, H_eff, 1); auto* pos_tiled = ggml_repeat(ctx, pos_2d, pos_target); x = ggml_add(ctx, x, pos_tiled); @@ -6220,11 +6254,12 @@ bool sam3_encode_image(sam3_state& state, // PEs live in a separate buffer so they survive gallocr teardown. { const int neck_dim = hp.neck_dim; // 256 + const int fs = sam3_eff_feat_size(state, hp); // effective grid (encode_img_size) const int scale_sizes[4] = { - hp.n_img_embd() * 4, // 288 - hp.n_img_embd() * 2, // 144 - hp.n_img_embd(), // 72 - hp.n_img_embd() / 2, // 36 + fs * 4, // 288 at native + fs * 2, // 144 at native + fs, // 72 at native + fs / 2, // 36 at native }; size_t pe_total = 0; @@ -7500,11 +7535,12 @@ bool sam3_encode_image_from_preprocessed(sam3_state& state, // Compute sinusoidal PEs { const int neck_dim = hp.neck_dim; + const int fs = sam3_eff_feat_size(state, hp); // effective grid (encode_img_size) const int scale_sizes[4] = { - hp.n_img_embd() * 4, - hp.n_img_embd() * 2, - hp.n_img_embd(), - hp.n_img_embd() / 2, + fs * 4, + fs * 2, + fs, + fs / 2, }; if (state.pe_buf) { @@ -8861,7 +8897,9 @@ static sam3_ddec_output sam3_build_ddec_graph( const int D = hp.neck_dim; // 256 const int NQ = hp.ddec_num_queries; // 200 const int B = (int)enc_feats->ne[2]; // batch (1) - const int feat_hw = hp.n_img_embd(); // 72 + // Grid size of the encoded image — derive from the actual rpb_coords tensor + // (sized from the effective feat_size, which encode_img_size may override). + const int feat_hw = rpb_coords ? (int)rpb_coords->ne[0] : (int)hp.n_img_embd(); // ── Initialize queries from query_embed ────────────────────────────── auto* content = ggml_reshape_3d(ctx, model.ddec.query_embed, D, NQ, 1); @@ -9101,8 +9139,9 @@ static struct ggml_tensor* sam3_build_seg_head_graph( // enc: [D, N_spatial, B] ggml_set_name(enc, "seg_enc_after_ca"); - // Replace lowest-res FPN feat with spatial portion of encoder output - const int64_t feat_hw = model.hparams.n_img_embd(); // 72 + // Replace lowest-res FPN feat with spatial portion of encoder output. + // Grid size derived from the actual encoder token count (honors encode_img_size). + const int64_t feat_hw = (int64_t)std::sqrt((double)enc->ne[1]); auto* enc_spatial = ggml_reshape_4d(ctx, enc, D, feat_hw, feat_hw, B); #ifndef NDEBUG auto* enc_spatial_dbg = ggml_cont(ctx, ggml_permute(ctx, enc_spatial, 2, 0, 1, 3)); @@ -9776,10 +9815,10 @@ sam3_result sam3_segment_pcs(sam3_state& state, #endif const auto& hp = model.hparams; const int D = hp.neck_dim; // 256 - const int H = hp.n_img_embd(); // 72 + const int H = sam3_eff_feat_size(state, hp); // 72 at native; honors encode_img_size const int L = hp.text_ctx_len; // 32 const int NQ = hp.ddec_num_queries; // 200 - const int N_spatial = H * H; // 5184 + const int N_spatial = H * H; // 5184 at native sam3_result result; // ── Check that image has been encoded ──────────────────────────────── From e649cc9f5501673619b1306c25ee785c80009d92 Mon Sep 17 00:00:00 2001 From: Julien Waechter Date: Wed, 5 Aug 2026 10:10:36 +0200 Subject: [PATCH 3/8] fix: track prompt changes without manual reset - store applied prompt in state to detect edits - auto-reset tracker when text input changes - extract reset logic into reset_all function - update Reset button to use new function - save propagated mask logits for pending masklets - fix masklet id churn by storing mask data early --- examples/main_video.cpp | 52 +++++++++++++++++++++++++++-------------- sam3.cpp | 14 +++++++++-- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/examples/main_video.cpp b/examples/main_video.cpp index b0efe00..f9218b9 100644 --- a/examples/main_video.cpp +++ b/examples/main_video.cpp @@ -117,6 +117,7 @@ struct vapp_state { // Tracking char text_prompt[256] = {}; + std::string applied_prompt; // prompt used by the current tracker sam3_video_params track_params; sam3_result result; bool tracker_created = false; @@ -231,6 +232,7 @@ static void create_tracker(vapp_state& app) { app.tracker = sam3_create_tracker(*app.model, app.track_params); } app.tracker_created = (app.tracker != nullptr); + app.applied_prompt = app.text_prompt; snprintf(app.status, sizeof(app.status), app.tracker_created ? "Tracker created. Press Play or add instances." : "Failed to create tracker."); @@ -343,6 +345,29 @@ static void clear_init_prompts(vapp_state& app) { app.has_init_box = false; } +// Full reset: stop playback, drop the tracker, re-create it from the current +// mode/prompt, and re-encode the first frame. Used by the Reset button and +// whenever the text prompt changes. +static void reset_all(vapp_state& app) { + app.playing = false; + app.tracker_created = false; + app.frame_encoded = false; + app.tracker.reset(); + app.result = {}; + app.frame_index = 0; + app.timeline.clear(); + app.timeline_max_frame = -1; + clear_init_prompts(app); + if (app.visual_only && app.init_mode == VMODE_TEXT) + app.init_mode = VMODE_BOX; + create_tracker(app); + if (app.tracker_created && !app.video_path.empty()) { + decode_and_track(app, 0); + } else if (!app.video_path.empty()) { + app.frame = sam3_decode_video_frame(app.video_path, 0); + } +} + static void export_frame_masks(const vapp_state& app) { for (size_t i = 0; i < app.result.detections.size(); ++i) { char path[256]; @@ -609,6 +634,14 @@ int main(int argc, char** argv) { ImGui::SameLine(); ImGui::SetNextItemWidth(200); ImGui::InputText("##prompt", app.text_prompt, sizeof(app.text_prompt)); + // Re-apply the tracker once the user finishes editing, so the new + // prompt takes effect without having to press Reset manually. + if (ImGui::IsItemDeactivatedAfterEdit() && + app.tracker_created && app.applied_prompt != app.text_prompt) { + reset_all(app); + snprintf(app.status, sizeof(app.status), "Prompt changed to \"%s\". Tracker re-created.", + app.text_prompt); + } } // Playback buttons @@ -629,24 +662,7 @@ int main(int argc, char** argv) { } ImGui::SameLine(); if (ImGui::Button("Reset")) { - app.playing = false; - app.tracker_created = false; - app.frame_encoded = false; - app.tracker.reset(); - app.result = {}; - app.frame_index = 0; - app.timeline.clear(); - app.timeline_max_frame = -1; - clear_init_prompts(app); - if (app.visual_only && app.init_mode == VMODE_TEXT) - app.init_mode = VMODE_BOX; - // Re-create tracker and encode first frame - create_tracker(app); - if (app.tracker_created && !app.video_path.empty()) { - decode_and_track(app, 0); - } else if (!app.video_path.empty()) { - app.frame = sam3_decode_video_frame(app.video_path, 0); - } + reset_all(app); snprintf(app.status, sizeof(app.status), "Reset. Ready to annotate."); } diff --git a/sam3.cpp b/sam3.cpp index c19b6b2..670dc4a 100644 --- a/sam3.cpp +++ b/sam3.cpp @@ -11900,9 +11900,19 @@ sam3_result sam3_track_frame(sam3_tracker& tracker, sam3_state& state, ml.last_seen = fi; auto r2 = sam3_bilinear_interpolate(p2.mask_logits.data(), p2.mask_w, p2.mask_h, state.orig_width, state.orig_height); + // Store the propagated mask so pending masklets participate in + // sam3_match_detections on this frame (otherwise every PCS + // detection creates a fresh pending masklet and IDs churn every + // frame; after hotstart_delay they all become duplicate actives). + pm[id].width = state.orig_width; + pm[id].height = state.orig_height; + pm[id].data.resize(state.orig_width * state.orig_height); int fg2 = 0; - for (auto v : r2) - if (v > 0.0f) fg2++; + for (int p = 0; p < (int)r2.size(); ++p) { + bool f = r2[p] > 0.0f; + pm[id].data[p] = f ? 255 : 0; + if (f) fg2++; + } float c2 = (float)fg2 / (state.orig_width * state.orig_height); ml.mds_sum += (c2 > 0.001f && p2.obj_score > 0.0f) ? 1 : -1; sam3_encode_memory(tracker, state, model, id, From 1a99d4bfaad14a0f30d37674d1030b5fce229bd0 Mon Sep 17 00:00:00 2001 From: Julien Waechter Date: Wed, 5 Aug 2026 11:21:43 +0200 Subject: [PATCH 4/8] fix: add missing standard library includes --- tests/test_metal_block_stage_compare.cpp | 1 + tests/test_metal_vit.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/test_metal_block_stage_compare.cpp b/tests/test_metal_block_stage_compare.cpp index 28c486e..8128132 100644 --- a/tests/test_metal_block_stage_compare.cpp +++ b/tests/test_metal_block_stage_compare.cpp @@ -1,6 +1,7 @@ #include "sam3.h" #include "test_utils.h" +#include #include #include #include diff --git a/tests/test_metal_vit.cpp b/tests/test_metal_vit.cpp index 9b5fef6..6e44fb2 100644 --- a/tests/test_metal_vit.cpp +++ b/tests/test_metal_vit.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include From 59be4f7d2de10cfae4c6ac1b5f0f3d8b75737683 Mon Sep 17 00:00:00 2001 From: Julien Waechter Date: Wed, 5 Aug 2026 16:23:37 +0200 Subject: [PATCH 5/8] feat: add install targets and packaging config - configure cmake install rules for headers, libs, and binaries - generate package config and version files for downstream usage - add install targets for all example and test executables - enable cpack to build tgz archives --- CMakeLists.txt | 37 ++++++++++++++++++++++++++++++ cmake/sam3-config.cmake.in | 20 ++++++++++++++++ examples/CMakeLists.txt | 6 +++++ tests/CMakeLists.txt | 47 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 cmake/sam3-config.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 32c8dd4..6f0d09b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,3 +45,40 @@ option(SAM3_BUILD_TESTS "Build test executables" OFF) if(SAM3_BUILD_TESTS) add_subdirectory(tests) endif() + +# --- Install --- +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +set(SAM3_INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Location of sam3 header files") +set(SAM3_LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Location of sam3 library files") +set(SAM3_BIN_INSTALL_DIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Location of sam3 binary files") + +install(TARGETS sam3 ARCHIVE DESTINATION ${SAM3_LIB_INSTALL_DIR}) +install(FILES sam3.h DESTINATION ${SAM3_INCLUDE_INSTALL_DIR}) +install(FILES stb/stb_image.h stb/stb_image_write.h DESTINATION ${SAM3_INCLUDE_INSTALL_DIR}/stb) + +configure_package_config_file( + cmake/sam3-config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/sam3-config.cmake + INSTALL_DESTINATION ${SAM3_LIB_INSTALL_DIR}/cmake/sam3 + PATH_VARS SAM3_INCLUDE_INSTALL_DIR SAM3_LIB_INSTALL_DIR SAM3_BIN_INSTALL_DIR) + +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/sam3-version.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/sam3-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/sam3-version.cmake + DESTINATION ${SAM3_LIB_INSTALL_DIR}/cmake/sam3) + +# Package archive (make package / cpack) +set(CPACK_PACKAGE_NAME "sam3") +set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) +set(CPACK_PACKAGE_VENDOR "sam3.cpp") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "C++ inference for Meta SAM 3 on ggml") +set(CPACK_GENERATOR "TGZ") +include(CPack) + diff --git a/cmake/sam3-config.cmake.in b/cmake/sam3-config.cmake.in new file mode 100644 index 0000000..8bd02c0 --- /dev/null +++ b/cmake/sam3-config.cmake.in @@ -0,0 +1,20 @@ +@PACKAGE_INIT@ + +# Find all dependencies before creating any target. +include(CMakeFindDependencyMacro) +find_dependency(ggml) + +set_and_check(SAM3_INCLUDE_DIR "@PACKAGE_SAM3_INCLUDE_INSTALL_DIR@") +set_and_check(SAM3_LIB_DIR "@PACKAGE_SAM3_LIB_INSTALL_DIR@") + +if(NOT TARGET sam3::sam3) + add_library(sam3::sam3 STATIC IMPORTED) + set_target_properties(sam3::sam3 + PROPERTIES + IMPORTED_LOCATION "${SAM3_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}sam3${CMAKE_STATIC_LIBRARY_SUFFIX}" + INTERFACE_INCLUDE_DIRECTORIES "${SAM3_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "ggml::all" + INTERFACE_COMPILE_FEATURES cxx_std_14) +endif() + +check_required_components(sam3) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 98c3c0e..5e2dfad 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -10,6 +10,9 @@ target_link_libraries(sam3_benchmark PRIVATE sam3) add_executable(sam3_profile_edgetam profile_edgetam.cpp) target_link_libraries(sam3_profile_edgetam PRIVATE sam3) +install(TARGETS sam3_quantize sam3_benchmark sam3_profile_edgetam + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + # SDL2 + ImGui are optional — build only if SDL2 is found. find_package(SDL2 QUIET) @@ -21,6 +24,9 @@ if(SDL2_FOUND) add_executable(sam3_video main_video.cpp) target_link_libraries(sam3_video PRIVATE sam3 imgui-sdl2 SDL2::SDL2) + + install(TARGETS sam3_image sam3_video + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) else() message(STATUS "SDL2 not found — skipping GUI examples") endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2d0f2ee..3e280d0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -130,3 +130,50 @@ target_link_libraries(test_sam2_video_debug PRIVATE sam3) add_executable(test_cuda_inference test_cuda_inference.cpp) target_link_libraries(test_cuda_inference PRIVATE sam3) +install(TARGETS + test_load + test_tokenizer + test_vit + test_phase5 + test_phase6 + test_metal_phase6 + test_phase3 + test_phase4 + test_phase7 + test_debug_encoder + test_preprocess_match + test_jpeg_decode + test_geom_enc + test_debug_fenc + test_text_enc_dump + test_e2e_pvs + test_e2e_pcs + test_text_enc_cat + test_llama_pcs + test_pvs_point + test_video_2frame + test_video_text + test_metal_win + test_metal_vit + test_metal_encoder_checkpoints + test_metal_flash_attn_ext_block15_compare + test_metal_vit_upstream_compare + test_metal_vit_prefix_compare + test_metal_block0_stage_compare + test_metal_block_stage_compare + test_metal_python_ref_compare + test_metal_conv_dw + test_metal_profile + test_metal_overhead + test_v_cont_removal + test_metal_real_profile + test_metal_conv_transpose_2d_stress + test_metal_conv_transpose_2d + test_visual_only + test_visual_only_compare + test_sam2_backbone_compare + test_sam2_pe_cache_size + test_sam2_video_debug + test_cuda_inference + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + From e2221d298103a91014f83a88ee4a7ad8850405a6 Mon Sep 17 00:00:00 2001 From: Julien Waechter Date: Wed, 5 Aug 2026 16:24:56 +0200 Subject: [PATCH 6/8] chore: update ggml submodule to use fork with cuda and metal support --- .gitmodules | 2 +- ggml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 4831ad0..04d930d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "ggml"] path = ggml - url = https://github.com/PABannier/ggml.git + url = https://github.com/greenjava/ggml.git diff --git a/ggml b/ggml index 331b9cb..499c8a7 160000 --- a/ggml +++ b/ggml @@ -1 +1 @@ -Subproject commit 331b9cba52b23d895bc4ad218c007eb5e667540f +Subproject commit 499c8a7611f7fde381fb8e9cc141adbbbbb6be75 From 38adcc690abd3e131b071a435dc653c6c45fe910 Mon Sep 17 00:00:00 2001 From: Julien Waechter Date: Wed, 5 Aug 2026 17:27:59 +0200 Subject: [PATCH 7/8] ci: add cuda support and update ci/release pipelines - add cuda build matrix for linux ci and release - replace manual packaging with cpack - standardize artifact naming and upload paths --- .github/workflows/ci.yml | 76 ++++++++++++++++-------- .github/workflows/release.yml | 109 +++++++++++----------------------- CMakeLists.txt | 9 ++- 3 files changed, 94 insertions(+), 100 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e76d312..3e6d661 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,19 +18,25 @@ jobs: fail-fast: false matrix: include: - - name: macOS (Clang) + - name: macOS (Clang, Metal) os: macos-latest - cmake_args: "" + cmake_args: "-DCMAKE_PREFIX_PATH=/opt/homebrew -DSAM3_BUILD_TESTS=ON" build_args: "" - - name: Linux (GCC) - os: ubuntu-latest - cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF" + - name: Ubuntu 24.04 (GCC) + os: ubuntu-24.04 + cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DSAM3_BUILD_TESTS=ON" build_args: "" + - name: Ubuntu 24.04 (GCC + CUDA) + os: ubuntu-24.04 + cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DSAM3_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=75 -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache -DSAM3_BUILD_TESTS=ON" + build_args: "" + cuda: true + - name: Windows (MSVC) os: windows-latest - cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -A x64 -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows" + cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DSAM3_BUILD_TESTS=ON -A x64 -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows" build_args: "--config Release" steps: @@ -39,13 +45,19 @@ jobs: with: submodules: recursive + - name: Setup CUDA toolkit + if: matrix.cuda + uses: Jimver/cuda-toolkit@v0.2.21 + with: + cuda: '12.8' + - name: Install Ninja (Linux) if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y ninja-build + run: sudo apt-get update && sudo apt-get install -y ninja-build libsdl2-dev libgl1-mesa-dev - name: Install Ninja (macOS) if: runner.os == 'macOS' - run: brew install ninja + run: brew install ninja sdl2 - name: Install SDL2 (Windows) if: runner.os == 'Windows' @@ -55,7 +67,7 @@ jobs: - name: ccache uses: hendrikmuhs/ccache-action@v1 with: - key: ci-${{ matrix.os }} + key: ci-${{ matrix.name }} - name: Configure run: > @@ -69,27 +81,39 @@ jobs: - name: Build run: cmake --build build ${{ matrix.build_args }} --parallel - - name: Verify build artifacts + - name: Install and verify package tree shell: bash run: | - if [ -f build/libsam3.a ]; then - echo "Found build/libsam3.a" - elif [ -f build/Release/sam3.lib ]; then - echo "Found build/Release/sam3.lib" - elif [ -f build/sam3.lib ]; then - echo "Found build/sam3.lib" + PREFIX="$RUNNER_TEMP/sam3-install" + cmake --install build ${{ matrix.build_args }} --prefix "$PREFIX" + + if [ -f "$PREFIX/lib/libsam3.a" ] || [ -f "$PREFIX/lib/sam3.lib" ]; then + echo "libsam3 installed OK" + else + echo "ERROR: sam3 library not found in install tree" + exit 1 + fi + + if [ -f "$PREFIX/include/sam3.h" ]; then + echo "sam3.h installed OK" + else + echo "ERROR: sam3.h not found in install tree" + exit 1 + fi + + if [ -f "$PREFIX/bin/sam3_image" ] || [ -f "$PREFIX/bin/sam3_image.exe" ]; then + echo "GUI examples installed OK" else - echo "ERROR: sam3 library not found" + echo "ERROR: sam3_image not found in install tree (SDL2 missing?)" exit 1 fi - if [ "$RUNNER_OS" = "Windows" ]; then - for exe in sam3_image.exe sam3_video.exe; do - if [ -f "build/examples/Release/$exe" ]; then - echo "Found build/examples/Release/$exe" - else - echo "ERROR: $exe not built (GUI examples were silently skipped — likely SDL2 not found)" - exit 1 - fi - done + if [ -f "$PREFIX/bin/test_load" ] || [ -f "$PREFIX/bin/test_load.exe" ]; then + echo "tests installed OK" + else + echo "ERROR: test_load not found in install tree" + exit 1 fi + + echo "Installed binaries:" + find "$PREFIX/bin" -maxdepth 1 -type f -printf "%f\n" | sort diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index efd8560..459e2d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,38 +15,41 @@ jobs: include: - name: macOS arm64 (Metal) os: macos-latest - artifact: sam3-darwin-arm64 - cmake_args: "-DBUILD_SHARED_LIBS=OFF" - build_args: "" + pkg: sam3-darwin-arm64 ext: tar.gz + cmake_args: "-DBUILD_SHARED_LIBS=OFF -DSAM3_BUILD_EXAMPLES=OFF -DSAM3_BUILD_TESTS=OFF" - name: macOS x86_64 os: macos-latest - artifact: sam3-darwin-x86_64 - cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF -DCMAKE_OSX_ARCHITECTURES=x86_64 -DGGML_NATIVE=OFF" - build_args: "" + pkg: sam3-darwin-x86_64 ext: tar.gz + cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF -DCMAKE_OSX_ARCHITECTURES=x86_64 -DGGML_NATIVE=OFF -DSAM3_BUILD_EXAMPLES=OFF -DSAM3_BUILD_TESTS=OFF" - - name: Linux x86_64 - os: ubuntu-latest - artifact: sam3-linux-x86_64 - cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF" - build_args: "" + - name: Ubuntu 24.04 x86_64 + os: ubuntu-24.04 + pkg: sam3-linux-x86_64-ubuntu24.04 ext: tar.gz + cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF -DSAM3_BUILD_EXAMPLES=OFF -DSAM3_BUILD_TESTS=OFF" - - name: Linux arm64 + - name: Ubuntu 24.04 arm64 os: ubuntu-24.04-arm - artifact: sam3-linux-arm64 - cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF" - build_args: "" + pkg: sam3-linux-arm64-ubuntu24.04 ext: tar.gz + cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF -DSAM3_BUILD_EXAMPLES=OFF -DSAM3_BUILD_TESTS=OFF" + + - name: Ubuntu 24.04 x86_64 (CUDA) + os: ubuntu-24.04 + pkg: sam3-linux-x86_64-ubuntu24.04-cuda + ext: tar.gz + cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF -DSAM3_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=\"75-virtual;80-virtual;86-real;89-real;90-virtual\" -DSAM3_BUILD_EXAMPLES=OFF -DSAM3_BUILD_TESTS=OFF" + cuda: true - name: Windows x86_64 os: windows-latest - artifact: sam3-win-x86_64 - cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF -A x64" - build_args: "--config Release" + pkg: sam3-win-x86_64 ext: zip + cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DBUILD_SHARED_LIBS=OFF -A x64 -DSAM3_BUILD_EXAMPLES=OFF -DSAM3_BUILD_TESTS=OFF" + build_args: "--config Release" steps: - name: Checkout @@ -54,6 +57,12 @@ jobs: with: submodules: recursive + - name: Setup CUDA toolkit + if: matrix.cuda + uses: Jimver/cuda-toolkit@v0.2.21 + with: + cuda: '12.8' + - name: Install Ninja (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y ninja-build @@ -65,13 +74,14 @@ jobs: - name: ccache uses: hendrikmuhs/ccache-action@v1 with: - key: release-${{ matrix.artifact }} + key: release-${{ matrix.pkg }} - name: Configure run: > cmake -B build ${{ runner.os != 'Windows' && '-G Ninja' || '' }} ${{ matrix.cmake_args }} + -DCPACK_PACKAGE_FILE_NAME=${{ matrix.pkg }} ${{ runner.os != 'Windows' && '-DCMAKE_BUILD_TYPE=Release' || '' }} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache @@ -79,62 +89,14 @@ jobs: - name: Build run: cmake --build build ${{ matrix.build_args }} --parallel - - name: Package (Unix) - if: runner.os != 'Windows' - run: | - mkdir -p staging/${{ matrix.artifact }}/{lib,include,bin} - - cp build/libsam3.a staging/${{ matrix.artifact }}/lib/ - find build/ggml -name "*.a" -exec cp {} staging/${{ matrix.artifact }}/lib/ \; - - cp sam3.h staging/${{ matrix.artifact }}/include/ - cp -r ggml/include/* staging/${{ matrix.artifact }}/include/ - - for bin in sam3_quantize sam3_benchmark sam3_profile_edgetam; do - if [ -f "build/examples/${bin}" ]; then - cp "build/examples/${bin}" staging/${{ matrix.artifact }}/bin/ - fi - done - - cd staging - tar czf ../${{ matrix.artifact }}.tar.gz ${{ matrix.artifact }} - - - name: Package (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - New-Item -ItemType Directory -Force -Path staging/${{ matrix.artifact }}/lib - New-Item -ItemType Directory -Force -Path staging/${{ matrix.artifact }}/include - New-Item -ItemType Directory -Force -Path staging/${{ matrix.artifact }}/bin - - # Library (MSVC multi-config puts outputs in Release/) - $libPaths = @("build/Release/sam3.lib", "build/sam3.lib") - foreach ($p in $libPaths) { - if (Test-Path $p) { Copy-Item $p staging/${{ matrix.artifact }}/lib/; break } - } - - # ggml static libs - Get-ChildItem -Path build/ggml -Recurse -Filter "*.lib" | Copy-Item -Destination staging/${{ matrix.artifact }}/lib/ - - # Headers - Copy-Item sam3.h staging/${{ matrix.artifact }}/include/ - Get-ChildItem -Path ggml/include -Filter "*.h" | Copy-Item -Destination staging/${{ matrix.artifact }}/include/ - - # Example binaries - foreach ($bin in @("sam3_quantize", "sam3_benchmark", "sam3_profile_edgetam")) { - $paths = @("build/examples/Release/${bin}.exe", "build/examples/${bin}.exe") - foreach ($p in $paths) { - if (Test-Path $p) { Copy-Item $p staging/${{ matrix.artifact }}/bin/; break } - } - } - - Compress-Archive -Path staging/${{ matrix.artifact }} -DestinationPath ${{ matrix.artifact }}.zip + - name: Package (CPack) + run: cmake --build build ${{ matrix.build_args }} --target package - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: ${{ matrix.artifact }} - path: ${{ matrix.artifact }}.${{ matrix.ext }} + name: ${{ matrix.pkg }} + path: build/${{ matrix.pkg }}.${{ matrix.ext }} release: name: Create Release @@ -160,6 +122,7 @@ jobs: files: | artifacts/sam3-darwin-arm64/*.tar.gz artifacts/sam3-darwin-x86_64/*.tar.gz - artifacts/sam3-linux-x86_64/*.tar.gz - artifacts/sam3-linux-arm64/*.tar.gz + artifacts/sam3-linux-x86_64-ubuntu24.04/*.tar.gz + artifacts/sam3-linux-arm64-ubuntu24.04/*.tar.gz + artifacts/sam3-linux-x86_64-ubuntu24.04-cuda/*.tar.gz artifacts/sam3-win-x86_64/*.zip diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f0d09b..ff5d055 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,13 @@ set(CPACK_PACKAGE_NAME "sam3") set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) set(CPACK_PACKAGE_VENDOR "sam3.cpp") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "C++ inference for Meta SAM 3 on ggml") -set(CPACK_GENERATOR "TGZ") +if(WIN32) + set(CPACK_GENERATOR "ZIP") +else() + set(CPACK_GENERATOR "TGZ") +endif() +if(NOT CPACK_PACKAGE_FILE_NAME) + set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}") +endif() include(CPack) From bfe91df6fb2824f0ff233f03f66444f6597bddd3 Mon Sep 17 00:00:00 2001 From: Julien Waechter Date: Thu, 6 Aug 2026 10:33:56 +0200 Subject: [PATCH 8/8] ci: update actions and fix cross-platform mkdir - upgrade checkout, upload, download artifact, and release actions - replace system("mkdir") calls with portable macro - move ensure_dir logic to dedicated test_fs.h header --- .github/workflows/ci.yml | 16 ++++++---- .github/workflows/release.yml | 14 ++++---- tests/test_fs.h | 48 ++++++++++++++++++++++++++++ tests/test_phase3.cpp | 13 +++----- tests/test_sam2_backbone_compare.cpp | 3 +- tests/test_sam2_pvs_compare.cpp | 3 +- tests/test_sam2_video_debug.cpp | 3 +- tests/test_text_enc_dump.cpp | 3 +- tests/test_utils.h | 39 ++-------------------- tests/test_vit.cpp | 9 +++--- 10 files changed, 84 insertions(+), 67 deletions(-) create mode 100644 tests/test_fs.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e6d661..bc95fac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,36 +20,40 @@ jobs: include: - name: macOS (Clang, Metal) os: macos-latest + key: macos cmake_args: "-DCMAKE_PREFIX_PATH=/opt/homebrew -DSAM3_BUILD_TESTS=ON" build_args: "" - name: Ubuntu 24.04 (GCC) os: ubuntu-24.04 + key: ubuntu cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DSAM3_BUILD_TESTS=ON" build_args: "" - name: Ubuntu 24.04 (GCC + CUDA) os: ubuntu-24.04 + key: ubuntu-cuda cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DSAM3_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=75 -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache -DSAM3_BUILD_TESTS=ON" build_args: "" cuda: true - name: Windows (MSVC) os: windows-latest + key: windows cmake_args: "-DGGML_METAL=OFF -DSAM3_METAL=OFF -DSAM3_BUILD_TESTS=ON -A x64 -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows" build_args: "--config Release" steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive - name: Setup CUDA toolkit if: matrix.cuda - uses: Jimver/cuda-toolkit@v0.2.21 + uses: Jimver/cuda-toolkit@v0.2.35 with: - cuda: '12.8' + cuda: '12.8.0' - name: Install Ninja (Linux) if: runner.os == 'Linux' @@ -65,9 +69,9 @@ jobs: run: vcpkg install sdl2:x64-windows - name: ccache - uses: hendrikmuhs/ccache-action@v1 + uses: hendrikmuhs/ccache-action@v1.2.23 with: - key: ci-${{ matrix.name }} + key: ci-${{ matrix.key }} - name: Configure run: > @@ -116,4 +120,4 @@ jobs: fi echo "Installed binaries:" - find "$PREFIX/bin" -maxdepth 1 -type f -printf "%f\n" | sort + ls -1 "$PREFIX/bin" | sort diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 459e2d9..a57b812 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,15 +53,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive - name: Setup CUDA toolkit if: matrix.cuda - uses: Jimver/cuda-toolkit@v0.2.21 + uses: Jimver/cuda-toolkit@v0.2.35 with: - cuda: '12.8' + cuda: '12.8.0' - name: Install Ninja (Linux) if: runner.os == 'Linux' @@ -72,7 +72,7 @@ jobs: run: brew install ninja - name: ccache - uses: hendrikmuhs/ccache-action@v1 + uses: hendrikmuhs/ccache-action@v1.2.23 with: key: release-${{ matrix.pkg }} @@ -93,7 +93,7 @@ jobs: run: cmake --build build ${{ matrix.build_args }} --target package - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ matrix.pkg }} path: build/${{ matrix.pkg }}.${{ matrix.ext }} @@ -107,12 +107,12 @@ jobs: steps: - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 with: path: artifacts - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ github.ref_name }} name: sam3.cpp ${{ github.ref_name }} diff --git a/tests/test_fs.h b/tests/test_fs.h new file mode 100644 index 0000000..e44c38a --- /dev/null +++ b/tests/test_fs.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#define SAM3_MKDIR(p) _mkdir(p) +#else +#define SAM3_MKDIR(p) mkdir(p, 0755) +#endif + +static inline bool ensure_dir(const std::string & path) { + if (path.empty()) { + return true; + } + + std::string cur; + size_t pos = 0; + if (path[0] == '/') { + cur = "/"; + pos = 1; + } + + while (pos <= path.size()) { + size_t next = path.find('/', pos); + if (next == std::string::npos) { + next = path.size(); + } + + const std::string part = path.substr(pos, next - pos); + if (!part.empty()) { + if (!cur.empty() && cur.back() != '/') { + cur.push_back('/'); + } + cur += part; + if (SAM3_MKDIR(cur.c_str()) != 0 && errno != EEXIST) { + return false; + } + } + + pos = next + 1; + } + + return true; +} diff --git a/tests/test_phase3.cpp b/tests/test_phase3.cpp index 3472efe..cc76910 100644 --- a/tests/test_phase3.cpp +++ b/tests/test_phase3.cpp @@ -1,3 +1,5 @@ +#define _USE_MATH_DEFINES + // Phase 3 Numerical Audit — test ViT backbone, neck, sinusoidal PE, RoPE // against Python reference tensors dumped by dump_phase3_reference.py. // @@ -11,6 +13,7 @@ // Compares intermediate and final outputs against Python references. #include "sam3.h" +#include "test_fs.h" #include #include @@ -289,10 +292,7 @@ static void test_encode_image(const std::string & model_path, fprintf(stderr, " Image encoding completed successfully\n"); std::string dump_dir = ref_dir + "/cpp_out_phase3"; - { - std::string cmd = "mkdir -p " + dump_dir; - (void)system(cmd.c_str()); - } + ensure_dir(dump_dir); // ── Helper: dump and compare a state tensor ────────────────────────── // C++ tensors are in ggml layout. We need to transpose to PyTorch layout. @@ -491,10 +491,7 @@ static void test_encode_from_preprocessed(const std::string & model_path, } std::string dump_dir = ref_dir + "/cpp_out_from_preproc"; - { - std::string cmd = "mkdir -p " + dump_dir; - (void)system(cmd.c_str()); - } + ensure_dir(dump_dir); // ── Compare ViT output ──────────────────────────────────────────────── fprintf(stderr, "\n --- ViT Output (same input) ---\n"); diff --git a/tests/test_sam2_backbone_compare.cpp b/tests/test_sam2_backbone_compare.cpp index 153f3ae..6eaef8a 100644 --- a/tests/test_sam2_backbone_compare.cpp +++ b/tests/test_sam2_backbone_compare.cpp @@ -3,6 +3,7 @@ // Usage: SAM2_DUMP_DIR=/tmp/debug_sam2_cpp test_sam2_backbone_compare #include "sam3.h" +#include "test_utils.h" #include #include #include @@ -14,7 +15,7 @@ int main(int argc, char** argv) { return 1; } - mkdir(argv[3], 0755); + SAM3_MKDIR(argv[3]); std::ifstream fin(argv[2], std::ios::binary); fin.seekg(0, std::ios::end); diff --git a/tests/test_sam2_pvs_compare.cpp b/tests/test_sam2_pvs_compare.cpp index 93efbd6..c43cc2f 100644 --- a/tests/test_sam2_pvs_compare.cpp +++ b/tests/test_sam2_pvs_compare.cpp @@ -4,6 +4,7 @@ // Default: orig 1200x1198, point at (600, 599) #include "sam3.h" +#include "test_utils.h" #include #include #include @@ -25,7 +26,7 @@ int main(int argc, char** argv) { float point_x = (argc > 6) ? atof(argv[6]) : 600.0f; float point_y = (argc > 7) ? atof(argv[7]) : 599.0f; - mkdir(argv[3], 0755); + SAM3_MKDIR(argv[3]); std::ifstream fin(argv[2], std::ios::binary); fin.seekg(0, std::ios::end); diff --git a/tests/test_sam2_video_debug.cpp b/tests/test_sam2_video_debug.cpp index 13917e7..9e8b0ee 100644 --- a/tests/test_sam2_video_debug.cpp +++ b/tests/test_sam2_video_debug.cpp @@ -12,6 +12,7 @@ * ~/Documents/sam2/notebooks/videos/bedroom 210 350 */ #include "sam3.h" +#include "test_utils.h" #include #include #include @@ -60,7 +61,7 @@ int main(int argc, char** argv) { float point_y = (argc > 4) ? atof(argv[4]) : 350.0f; const char* dump_dir = "/tmp/debug_sam2_cpp"; - mkdir(dump_dir, 0755); + SAM3_MKDIR(dump_dir); const int N_FRAMES = argc > 5 ? atoi(argv[5]) : 5; int encode_img_size = (argc > 6) ? atoi(argv[6]) : 0; diff --git a/tests/test_text_enc_dump.cpp b/tests/test_text_enc_dump.cpp index 174d647..333b4c5 100644 --- a/tests/test_text_enc_dump.cpp +++ b/tests/test_text_enc_dump.cpp @@ -1,4 +1,5 @@ #include "sam3.h" +#include "test_utils.h" #include #include @@ -15,7 +16,7 @@ int main(int argc, char ** argv) { const std::string output_dir = argv[2]; // Create output directory - mkdir(output_dir.c_str(), 0755); + SAM3_MKDIR(output_dir.c_str()); // Load tokenizer from embedded data in model file if (!sam3_test_load_tokenizer(model_path)) { diff --git a/tests/test_utils.h b/tests/test_utils.h index ef5907a..0406198 100644 --- a/tests/test_utils.h +++ b/tests/test_utils.h @@ -1,13 +1,12 @@ #pragma once -#include +#include "test_fs.h" + #include #include #include #include #include -#include -#include #include struct ref_tensor_f32 { @@ -158,37 +157,3 @@ static inline int compare_exact_i32(const std::vector & got, return n_bad; } -static inline bool ensure_dir(const std::string & path) { - if (path.empty()) { - return true; - } - - std::string cur; - size_t pos = 0; - if (path[0] == '/') { - cur = "/"; - pos = 1; - } - - while (pos <= path.size()) { - size_t next = path.find('/', pos); - if (next == std::string::npos) { - next = path.size(); - } - - const std::string part = path.substr(pos, next - pos); - if (!part.empty()) { - if (!cur.empty() && cur.back() != '/') { - cur.push_back('/'); - } - cur += part; - if (mkdir(cur.c_str(), 0755) != 0 && errno != EEXIST) { - return false; - } - } - - pos = next + 1; - } - - return true; -} diff --git a/tests/test_vit.cpp b/tests/test_vit.cpp index affb76f..6843f30 100644 --- a/tests/test_vit.cpp +++ b/tests/test_vit.cpp @@ -1,4 +1,7 @@ +#define _USE_MATH_DEFINES + #include "sam3.h" +#include "test_fs.h" #include #include @@ -331,11 +334,7 @@ static bool test_encode_image(const std::string & model_path, int n_fail_enc = 0; std::string dump_dir = ref_dir + "/cpp_out"; - { - // Create dump dir - std::string cmd = "mkdir -p " + dump_dir; - (void)system(cmd.c_str()); - } + ensure_dir(dump_dir); // Helper to compare intermediate tensors. // ggml layout [E, W, H], Python ref is [1, H, W, E] (NHWC)