Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
39938a6
feat(build): SAM3_CUDA ggml-CUDA backend option (NVIDIA fast-path)
njayj Jun 23, 2026
fa06b85
feat(cuda): select the ggml-CUDA backend in the model loader (CUDA->V…
njayj Jun 23, 2026
127e61b
feat(capi): expose active ggml backend name (anti silent-CPU-fallback)
njayj Jun 23, 2026
2604544
build(capi): SAM3_CAPI_SHARED C-ABI DLL target + force CUDA graphs on…
njayj Jul 4, 2026
a3194c7
feat(cuda): guarded runtime fallback chain + ggml encoder-ahead producer
njayj Jul 4, 2026
d0d2c45
cuda: fall back to manual attention for head dims ggml-cuda flash-att…
Hunter-Dolan Jul 7, 2026
356475e
cuda: stage-transport driver increment 1 — sam3_stage_feed + the 4 ne…
Hunter-Dolan Jul 7, 2026
d35fab4
cuda: stage-transport increment 2 — constant device twins + weight-re…
Hunter-Dolan Jul 7, 2026
092b313
cuda: stage-transport increment 3 — fixed bank ring + device prompt a…
Hunter-Dolan Jul 8, 2026
e1b5665
cuda: stage-transport increment 4 — device memenc→perceiver chain
Hunter-Dolan Jul 8, 2026
edb60a5
cuda: stage-transport increment 5 — device default on CUDA + async D2…
Hunter-Dolan Jul 8, 2026
f5875db
cuda: increment 6a — cached propagate stage graph (CUDA-graph replay …
Hunter-Dolan Jul 8, 2026
b8e7ecb
cuda: increment 6b — cached memory-encoder + perceiver stage graphs
Hunter-Dolan Jul 8, 2026
e865052
cuda: increment 6c — cached image-encoder stage graph
Hunter-Dolan Jul 8, 2026
56a27ec
cuda: increment 7 — device-resident encoder-ahead seam (18 fps threaded)
Hunter-Dolan Jul 8, 2026
3772b35
cuda: device frame preprocess (plan v1 step 5)
Hunter-Dolan Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ sam3.cpp — a C++14 port of Meta's SAM 3 (Segment Anything Model 3) using ggml

**Functions that follow this pattern:** `sam3_segment_pcs` (5 sub-graphs), `sam3_segment_pvs`, `sam3_propagate_single`, `sam3_encode_memory`.

4. **Device-transport addendum (CUDA stage-graph reuse).** On the CUDA
device-transport driver (`sam3_transport`; see `report/CUDA-RESIDENCY-DESIGN.md`
in the trackbench repo) a stage MAY keep its own `ggml_context` +
`ggml_cgraph` + `ggml_gallocr` alive across frames and recompute it in place
(`sam3_stage_cache`), instead of rule 1's build→compute→free-per-call,
PROVIDED: (a) the cached graph is keyed on every shape/config input that
affects its topology and is released + rebuilt on any key change; (b) stages
are never merged — one cached graph per stage, so rule 1's buffer-aliasing
rationale still holds within each compute; (c) all per-frame data still
enters through fresh input leaves (rule 2 untouched). This is what lets
ggml-CUDA's graph capture reach steady replay (stable node/leaf data
pointers) and removes the per-frame cudaMalloc/free churn. Precedent: the
encoder's persistent `state.neck_trk` buffers (steady-state fast path). The
host driver (mac/Metal/CPU) keeps the verbatim per-call pattern.

## Implementation plan

All work follows the phased plan in `PLAN.md`. Read it before starting any phase. Each phase has concrete steps, verification criteria, and the exact structs/functions to implement.
Expand Down
66 changes: 59 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,41 @@ if(NOT APPLE AND SAM3_VULKAN)
set(GGML_VULKAN ON CACHE BOOL "" FORCE)
endif()

# RFD 0011 (NVIDIA fast-path): ggml-CUDA runs the same EdgeTAM graph on NVIDIA GPUs
# with tensor-core FP16 kernels + CUDA graphs + FlashAttention. It is the NVIDIA
# sibling of the Vulkan (cross-vendor) and Metal/CoreML (Apple) backends. The cgo
# C-ABI (coreml/edgetam_capi.cpp) is reused below — its CoreML stage calls are
# #ifdef SAM3_COREML-guarded, so a CUDA build runs the pure-ggml path on the CUDA
# device. Setting GGML_CUDA=ON makes ggml define GGML_USE_CUDA PUBLIC (ggml_add_backend
# CUDA), which the model loader's #ifdef GGML_USE_CUDA branch keys on. Requires the
# CUDA Toolkit (nvcc) at build. SAM3_CUDA and SAM3_VULKAN may both be ON (fat build):
# the loader prefers CUDA then falls back to Vulkan then CPU.
option(SAM3_CUDA "Enable the ggml-CUDA backend (NVIDIA GPUs)" OFF)
if(NOT APPLE AND SAM3_CUDA)
set(GGML_CUDA ON CACHE BOOL "" FORCE)
# CUDA graphs: standalone ggml defaults GGML_CUDA_GRAPHS to OFF (llama.cpp
# flips it on in its own build). Without it, every frame pays per-kernel
# launch overhead across the EdgeTAM graph's hundreds of small kernels.
# Force it ON so the advertised "tensor-core FP16 + CUDA graphs" path is
# actually compiled; ggml auto-falls back to normal kernel launches at
# runtime for graphs it cannot capture, so this is safe.
set(GGML_CUDA_GRAPHS ON CACHE BOOL "" FORCE)
endif()

add_subdirectory(ggml)

# sam3 static library
add_library(sam3 STATIC sam3.cpp sam3.h)
if(NOT APPLE AND SAM3_CUDA)
# STEP-5 device frame preprocess (Windows parity plan v1): bit-exact
# CUDA twin of sam3_resize_bilinear + ImageNet normalize. -fmad=false is
# LOAD-BEARING for the byte gate (no FMA contraction in the double chain).
enable_language(CUDA)
target_sources(sam3 PRIVATE coreml/sam3_preproc.cu)
set_source_files_properties(coreml/sam3_preproc.cu PROPERTIES LANGUAGE CUDA)
set_property(SOURCE coreml/sam3_preproc.cu APPEND PROPERTY COMPILE_OPTIONS "-fmad=false")
target_compile_definitions(sam3 PRIVATE SAM3_PREPROC_CUDA)
endif()
target_include_directories(sam3 PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/stb
Expand Down Expand Up @@ -75,13 +106,34 @@ if(APPLE)
endif()
endif()

# RFD 0011 (Windows/Vulkan): reuse the cgo tracker C-ABI on the non-Apple Vulkan
# path WITHOUT the CoreML bridge. edgetam_capi.cpp's CoreML stage calls + producer-
# encoder pool are #ifdef SAM3_COREML-guarded, so here the tracker runs the pure-
# ggml path on the Vulkan device. SAM3_COREML is NOT defined, no .mm, no frameworks.
if(NOT APPLE AND SAM3_VULKAN)
target_sources(sam3 PRIVATE coreml/edgetam_capi.cpp)
target_include_directories(sam3 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/coreml)
# RFD 0011 (Windows/Vulkan + NVIDIA/CUDA): reuse the cgo tracker C-ABI on the
# non-Apple GPU paths WITHOUT the CoreML bridge. edgetam_capi.cpp's CoreML stage
# calls are #ifdef SAM3_COREML-guarded, so here the tracker runs the pure-ggml
# path on the Vulkan or CUDA device (with the opt-in ggml encoder-ahead producer
# on CUDA). SAM3_COREML is NOT defined, no .mm, no frameworks.
#
# SAM3_CAPI_SHARED: build the C-ABI as its own SHARED library (sam3capi.dll /
# libsam3capi.so) instead of folding it into the static libsam3. This is the
# Windows/CUDA cgo boundary: nvcc requires MSVC as its host compiler on Windows,
# and an MSVC-built C++ static lib cannot link into a mingw cgo binary (C++ ABI /
# runtime mismatch). A C-ABI DLL is toolchain-neutral — all C++ (sam3 + ggml +
# ggml-cuda + ggml-vulkan) stays inside the MSVC-built DLL and Go links only the
# 15 extern "C" entry points. Keep BUILD_SHARED_LIBS=OFF with this so ggml links
# statically INTO the DLL (one artifact + the CUDA runtime DLLs).
option(SAM3_CAPI_SHARED "Build the cgo C-ABI tracker as a shared library (Windows/MSVC cgo boundary)" OFF)
if(NOT APPLE AND (SAM3_VULKAN OR SAM3_CUDA))
if(SAM3_CAPI_SHARED)
add_library(sam3capi SHARED coreml/edgetam_capi.cpp)
target_include_directories(sam3capi PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/coreml)
# ET_CAPI_BUILD_DLL turns on __declspec(dllexport) for the capi entry
# points (see ET_API in edgetam_capi.h) — extern "C" alone does not
# export symbols from an MSVC DLL.
target_compile_definitions(sam3capi PRIVATE ET_CAPI_BUILD_DLL)
target_link_libraries(sam3capi PRIVATE sam3)
else()
target_sources(sam3 PRIVATE coreml/edgetam_capi.cpp)
target_include_directories(sam3 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/coreml)
endif()
endif()

# Examples (only when top-level)
Expand Down
86 changes: 65 additions & 21 deletions coreml/edgetam_capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ struct EtTracker {
sam3_tracker_ptr tracker;

// RFD 0011 U8 Wave 5 (encoder-ahead threading): a producer encoder handle
// (its OWN MLModel, per-thread ownership) + a fixed neck pool. Go owns slot
// free/ready coordination via channels, so NO lock here — each slot is written
// once by encode_slot (producer thread) then read once by track_slot (consumer
// thread). track_slot is the ONLY caller of sam3_coreml_set_prefetched_neck
// (global, consumer-thread-only). prod_enc is non-null only when CoreML is on.
// (its OWN engine instance, per-thread ownership) + a fixed neck pool. Go owns
// slot free/ready coordination via channels, so NO lock here — each slot is
// written once by encode_slot (producer thread) then read once by track_slot
// (consumer thread). track_slot is the ONLY caller of
// sam3_coreml_set_prefetched_neck (global, consumer-thread-only).
// At most ONE producer engine is ever non-null:
// prod_enc — CoreML MLModel (SAM3_COREML builds)
// ggml_ea — second ggml-CUDA backend instance (non-CoreML builds, opt-in
// via SAM3_GGML_ENCODER_AHEAD=1; the CoreML-threading
// equivalent for NVIDIA)
edgetam_coreml_handle prod_enc = nullptr;
sam3_encoder_ahead* ggml_ea = nullptr;
static const int POOL = 3;
std::vector<float> neck[POOL][3]; // n0 256*256*256, n1 128*128*256, n2 64*64*256

Expand Down Expand Up @@ -89,17 +95,33 @@ extern "C" edgetam_tracker_t edgetam_capi_create(const char* models_dir,
t->tracker = sam3_create_visual_tracker(*t->model, vp);
#ifdef SAM3_COREML
// Wave 5: producer encoder handle + neck pool (CoreML encoder-ahead threading).
// Absent on non-CoreML builds → prod_enc stays null → pool_size()==0 →
// the cgo session uses the synchronous (single-thread) Track path.
if (models_dir && *models_dir) {
std::string d = models_dir;
t->prod_enc = edgetam_coreml_create((d + "/edgetam_encoder_neck_nhwc.mlpackage").c_str(), /*ANE*/ 1);
}
#else
// RFD 0011 (CUDA): ggml encoder-ahead producer — the CoreML-threading
// equivalent for NVIDIA. A second ggml-CUDA backend instance encodes
// frame N+1 on its own stream while the consumer propagates frame N.
// OPT-IN (SAM3_GGML_ENCODER_AHEAD=1) and default OFF: same-GPU
// producer/consumer contention is hardware-dependent (the M4 sweep
// showed co-location can LOSE) — A/B on the target GPU first.
// sam3_encoder_ahead_create itself returns NULL unless the active
// backend is CUDA and the model is EdgeTAM, so a Vulkan/CPU fallback
// run cleanly keeps pool_size()==0 → the synchronous Track path.
const char* ea_env = getenv("SAM3_GGML_ENCODER_AHEAD");
if (ea_env && *ea_env && strcmp(ea_env, "0") != 0) {
t->ggml_ea = sam3_encoder_ahead_create(*t->model);
}
#endif
// Either producer engine present → allocate the fixed neck pool; neither
// → pool_size()==0 → the cgo session uses the synchronous Track path.
if (t->prod_enc || t->ggml_ea) {
const int Wd[3] = {256, 128, 64};
for (int s = 0; s < EtTracker::POOL; ++s)
for (int i = 0; i < 3; ++i)
t->neck[s][i].assign((size_t)256 * Wd[i] * Wd[i], 0.f);
}
#endif
return t.release();
} catch (...) {
return nullptr;
Expand Down Expand Up @@ -174,6 +196,7 @@ extern "C" void edgetam_capi_destroy(edgetam_tracker_t h) {
#ifdef SAM3_COREML
if (t && t->prod_enc) edgetam_coreml_destroy(t->prod_enc);
#endif
if (t && t->ggml_ea) sam3_encoder_ahead_destroy(t->ggml_ea);
delete t;
}

Expand All @@ -186,28 +209,36 @@ extern "C" void edgetam_capi_destroy(edgetam_tracker_t h) {

extern "C" int edgetam_capi_pool_size(edgetam_tracker_t h) {
auto* t = static_cast<EtTracker*>(h);
return (t && t->prod_enc) ? EtTracker::POOL : 0; // 0 => threading unavailable
return (t && (t->prod_enc || t->ggml_ea)) ? EtTracker::POOL : 0; // 0 => threading unavailable
}

extern "C" int edgetam_capi_encode_slot(edgetam_tracker_t h, int slot,
const uint8_t* rgb, int w, int hgt) {
auto* t = static_cast<EtTracker*>(h);
if (!t || !t->prod_enc || !rgb || slot < 0 || slot >= EtTracker::POOL) return 0;
#ifdef SAM3_COREML
if (!t || (!t->prod_enc && !t->ggml_ea) || !rgb || slot < 0 || slot >= EtTracker::POOL) return 0;
try {
#ifdef SAM3_COREML
sam3_image img = make_image(rgb, w, hgt);
std::vector<float> norm = sam3_coreml_preprocess_image(img, 1024);
return edgetam_coreml_encode(t->prod_enc, norm.data(),
t->neck[slot][0].data(),
t->neck[slot][1].data(),
t->neck[slot][2].data()) ? 1 : 0;
#else
// ggml encoder-ahead (CUDA): preprocess + encode on the producer's own
// backend instance. Device transport keeps the neck in per-slot DEVICE
// tensors (zero PCIe); host transport writes this slot's float buffers.
sam3_image img = make_image(rgb, w, hgt);
if (sam3_encoder_ahead_encode_dev(t->ggml_ea, *t->model, img, slot))
return 1;
return sam3_encoder_ahead_encode(t->ggml_ea, *t->model, img,
t->neck[slot][0].data(),
t->neck[slot][1].data(),
t->neck[slot][2].data()) ? 1 : 0;
#endif
} catch (...) {
return 0;
}
#else
(void)rgb; (void)w; (void)hgt; // encoder-ahead threading is CoreML-only
return 0;
#endif
}

extern "C" et_result edgetam_capi_track_slot(edgetam_tracker_t h, int slot,
Expand All @@ -217,13 +248,19 @@ extern "C" et_result edgetam_capi_track_slot(edgetam_tracker_t h, int slot,
auto* t = static_cast<EtTracker*>(h);
if (!t || !rgb || slot < 0 || slot >= EtTracker::POOL) return r;
try {
#ifdef SAM3_COREML
// Consumer thread only: hand the producer-encoded neck to the library so
// sam3_propagate_frame's encode step is skipped (no ggml/CoreML encode here).
sam3_coreml_set_prefetched_neck(t->neck[slot][0].data(),
t->neck[slot][1].data(),
t->neck[slot][2].data());
#endif
// sam3_propagate_frame's encode step is skipped (no in-line encode here).
// The seam is backend-agnostic — the slot was filled by the CoreML
// producer OR the ggml-CUDA encoder-ahead producer.
if (t->prod_enc || t->ggml_ea) {
// Prefer the device-resident seam (slot filled by encode_dev);
// fall back to the host-float seam for CoreML / host transport.
if (!(t->ggml_ea && sam3_set_prefetched_neck_dev(t->ggml_ea, slot))) {
sam3_coreml_set_prefetched_neck(t->neck[slot][0].data(),
t->neck[slot][1].data(),
t->neck[slot][2].data());
}
}
sam3_image img = make_image(rgb, w, hgt);
sam3_result res = sam3_propagate_frame(*t->tracker, *t->state, *t->model, img);
if (res.detections.empty()) { t->last_mask = sam3_mask{}; return r; }
Expand All @@ -245,3 +282,10 @@ extern "C" et_result edgetam_capi_track_slot(edgetam_tracker_t h, int slot,
extern "C" const char* edgetam_capi_version(void) {
return SAM3_VERSION;
}

extern "C" const char* edgetam_capi_backend(edgetam_tracker_t h) {
auto* t = static_cast<EtTracker*>(h);
// *t->model matches the existing capi pattern (sam3_encode_image(*t->state,
// *t->model, ...)); sam3_backend_name reads the model's active ggml backend.
return (t && t->model) ? sam3_backend_name(*t->model) : "none";
}
36 changes: 26 additions & 10 deletions coreml/edgetam_capi.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@

#include <stdint.h>

// ET_API: symbol visibility for the SAM3_CAPI_SHARED build (sam3capi.dll — the
// Windows/MSVC cgo boundary; see CMakeLists.txt). extern "C" alone does not
// export symbols from an MSVC DLL, so the DLL build defines ET_CAPI_BUILD_DLL
// and every entry point below carries __declspec(dllexport). Static builds
// (Mac CoreML, Linux CUDA, mingw Vulkan) expand ET_API to nothing.
#if defined(_WIN32) && defined(ET_CAPI_BUILD_DLL)
#define ET_API __declspec(dllexport)
#else
#define ET_API
#endif

#ifdef __cplusplus
extern "C" {
#endif
Expand All @@ -33,37 +44,42 @@ typedef struct {
// path + ggml fallback). models_dir = dir holding the 4 CoreML .mlpackages; if
// non-empty, the CoreML encoder/mem-attn/decoder stages are enabled. Returns
// NULL on failure.
edgetam_tracker_t edgetam_capi_create(const char* models_dir, const char* ggml_model,
ET_API edgetam_tracker_t edgetam_capi_create(const char* models_dir, const char* ggml_model,
int use_samurai, int use_lifecycle);

// Seed the target from a box on frame 0. rgb = contiguous RGB24 (w*h*3 bytes).
// seed box in PIXELS. Returns 1 on success.
int edgetam_capi_seed(edgetam_tracker_t t, const uint8_t* rgb, int w, int h, et_box seed_px);
ET_API int edgetam_capi_seed(edgetam_tracker_t t, const uint8_t* rgb, int w, int h, et_box seed_px);

// Track one frame. rgb = contiguous RGB24 (w*h*3). Returns the per-frame result
// (box normalized [0,1]); valid==0 when the target is lost this frame.
et_result edgetam_capi_track(edgetam_tracker_t t, const uint8_t* rgb, int w, int h);
ET_API et_result edgetam_capi_track(edgetam_tracker_t t, const uint8_t* rgb, int w, int h);

// RFD 0011 U8 mask export — copy the LAST track's binary mask (0/255, row-major,
// w*h bytes, original-frame resolution) into `out` (capacity `cap`). Writes dims
// to *w,*h. Returns the byte count (w*h); if cap < w*h nothing is copied and w*h
// is returned so the caller can size its buffer. 0 => no mask (lost / pre-track).
// Valid after edgetam_capi_track / edgetam_capi_track_slot on the same handle.
int edgetam_capi_last_mask(edgetam_tracker_t t, uint8_t* out, int cap, int* w, int* h);
ET_API int edgetam_capi_last_mask(edgetam_tracker_t t, uint8_t* out, int cap, int* w, int* h);

// RFD 0011 U8 Wave 5 — encoder-ahead threading split. pool_size returns the slot
// count (0 if CoreML/threading is unavailable). encode_slot (PRODUCER thread)
// preprocesses+encodes rgb into slot's neck buffers via the producer's own encoder
// handle. track_slot (CONSUMER thread) consumes slot's prefetched neck + propagates.
// Go owns slot free/ready coordination (channels) — one writer then one reader per
// slot, so these are safe to overlap across the two threads on DIFFERENT slots.
int edgetam_capi_pool_size(edgetam_tracker_t t);
int edgetam_capi_encode_slot(edgetam_tracker_t t, int slot, const uint8_t* rgb, int w, int h);
et_result edgetam_capi_track_slot(edgetam_tracker_t t, int slot, const uint8_t* rgb, int w, int h);
ET_API int edgetam_capi_pool_size(edgetam_tracker_t t);
ET_API int edgetam_capi_encode_slot(edgetam_tracker_t t, int slot, const uint8_t* rgb, int w, int h);
ET_API et_result edgetam_capi_track_slot(edgetam_tracker_t t, int slot, const uint8_t* rgb, int w, int h);

ET_API void edgetam_capi_reset(edgetam_tracker_t t);
ET_API void edgetam_capi_destroy(edgetam_tracker_t t);
ET_API const char* edgetam_capi_version(void); // SAM3_VERSION, for cgo link sanity

void edgetam_capi_reset(edgetam_tracker_t t);
void edgetam_capi_destroy(edgetam_tracker_t t);
const char* edgetam_capi_version(void); // SAM3_VERSION, for cgo link sanity
// Active ggml backend name (e.g. "CUDA0", "Vulkan0", "Metal", "CPU", or "none").
// Lets the Go side assert the GPU backend really initialized — catching a silent
// CPU fallback that would be "fast and wrong" (cf. Egor part-2 CUDA 12/13 mismatch).
ET_API const char* edgetam_capi_backend(edgetam_tracker_t t);

#ifdef __cplusplus
}
Expand Down
Loading