The per-film annex was folded in after the gallery multiply by a host-side
cosine loop over a vector of {embedding, actor} structs, justified in-comment
by "tens of embeddings". AR-018/AR-019 retired that assumption: every owned
track promotes, so the annex grows with cast size and film length.
TrackGallery now holds it as a contiguous row-major matrix with a parallel
actor index — the flat_emb_/flat_actor_ shape the baked gallery already uses —
and hands newly promoted rows to the matcher once per frame. The matcher pushes
them into the similarity engine's resident matrix through a new
ISimilarityEngine::append_rows, so one SGEMM covers baked and promoted
references alike and best-of-N is a single pass over one similarity column.
Capacity doubles on overflow, and the GPU backends grow device-to-device, so a
promotion never re-uploads the gallery across the bus.
Absorbing promotions runs once per frame, after every face has been scored.
Appending mid-frame would invalidate the similarity pointer the chunk loop is
still reading, and it also removes an incidental dependence on face order
within a frame — a promotion helps subsequent frames, never the one that
produced it, which is the semantics the expansion store already documented.
OpenBLAS becomes a requirement of the CPU GEMM backend rather than an
opportunistic upgrade. That path is what CI and the cpu builder image run, so
falling back to the scalar loop in silence meant AR-027 could be measured — or
believed — on a kernel no release uses. The loop survives as the correctness
oracle the BLAS backends are diffed against, behind SAE_ALLOW_SCALAR_GEMM.
Call site 3, the deferred TBI pass, is untouched: it does not exist until
AR-020, so AR-026 stays In Progress.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TRACES: AR-026 | UT-004, UT-005 | SR-001
57 lines
3.0 KiB
C++
57 lines
3.0 KiB
C++
#pragma once
|
||
#include <memory>
|
||
#include <vector>
|
||
|
||
// ── ISimilarityEngine ─────────────────────────────────────────────────────────
|
||
// Backend-agnostic gallery similarity engine for the identity matcher.
|
||
//
|
||
// The full reference gallery (n_gallery × 512 L2-normalised embeddings) is
|
||
// uploaded to the GPU once at construction and stays resident. Per frame, the
|
||
// small query matrix (n_faces × 512) is uploaded and a single SGEMM produces the
|
||
// similarity matrix S (n_gallery × n_faces, column-major) — i.e. S[g + f*n_gal]
|
||
// is cosine_similarity(gallery[g], query[f]).
|
||
//
|
||
// The GPU math backend (cuBLAS/CUDA or rocBLAS/HIP) is selected at compile time
|
||
// by CMake (SAE_GEMM_BACKEND); backends/gemm_backend.cpp provides
|
||
// make_similarity_engine(). The core matcher node sees only this interface and
|
||
// holds no CUDA/HIP/BLAS headers.
|
||
//
|
||
// TRACES: AR-026 | SR-001
|
||
// The resident matrix GROWS. Per-film expansion (AR-018/AR-019) promotes new
|
||
// reference views mid-film, and those have to be scored by the same multiply as
|
||
// the baked references rather than by a side loop — "this set is small" is not
|
||
// an exception, because the annex grows with cast size and film length. Rows are
|
||
// therefore appended to the resident matrix and the next compute() covers baked
|
||
// and promoted references alike, in one GEMM. The deferred pass (AR-020) then
|
||
// inherits a single contiguous operand to score the TBI queue against.
|
||
|
||
struct ISimilarityEngine {
|
||
virtual ~ISimilarityEngine() = default;
|
||
|
||
// Largest n_faces accepted by compute() per call (bounds GPU buffer sizes).
|
||
virtual int max_faces() const = 0;
|
||
|
||
// Rows currently resident: the baked gallery plus every appended promotion.
|
||
// This is compute()'s column stride, and it changes as rows are appended —
|
||
// read it per call rather than caching it across frames.
|
||
virtual int n_gallery() const = 0;
|
||
|
||
/// TRACES: AR-026 | SR-001
|
||
/// Append n_rows unit-norm embeddings (row-major, 512 floats each) to the
|
||
/// resident matrix. Amortised O(1) per row: capacity grows geometrically, so
|
||
/// a promotion does not re-upload the gallery. Invalidates any pointer
|
||
/// previously returned by compute().
|
||
virtual void append_rows(const float* rows_row_major, int n_rows) = 0;
|
||
|
||
// Compute similarities for n_faces query embeddings.
|
||
// query_row_major: n_faces × 512, row fi at query + fi*512.
|
||
// Returns a pointer to host memory holding S column-major: the gallery
|
||
// similarities for face fi start at result + fi*n_gallery(). The pointer is
|
||
// owned by the engine and valid until the next compute() or append_rows().
|
||
virtual const float* compute(const float* query_row_major, int n_faces) = 0;
|
||
};
|
||
|
||
// gallery_row_major: n_gallery × 512, embedding i at gallery + i*512.
|
||
std::unique_ptr<ISimilarityEngine> make_similarity_engine(
|
||
const float* gallery_row_major, int n_gallery, int max_faces);
|