fix(AR-013): reap tracks on the evidence watermark, not the tracker's clock
The registry closed a track when the *tracker's* timestamp passed `track_extinction_sec`. But votes arrive from the matcher, which is a separate KPN node behind a channel, and much the slower of the pair. Backpressure — working exactly as AR-004 intends — turns that channel's depth into lag, so the tracker's clock can be far ahead of the last frame anybody has voted on. Tracks were therefore closed before their evidence arrived: the votes landed on ids that no longer existed, were counted as dropped, and the track was emitted unowned or not at all. The symptom is the part worth remembering: **a deeper channel produced fewer identifications, from identical input.** On the SuperHero fixture, 5 actors / 16 windows at depth 32 against 3 actors / 5 windows at depth 10322; through the replay harness, capacity 32 gave 5 actors and 10322 gave 0. A throughput knob was silently changing the answer, which makes every sweep tuned against it suspect. The fix is not to bound the channel against `track_extinction_sec` — that makes an algorithm constant police a throughput knob and leaves the result a function of scheduling. It is to reap on an evidence watermark: the matcher advances it as it folds each frame in, and a track is only finished once everything up to its extinction point has actually been voted on. Same device `SceneBoundaries::scored_through()` uses for the AR-010 join — a consumer past that point is asking about frames nobody has looked at yet, and the honest answer is to wait rather than guess. Association keeps the tracker's clock, and separating the two is the other half. They answer different questions: "may this detection link to that track?" is asked now, about a box seen `track_extinction_sec` ago; "is that track finished?" cannot be answered until every vote is in. Deferring association to the evidence clock — which deferring the erase alone did — left retired tracks associable for as long as the matcher lagged, so a new face re-associated onto a long-dead track and two people merged into one window. The watermark is monotonic and only ever *delays* a reap, so no window is extended by it: AR-013's "a window ends at the last sighting, never after" is a property of `emit_locked`, which takes `last_seen` and never `now`. `dropped_votes` is exposed and reported — by main at shutdown and through the replay bindings — because this failed silently for as long as it did precisely because nothing counted it. It warns rather than aborts: a dropped frame means the output describes footage nobody analysed and is always wrong, while a dropped vote degrades a claim without falsifying it, and there is no measurement yet of how often it happens on real content. replay.py's channel capacity stops being the whole film. It was sized that way to dodge a PyNode overflow drop that AR-004 has since replaced with parking, and removing backpressure that way is what made the defect above so extreme. Tag separators in kpn_bindings.cpp corrected to pipes between requirement types, which the traceability gate was reporting as diagnostics; the matrix is regenerated and reports 0 orphan tags. 149/149. TRACES: AR-004, AR-012, AR-013, AR-025 | VR-011 | SR-002 | PR-002
This commit is contained in:
+28
-1
@@ -258,7 +258,7 @@ static Config config_from_dict(nb::dict d) {
|
||||
if (d.contains("require_gallery_stamp"))
|
||||
cfg.require_gallery_stamp = nb::cast<bool>(d["require_gallery_stamp"]);
|
||||
|
||||
/// TRACES: VR-011, IR-001 | PR-002, SR-003
|
||||
/// TRACES: VR-011 | IR-001 | PR-002 | SR-003
|
||||
// The sink is a real node in this network now, so it needs the two things
|
||||
// that decide what it writes and where. Both used to be irrelevant here
|
||||
// because the replay never had a sink -- Python rebuilt presence instead,
|
||||
@@ -429,6 +429,33 @@ NB_MODULE(sae_kpn, m) {
|
||||
/// replay, which a long sweep will notice.
|
||||
m.def("release_pipeline", [](Net& net) { sessions().erase(&net); }, "net"_a);
|
||||
|
||||
/// TRACES: VR-011 | AR-025 | PR-002
|
||||
/// The registry's own count of how often it was wrong, exposed so a replay
|
||||
/// can fail on it instead of returning a plausible-looking empty answer.
|
||||
///
|
||||
/// `dropped_votes` is the one that matters here and it earned its keep
|
||||
/// immediately. A vote lands on a track the registry has already reaped when
|
||||
/// the matcher lags the tracker by more than track_extinction_sec of film.
|
||||
/// In scene_analyze that cannot happen -- channels are 16-64 deep, so
|
||||
/// backpressure pins the two nodes within a few frames of each other. This
|
||||
/// harness sized every channel to the whole film to avoid a PyNode overflow
|
||||
/// drop, which removed the backpressure entirely: the tracker ran the film
|
||||
/// to the end while the matcher was still in its first minute, every vote
|
||||
/// arrived after its track was gone, no track was ever owned, and the run
|
||||
/// produced zero presence windows while cheerfully reporting 1647 frames
|
||||
/// with an identified face.
|
||||
m.def("pipeline_diagnostics", [](Net& net) {
|
||||
nb::dict d;
|
||||
auto it = sessions().find(&net);
|
||||
if (it == sessions().end() || !it->second->registry) return d;
|
||||
const auto& r = *it->second->registry;
|
||||
d["dropped_votes"] = r.dropped_votes();
|
||||
d["belief_swaps"] = r.belief_swaps();
|
||||
d["actor_conflicts"] = r.actor_conflicts();
|
||||
d["live_tracks"] = static_cast<int>(r.live());
|
||||
return d;
|
||||
}, "net"_a);
|
||||
|
||||
/// True once the sink has written its output. The sink flushes on the EOF
|
||||
/// annotation, so a caller that reads the file before this is racing it.
|
||||
m.def("pipeline_done", [](Net& net) {
|
||||
|
||||
Reference in New Issue
Block a user