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
This commit is contained in:
@@ -34,9 +34,23 @@ constexpr int kDim = 512;
|
||||
|
||||
#if defined(SAE_GEMM_CPU)
|
||||
|
||||
#if defined(SAE_GEMM_CBLAS)
|
||||
#include <cblas.h>
|
||||
#endif
|
||||
|
||||
// ── 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
|
||||
// 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 {
|
||||
@@ -47,7 +61,13 @@ public:
|
||||
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 ("
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -58,7 +78,18 @@ public:
|
||||
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]).
|
||||
// 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_;
|
||||
@@ -69,6 +100,7 @@ public:
|
||||
out[g] = acc;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return host_sims_.data();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user