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,7 +34,20 @@ target_include_directories(sae_tests PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||
# SAE_MODELS_DIR: config.hpp (pulled in by track_gallery.hpp) bakes model paths.
|
||||
# SAE_TEST_FIXTURES_DIR: the audio golden vector is read from the source tree,
|
||||
# not copied, so the file the plugin repo shares is the file under test.
|
||||
# AR-026/AR-027: exercise the same kernel CI actually runs. Without this the
|
||||
# suite compiles the scalar fallback while the CPU builder image links OpenBLAS,
|
||||
# so the tested path and the shipped path would differ.
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(OPENBLAS_T QUIET openblas)
|
||||
endif()
|
||||
if(OPENBLAS_T_FOUND)
|
||||
target_include_directories(sae_tests PRIVATE ${OPENBLAS_T_INCLUDE_DIRS})
|
||||
target_link_libraries(sae_tests PRIVATE ${OPENBLAS_T_LINK_LIBRARIES})
|
||||
endif()
|
||||
|
||||
target_compile_definitions(sae_tests PRIVATE
|
||||
$<$<BOOL:${OPENBLAS_T_FOUND}>:SAE_GEMM_CBLAS>
|
||||
SAE_GEMM_CPU
|
||||
SAE_MODELS_DIR="${SAE_MODELS_DIR}"
|
||||
SAE_TEST_FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures")
|
||||
|
||||
@@ -74,3 +74,106 @@ TEST_CASE("align_face returns empty on degenerate (collinear) landmarks", "[face
|
||||
cv::Mat crop = align_face(img, lm);
|
||||
CHECK(crop.empty());
|
||||
}
|
||||
|
||||
// ── AR-030: the alignment residual as a visibility measure ────────────────────
|
||||
// These assert the *properties* the measure is relied on for, not a magic value.
|
||||
// Each would fail under a RANSAC fit, which buys a small residual by discarding
|
||||
// the very landmarks that carry the signal.
|
||||
|
||||
namespace {
|
||||
|
||||
std::array<cv::Point2f, 5> canonical() {
|
||||
std::array<cv::Point2f, 5> lm;
|
||||
for (int i = 0; i < 5; ++i) lm[i] = {kArcFaceRef[i][0], kArcFaceRef[i][1]};
|
||||
return lm;
|
||||
}
|
||||
|
||||
// Rotate by `deg` in-plane, scale uniformly, translate — i.e. exactly the 4 DoF
|
||||
// the similarity transform models.
|
||||
std::array<cv::Point2f, 5> similarity(const std::array<cv::Point2f, 5>& in,
|
||||
float deg, float s, float tx, float ty) {
|
||||
const float r = deg * 3.14159265358979f / 180.f;
|
||||
const float c = std::cos(r), sn = std::sin(r);
|
||||
std::array<cv::Point2f, 5> out;
|
||||
for (int i = 0; i < 5; ++i)
|
||||
out[i] = {s * (c * in[i].x - sn * in[i].y) + tx,
|
||||
s * (sn * in[i].x + c * in[i].y) + ty};
|
||||
return out;
|
||||
}
|
||||
|
||||
// Squash x about the centroid by `k`: the anisotropic deformation an out-of-plane
|
||||
// yaw produces, and the one a similarity provably cannot absorb.
|
||||
std::array<cv::Point2f, 5> foreshorten(const std::array<cv::Point2f, 5>& in, float k) {
|
||||
float cx = 0.f;
|
||||
for (const auto& p : in) cx += p.x;
|
||||
cx /= 5.f;
|
||||
std::array<cv::Point2f, 5> out = in;
|
||||
for (auto& p : out) p.x = cx + (p.x - cx) * k;
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("residual is zero for a face in canonical pose", "[face_utils][AR-030]") {
|
||||
const Alignment a = estimate_alignment(canonical());
|
||||
REQUIRE(a.ok);
|
||||
CHECK_THAT(a.residual, WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("residual ignores in-plane roll, scale and translation", "[face_utils][AR-030]") {
|
||||
// The structural claim behind AR-030: the fit absorbs all four similarity
|
||||
// DoF exactly, so what remains is only the deformation a similarity cannot
|
||||
// explain. A rolled head must not read as a turned one.
|
||||
for (float deg : {-40.f, -12.f, 0.f, 17.f, 65.f}) {
|
||||
const Alignment a = estimate_alignment(similarity(canonical(), deg, 3.5f, 220.f, -40.f));
|
||||
REQUIRE(a.ok);
|
||||
CHECK_THAT(a.residual, WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("residual rises monotonically with foreshortening", "[face_utils][AR-030]") {
|
||||
float prev = -1.f;
|
||||
for (float k : {1.0f, 0.9f, 0.75f, 0.5f, 0.3f}) {
|
||||
const Alignment a = estimate_alignment(foreshorten(canonical(), k));
|
||||
REQUIRE(a.ok);
|
||||
CHECK(a.residual > prev);
|
||||
prev = a.residual;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("residual is independent of face size", "[face_utils][AR-030]") {
|
||||
// The measure must not silently re-express face size — that is AR-002's job,
|
||||
// and double-counting it would make a small frontal face look occluded.
|
||||
// Same deformation, two very different face sizes, one answer.
|
||||
const auto small = similarity(foreshorten(canonical(), 0.7f), 20.f, 1.0f, 0.f, 0.f);
|
||||
const auto large = similarity(foreshorten(canonical(), 0.7f), 20.f, 12.0f, 500.f, 300.f);
|
||||
|
||||
const Alignment a = estimate_alignment(small);
|
||||
const Alignment b = estimate_alignment(large);
|
||||
REQUIRE(a.ok);
|
||||
REQUIRE(b.ok);
|
||||
CHECK_THAT(b.residual, WithinAbs(a.residual, 1e-2f));
|
||||
}
|
||||
|
||||
TEST_CASE("the fit never mirrors the face", "[face_utils][AR-030]") {
|
||||
// SVD will happily return an orientation-reversing solution; a similarity
|
||||
// transform may rotate but never reflect. Without the determinant guard a
|
||||
// mirrored landmark set fits "perfectly" as a reflection.
|
||||
const auto mirrored = foreshorten(canonical(), -1.f);
|
||||
const Alignment a = estimate_alignment(mirrored);
|
||||
REQUIRE(a.ok);
|
||||
|
||||
const double det = a.M.at<double>(0,0) * a.M.at<double>(1,1)
|
||||
- a.M.at<double>(0,1) * a.M.at<double>(1,0);
|
||||
CHECK(det > 0.0);
|
||||
CHECK(a.residual > 1.0f); // and the mirroring shows up as misfit
|
||||
}
|
||||
|
||||
TEST_CASE("degenerate landmarks report not-ok rather than a residual", "[face_utils][AR-030]") {
|
||||
std::array<cv::Point2f, 5> lm;
|
||||
for (auto& p : lm) p = {50.f, 50.f};
|
||||
|
||||
const Alignment a = estimate_alignment(lm);
|
||||
CHECK_FALSE(a.ok);
|
||||
CHECK(a.M.empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user