feat(engine): HDF5-native galleries with embedded calibration; TensorRT backends; scene detection

Gallery format switches from JSON to HDF5 exclusively (JSON read-only kept for
back-compat): save_gallery always writes HDF5, and the fitted Platt-sigmoid
calibration (a, b, valid, hash) is now embedded directly in the gallery file
instead of a sidecar .calib_cache.json — identity_matcher reads it from the
loaded gallery and writes back only when the embeddings actually changed
(hash mismatch), skipping the O(n^2) refit otherwise.

Also includes: TensorRT inference backend support (ort_backend.cpp,
trt_backend.cpp), gemm_backend improvements, TransNetV2-based scene-boundary
detection wired through frame_source/face_tracker/main, and CMake build
target updates for the new sources.

Bumps the KPN submodule to feature/persistent-pipeline-reuse (push_blocking
backpressure, node_ptr/node_stats introspection, ObjectVariantNodeWrapper for
stateful functors) — needed by the optimizer's sae_kpn Python bindings.
This commit is contained in:
2026-07-19 19:04:03 +02:00
parent aca6147d69
commit 41a277bc19
19 changed files with 1151 additions and 216 deletions
+70 -8
View File
@@ -1,9 +1,11 @@
// ── Gallery GEMM backend ──────────────────────────────────────────────────────
// GPU similarity engine for the identity matcher. Uploads the reference gallery
// once and computes the per-frame similarity matrix with a single SGEMM. The GPU
// math library is selected at compile time by CMake (SAE_GEMM_BACKEND):
// cuBLAS/CUDA or rocBLAS/HIP. This is the ONLY translation unit that includes
// cublas/cuda or rocblas/hip headers.
// Similarity engine for the identity matcher. Uploads the reference gallery
// once and computes the per-frame similarity matrix with a single SGEMM. The
// math backend is selected at compile time by CMake (SAE_GEMM_BACKEND):
// cuBLAS/CUDA, rocBLAS/HIP, or a portable CPU reference (SAE_GEMM_CPU). The two
// GPU paths are the ONLY translation units that include cublas/cuda or
// rocblas/hip headers; the CPU path pulls in no GPU headers at all and exists so
// the pipeline can build and be tested on a machine without a GPU (CI).
#include "inference/similarity.hpp"
@@ -13,8 +15,10 @@
#elif defined(SAE_GEMM_ROCM)
#include <hip/hip_runtime.h>
#include <rocblas/rocblas.h>
#elif defined(SAE_GEMM_CPU)
// no external headers — portable reference implementation below
#else
#error "gemm_backend.cpp requires SAE_GEMM_CUDA or SAE_GEMM_ROCM to be defined"
#error "gemm_backend.cpp requires SAE_GEMM_CUDA, SAE_GEMM_ROCM or SAE_GEMM_CPU to be defined"
#endif
#include <cstring>
@@ -26,6 +30,64 @@
namespace {
constexpr int kDim = 512;
#if defined(SAE_GEMM_CPU)
// ── CPU reference engine ──────────────────────────────────────────────────────
// Portable, dependency-free path used for CI and as the correctness oracle for
// the GPU backends. The gallery is L2-normalised (as are the queries), so each
// similarity is a plain dot product. S is stored column-major to match the GPU
// backends: the gallery similarities for face fi start at result + fi*n_gallery.
class SimilarityEngine final : public ISimilarityEngine {
public:
SimilarityEngine(const float* gallery_row_major, int n_gallery, int max_faces)
: n_gallery_(n_gallery), max_faces_(max_faces),
gallery_(gallery_row_major,
gallery_row_major + static_cast<size_t>(n_gallery) * kDim)
{
host_sims_.resize(static_cast<size_t>(max_faces_) * n_gallery_);
std::cerr << "[similarity] CPU reference engine: gallery resident in host RAM ("
<< (gallery_.size() * sizeof(float)) / (1024 * 1024) << " MiB)\n";
}
int max_faces() const override { return max_faces_; }
const float* compute(const float* query_row_major, int n_faces) override {
if (n_faces <= 0) return host_sims_.data();
if (n_faces > max_faces_)
throw std::runtime_error("SimilarityEngine: n_faces exceeds max_faces");
// S(g, f) col-major = dot(gallery[g], query[f]).
for (int f = 0; f < n_faces; ++f) {
const float* q = query_row_major + static_cast<size_t>(f) * kDim;
float* out = host_sims_.data() + static_cast<size_t>(f) * n_gallery_;
for (int g = 0; g < n_gallery_; ++g) {
const float* row = gallery_.data() + static_cast<size_t>(g) * kDim;
float acc = 0.f;
for (int d = 0; d < kDim; ++d) acc += row[d] * q[d];
out[g] = acc;
}
}
return host_sims_.data();
}
private:
int n_gallery_{0};
int max_faces_{0};
std::vector<float> gallery_; // n_gallery × 512, row-major
std::vector<float> host_sims_; // max_faces × n_gallery, column-major
};
} // namespace
std::unique_ptr<ISimilarityEngine> make_similarity_engine(
const float* gallery_row_major, int n_gallery, int max_faces) {
return std::make_unique<SimilarityEngine>(gallery_row_major, n_gallery, max_faces);
}
#else // GPU backends (CUDA / ROCM)
struct GpuError : std::runtime_error {
using std::runtime_error::runtime_error;
};
@@ -102,8 +164,6 @@ inline const char* backend_name() { return "rocBLAS/HIP"; }
#endif
constexpr int kDim = 512;
class SimilarityEngine final : public ISimilarityEngine {
public:
SimilarityEngine(const float* gallery_row_major, int n_gallery, int max_faces)
@@ -175,3 +235,5 @@ std::unique_ptr<ISimilarityEngine> make_similarity_engine(
const float* gallery_row_major, int n_gallery, int max_faces) {
return std::make_unique<SimilarityEngine>(gallery_row_major, n_gallery, max_faces);
}
#endif // SAE_GEMM_CPU / GPU backends