feat: no fixed cap on faces per frame
AR-003 — max_faces defaults to 0, meaning no cap. A fixed cap discards the SMALLEST faces first, which are exactly the background cast X-Ray still credits with scene membership, so the pipeline was systematically losing the people it is supposed to find in crowded scenes. This is only safe now that AR-004 landed. Previously an uncapped frame would have pushed more work into channels that dropped on overflow, trading a visible cap for silent loss. With backpressure the producer slows instead, so per-frame cost is contained rather than discarded. The matcher's kMaxFaces used to throw above 32, which made it an accidental second cap. It sizes the similarity engine's preallocated buffer, so it bounds memory rather than face count — the frame is now scored in batches of that size. Memory stays bounded; faces do not. Largest-first ordering is kept even without the cap, and the comment now says why: the Hungarian solver tie-breaks on index order, so that ordering is load-bearing for the replay determinism test rather than a leftover of the cap. Verified end to end on a real clip: identical output to the capped run (385 frames, 693 faces), which is expected since that footage peaks at 4 faces per frame — the point is the absence of a regression. The committed fixtures remain byte-identical and valid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-003 | SR-002
This commit is contained in:
@@ -145,20 +145,33 @@ struct IdentityMatcherFunc {
|
||||
actors.reserve(n_faces);
|
||||
|
||||
if (n_faces == 0) return {std::move(tf.source), {}};
|
||||
if (n_faces > kMaxFaces)
|
||||
throw std::runtime_error("identity_matcher: n_faces exceeds kMaxFaces");
|
||||
|
||||
std::vector<float> host_query(static_cast<size_t>(n_faces) * 512);
|
||||
for (int fi = 0; fi < n_faces; ++fi) {
|
||||
std::memcpy(host_query.data() + static_cast<size_t>(fi) * 512,
|
||||
tf.embeddings[fi].data(), 512 * sizeof(float));
|
||||
/// TRACES: AR-003, AR-004 | SR-002
|
||||
// kMaxFaces sizes the similarity engine's preallocated buffer, so it
|
||||
// bounds MEMORY, not how many faces a frame may contain. It used to
|
||||
// throw above the bound, which made it a hard cap on crowd scenes by
|
||||
// accident; now the frame is scored in batches of that size.
|
||||
//
|
||||
// Faces per frame are unbounded (AR-003) because X-Ray credits scene
|
||||
// membership to background cast too, and a fixed cap discards exactly
|
||||
// those — the smallest faces are dropped first. Cost is contained by
|
||||
// backpressure (AR-004), which slows the producer, rather than by
|
||||
// silently throwing work away.
|
||||
std::vector<float> host_query(static_cast<size_t>(kMaxFaces) * 512);
|
||||
|
||||
for (int base = 0; base < n_faces; base += kMaxFaces) {
|
||||
const int chunk = std::min(kMaxFaces, n_faces - base);
|
||||
for (int k = 0; k < chunk; ++k) {
|
||||
std::memcpy(host_query.data() + static_cast<size_t>(k) * 512,
|
||||
tf.embeddings[base + k].data(), 512 * sizeof(float));
|
||||
}
|
||||
|
||||
// S (N_gallery × n_faces) col-major: face fi's gallery sims at sims + fi*n_gallery.
|
||||
const float* host_sims = sim_engine_->compute(host_query.data(), n_faces);
|
||||
// S (N_gallery × chunk) col-major: face k's gallery sims at sims + k*n_gallery.
|
||||
const float* host_sims = sim_engine_->compute(host_query.data(), chunk);
|
||||
|
||||
for (int fi = 0; fi < n_faces; ++fi) {
|
||||
const float* sims = host_sims + static_cast<size_t>(fi) * n_gallery_;
|
||||
for (int ci = 0; ci < chunk; ++ci) {
|
||||
const int fi = base + ci;
|
||||
const float* sims = host_sims + static_cast<size_t>(ci) * n_gallery_;
|
||||
|
||||
std::vector<float> best_sim(gallery_.actors.size(),
|
||||
-std::numeric_limits<float>::max());
|
||||
@@ -257,6 +270,7 @@ struct IdentityMatcherFunc {
|
||||
|
||||
actors.push_back(std::move(ia));
|
||||
}
|
||||
} // chunk loop
|
||||
|
||||
return {std::move(tf.source), std::move(actors)};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user