Files
scene-actor-extraction/src/backends/gemm_backend.cpp
T
dtourolleandClaude Opus 5 cc1bed92d8 perf: back the CPU similarity GEMM with OpenBLAS
The CPU path was a scalar triple loop. It is the correctness oracle for the GPU
backends, but it is also what CI runs — there is no GPU on the N100 host — and
since AR-003 removed the per-frame face cap, a crowded frame now scores many
faces against a library-scale gallery. Scoring one face against 5000 embeddings
is 2.6 MFLOP; in scalar that does not hold up (AR-027).

S(g,f) viewed as row-major [n_faces x n_gallery] is exactly query * gallery^T,
so the loop nest collapses into a single cblas_sgemm.

OpenBLAS is optional in the build: found via pkg-config, and the scalar path
remains when it is absent so no hard dependency is added and the two can be
diffed when a similarity looks wrong. The configure step warns rather than
failing, since a developer without it should still get a working tree.

The test target links it too. Without that the suite compiles the scalar
fallback while the builder image ships CBLAS, so CI would be verifying a kernel
that is not the one running in production — the same class of mistake as testing
a path the gate never executes.

Recorded as required (not optional) in the DP-007 image, for the same reason.

Suite: 92 cases, 6136 assertions, with CBLAS compiled in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: AR-026, AR-027, DP-007 | SR-001
2026-07-31 15:04:29 +02:00

