feat(gemm): the annex is a matrix, not a list — scored by the same GEMM
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
This commit is contained in:
@@ -39,6 +39,13 @@
|
||||
// uploaded and a single SGEMM computes the full similarity matrix in well under
|
||||
// a millisecond. The GPU math backend (cuBLAS or rocBLAS) lives behind
|
||||
// ISimilarityEngine (backends/gemm_backend.cpp) and is selected at compile time.
|
||||
//
|
||||
// TRACES: AR-026 | SR-001
|
||||
// That resident matrix grows during a film: per-film expansion (AR-019) promotes
|
||||
// pose-varied views, and they are APPENDED to it rather than scored separately,
|
||||
// so one multiply covers baked and promoted references alike and best-of-N is a
|
||||
// single pass over one similarity column. There is no second similarity path in
|
||||
// this node to fall out of step with the first.
|
||||
|
||||
struct IdentityMatcherFunc {
|
||||
static constexpr std::string_view label() { return "identity_matcher"; }
|
||||
@@ -183,29 +190,30 @@ struct IdentityMatcherFunc {
|
||||
tf.embeddings[base + k].data(), 512 * sizeof(float));
|
||||
}
|
||||
|
||||
// S (N_gallery × chunk) col-major: face k's gallery sims at sims + k*n_gallery.
|
||||
/// TRACES: AR-026 | SR-001
|
||||
// One GEMM now covers baked references AND the per-film annex: promoted
|
||||
// rows were appended to the engine's resident matrix, so they are just
|
||||
// more gallery rows with an entry in flat_actor_. The annex used to be
|
||||
// folded in afterwards by a host-side cosine loop, justified by "tens of
|
||||
// embeddings" — an assumption AR-018/AR-019 retired, since every owned
|
||||
// track promotes and the annex grows with cast size and film length.
|
||||
//
|
||||
// n_gallery() is read per frame, not cached: it grows as promotions land.
|
||||
const int n_gal = sim_engine_->n_gallery();
|
||||
const float* host_sims = sim_engine_->compute(host_query.data(), chunk);
|
||||
|
||||
for (int ci = 0; ci < chunk; ++ci) {
|
||||
const int fi = base + ci;
|
||||
const float* sims = host_sims + static_cast<size_t>(ci) * n_gallery_;
|
||||
const float* sims = host_sims + static_cast<size_t>(ci) * n_gal;
|
||||
|
||||
std::vector<float> best_sim(gallery_.actors.size(),
|
||||
-std::numeric_limits<float>::max());
|
||||
for (int ei = 0; ei < n_gallery_; ++ei) {
|
||||
for (int ei = 0; ei < n_gal; ++ei) {
|
||||
float sim = sims[ei];
|
||||
int ai = flat_actor_[ei];
|
||||
if (sim > best_sim[ai]) best_sim[ai] = sim;
|
||||
}
|
||||
|
||||
// Fold in the per-film annex (CPU-side, tens of embeddings). Promoted
|
||||
// pose-varied views compete for best-of-N exactly like baked refs, so
|
||||
// a face at a pose the gallery lacked can now win its true actor.
|
||||
for (const auto& ae : track_gallery_.annex()) {
|
||||
float sim = cosine_similarity(tf.embeddings[fi], ae.emb);
|
||||
if (sim > best_sim[ae.actor_idx]) best_sim[ae.actor_idx] = sim;
|
||||
}
|
||||
|
||||
int best_actor = -1;
|
||||
int second_actor = -1;
|
||||
float best_s = -std::numeric_limits<float>::max();
|
||||
@@ -299,10 +307,34 @@ struct IdentityMatcherFunc {
|
||||
}
|
||||
} // chunk loop
|
||||
|
||||
absorb_promotions();
|
||||
|
||||
return {std::move(tf.source), std::move(actors)};
|
||||
}
|
||||
|
||||
private:
|
||||
/// TRACES: AR-026 | SR-001
|
||||
/// Move rows promoted during this frame into the resident gallery matrix,
|
||||
/// extending the actor mapping in lockstep so row i keeps naming the actor
|
||||
/// at flat_actor_[i]. 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 is also the semantics the expansion store
|
||||
/// documents — a promotion helps SUBSEQUENT frames, never the one that
|
||||
/// produced it, so identification cannot depend on face order within a frame.
|
||||
void absorb_promotions() {
|
||||
if (!track_gallery_.enabled()) return;
|
||||
|
||||
pending_emb_.clear();
|
||||
pending_actor_.clear();
|
||||
const int n = track_gallery_.drain_promotions(pending_emb_, pending_actor_);
|
||||
if (n == 0) return;
|
||||
|
||||
sim_engine_->append_rows(pending_emb_.data(), n);
|
||||
flat_actor_.insert(flat_actor_.end(),
|
||||
pending_actor_.begin(), pending_actor_.end());
|
||||
n_gallery_ = sim_engine_->n_gallery();
|
||||
}
|
||||
|
||||
ActorGallery gallery_;
|
||||
GalleryCalibration cal_;
|
||||
float prob_threshold_;
|
||||
@@ -310,10 +342,18 @@ private:
|
||||
float threshold_;
|
||||
float ratio_;
|
||||
float ratio_ceil_;
|
||||
/// flat_emb_ is the BAKED reference set only — it is the calibration fit's
|
||||
/// input (AR-023) and is not touched again after construction. flat_actor_,
|
||||
/// by contrast, is the actor mapping parallel to the *engine's* rows, so it
|
||||
/// grows with every promotion absorbed (AR-026) and is the longer of the two.
|
||||
std::vector<Embedding> flat_emb_;
|
||||
std::vector<int> flat_actor_;
|
||||
int n_gallery_{0};
|
||||
|
||||
// Reused across frames so absorbing a promotion allocates nothing.
|
||||
std::vector<float> pending_emb_;
|
||||
std::vector<int> pending_actor_;
|
||||
|
||||
std::unique_ptr<ISimilarityEngine> sim_engine_;
|
||||
TrackGallery track_gallery_;
|
||||
std::shared_ptr<TrackRegistry> registry_;
|
||||
|
||||
Reference in New Issue
Block a user