272 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── Gallery GEMM backend ──────────────────────────────────────────────────────
// 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"
#if defined(SAE_GEMM_CUDA)
#include <cublas_v2.h>
#include <cuda_runtime_api.h>
#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, SAE_GEMM_ROCM or SAE_GEMM_CPU to be defined"
#endif
#include <cstring>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
constexpr int kDim = 512;
#if defined(SAE_GEMM_CPU)
#if defined(SAE_GEMM_CBLAS)
#include <cblas.h>
#endif
// ── CPU reference engine ──────────────────────────────────────────────────────
// Used for CI and as the correctness oracle for the GPU backends.
//
// TRACES: AR-026, AR-027 | SR-001
// Backed by CBLAS (OpenBLAS) when available, falling back to a scalar loop when
// not. The fallback is portable but scales badly: scoring one face against a
// 5000-embedding gallery is 2.6 MFLOP, and a crowded frame multiplies that by
// the face count. Since AR-003 removed the per-frame face cap and CI has no GPU,
// the CPU path is now the one that has to hold up under a library-scale gallery
// (AR-027) rather than merely be correct.
//
// The fallback is kept rather than made mandatory so the build has no hard new
// dependency, and so the two can be diffed when a similarity looks wrong. 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 engine ("
#if defined(SAE_GEMM_CBLAS)
<< "CBLAS"
#else
<< "scalar fallback — no CBLAS; expect poor scaling on a large gallery"
#endif
<< "): 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]). Viewed as row-major
// [n_faces x n_gallery] that is exactly query * gallery^T, so it is one
// GEMM rather than a loop nest.
#if defined(SAE_GEMM_CBLAS)
cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans,
/*M=*/n_faces, /*N=*/n_gallery_, /*K=*/kDim,
/*alpha=*/1.0f,
query_row_major, /*lda=*/kDim,
gallery_.data(), /*ldb=*/kDim,
/*beta=*/0.0f,
host_sims_.data(), /*ldc=*/n_gallery_);
#else
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;
}
}
#endif
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;
};
#if defined(SAE_GEMM_CUDA)
using stream_t = cudaStream_t;
using blas_handle_t = cublasHandle_t;
inline void check_gpu(cudaError_t e, const char* what) {
if (e != cudaSuccess)
throw GpuError(std::string(what) + ": " + cudaGetErrorString(e));
}
inline void check_blas(cublasStatus_t s, const char* what) {
if (s != CUBLAS_STATUS_SUCCESS)
throw GpuError(std::string(what) + ": cublas error " + std::to_string(s));
}
inline void gpu_malloc(void** p, size_t bytes) { check_gpu(cudaMalloc(p, bytes), "cudaMalloc"); }
inline void gpu_free(void* p) { cudaFree(p); }
inline void gpu_memcpy_h2d(void* dst, const void* src, size_t n, stream_t s) { check_gpu(cudaMemcpyAsync(dst, src, n, cudaMemcpyHostToDevice, s), "H2D"); }
inline void gpu_memcpy_d2h(void* dst, const void* src, size_t n, stream_t s) { check_gpu(cudaMemcpyAsync(dst, src, n, cudaMemcpyDeviceToHost, s), "D2H"); }
inline void gpu_memcpy_h2d_sync(void* dst, const void* src, size_t n) { check_gpu(cudaMemcpy(dst, src, n, cudaMemcpyHostToDevice), "H2D_sync"); }
inline void stream_create(stream_t* s) { check_gpu(cudaStreamCreate(s), "cudaStreamCreate"); }
inline void stream_destroy(stream_t s) { cudaStreamDestroy(s); }
inline void stream_sync(stream_t s) { check_gpu(cudaStreamSynchronize(s), "cudaStreamSync"); }
inline void blas_create(blas_handle_t* h) { check_blas(cublasCreate(h), "cublasCreate"); }
inline void blas_destroy(blas_handle_t h) { cublasDestroy(h); }
inline void blas_set_stream(blas_handle_t h, stream_t s) { check_blas(cublasSetStream(h, s), "cublasSetStream"); }
inline void blas_sgemm(blas_handle_t h, int m, int n, int k,
const float* A, const float* B, float* C) {
const float alpha = 1.f, beta = 0.f;
check_blas(cublasSgemm(h, CUBLAS_OP_T, CUBLAS_OP_N,
m, n, k, &alpha, A, k, B, k, &beta, C, m),
"cublasSgemm");
}
inline const char* backend_name() { return "cuBLAS/CUDA"; }
#else // SAE_GEMM_ROCM
using stream_t = hipStream_t;
using blas_handle_t = rocblas_handle;
inline void check_gpu(hipError_t e, const char* what) {
if (e != hipSuccess)
throw GpuError(std::string(what) + ": " + hipGetErrorString(e));
}
inline void check_blas(rocblas_status s, const char* what) {
if (s != rocblas_status_success)
throw GpuError(std::string(what) + ": rocblas error " + std::to_string(s));
}
inline void gpu_malloc(void** p, size_t bytes) { check_gpu(hipMalloc(p, bytes), "hipMalloc"); }
inline void gpu_free(void* p) { (void)hipFree(p); }
inline void gpu_memcpy_h2d(void* dst, const void* src, size_t n, stream_t s) { check_gpu(hipMemcpyAsync(dst, src, n, hipMemcpyHostToDevice, s), "H2D"); }
inline void gpu_memcpy_d2h(void* dst, const void* src, size_t n, stream_t s) { check_gpu(hipMemcpyAsync(dst, src, n, hipMemcpyDeviceToHost, s), "D2H"); }
inline void gpu_memcpy_h2d_sync(void* dst, const void* src, size_t n) { check_gpu(hipMemcpy(dst, src, n, hipMemcpyHostToDevice), "H2D_sync"); }
inline void stream_create(stream_t* s) { check_gpu(hipStreamCreate(s), "hipStreamCreate"); }
inline void stream_destroy(stream_t s) { (void)hipStreamDestroy(s); }
inline void stream_sync(stream_t s) { check_gpu(hipStreamSynchronize(s), "hipStreamSync"); }
inline void blas_create(blas_handle_t* h) { check_blas(rocblas_create_handle(h), "rocblas_create_handle"); }
inline void blas_destroy(blas_handle_t h) { rocblas_destroy_handle(h); }
inline void blas_set_stream(blas_handle_t h, stream_t s) { check_blas(rocblas_set_stream(h, s), "rocblas_set_stream"); }
inline void blas_sgemm(blas_handle_t h, int m, int n, int k,
const float* A, const float* B, float* C) {
const float alpha = 1.f, beta = 0.f;
// rocblas_sgemm is column-major; same transposition trick as cuBLAS:
// C(m×n) = A(k×m)^T * B(k×n) → S(N_gallery × n_faces) = G^T * Q
check_blas(rocblas_sgemm(h, rocblas_operation_transpose, rocblas_operation_none,
m, n, k, &alpha, A, k, B, k, &beta, C, m),
"rocblas_sgemm");
}
inline const char* backend_name() { return "rocBLAS/HIP"; }
#endif
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)
{
const size_t gallery_floats = static_cast<size_t>(n_gallery_) * kDim;
gpu_malloc(reinterpret_cast<void**>(&d_gallery_), gallery_floats * sizeof(float));
gpu_memcpy_h2d_sync(d_gallery_, gallery_row_major, gallery_floats * sizeof(float));
gpu_malloc(reinterpret_cast<void**>(&d_query_),
static_cast<size_t>(max_faces_) * kDim * sizeof(float));
gpu_malloc(reinterpret_cast<void**>(&d_sims_),
static_cast<size_t>(max_faces_) * n_gallery_ * sizeof(float));
stream_create(&stream_);
blas_create(&handle_);
blas_set_stream(handle_, stream_);
host_sims_.resize(static_cast<size_t>(max_faces_) * n_gallery_);
std::cerr << "[similarity] " << backend_name() << " engine: gallery resident on GPU ("
<< (gallery_floats * sizeof(float)) / (1024 * 1024) << " MiB)\n";
}
~SimilarityEngine() override {
if (d_gallery_) gpu_free(d_gallery_);
if (d_query_) gpu_free(d_query_);
if (d_sims_) gpu_free(d_sims_);
if (handle_) blas_destroy(handle_);
if (stream_) stream_destroy(stream_);
}
SimilarityEngine(const SimilarityEngine&) = delete;
SimilarityEngine& operator=(const SimilarityEngine&) = delete;
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");
gpu_memcpy_h2d(d_query_, query_row_major,
static_cast<size_t>(n_faces) * kDim * sizeof(float), stream_);
// S (N_gallery × n_faces) col-major = G(512 × N_gallery)^T * Q(512 × n_faces)
blas_sgemm(handle_, n_gallery_, n_faces, kDim, d_gallery_, d_query_, d_sims_);
gpu_memcpy_d2h(host_sims_.data(), d_sims_,
static_cast<size_t>(n_gallery_) * n_faces * sizeof(float), stream_);
stream_sync(stream_);
return host_sims_.data();
}
private:
int n_gallery_{0};
int max_faces_{0};
float* d_gallery_{nullptr};
float* d_query_{nullptr};
float* d_sims_{nullptr};
std::vector<float> host_sims_;
stream_t stream_{};
blas_handle_t handle_{};
};
} // 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);
}
#endif // SAE_GEMM_CPU / GPU backends