fix(byte_tracker): stop unbounded growth of removed_stracks - #15
Conversation
`ByteTracker::update()` folded every newly-removed track into a permanent
`removed_stracks` vector:
self.removed_stracks = Self::joint_stracks(&self.removed_stracks,
¤t_removed_stracks);
`removed_stracks` is only read once per frame, to drop just-removed tracks
from `lost_stracks`. Because track ids are monotonic and a removed track is
already absent from both `tracked_stracks` and `lost_stracks`, it can never
reappear there, so keeping the full history serves no purpose — a one-frame
window is sufficient.
Accumulating it meant `joint_stracks`/`sub_stracks` cloned an ever-growing
vector of `STrack` (each carrying Kalman state) on *every* frame. On a
long-running stream this leaks memory and makes the per-frame cost grow with
the total number of tracks ever seen (progressive FPS/RSS degradation).
Fix: keep only the tracks removed on the current frame. Tracking output is
unchanged (the rest of `update()` is untouched); added a regression test that
runs many spawn/despawn cycles and asserts `removed_stracks` stays bounded
while a persistent object keeps a stable track id. Full suite: 150 passed.
|
Sorry, I completely missed it! Thanks for the PR! I’ll review it tonight. |
|
Thank you for the detailed explanation and regression test. I compared this change against the original ByteTrack implementation, the Roboflow implementation, and the ByteTrack paper. Original ByteTrack implementationThe original Python implementation retains every removed track indefinitely: self.lost_stracks = sub_stracks(self.lost_stracks, self.removed_stracks)
self.removed_stracks.extend(removed_stracks)The current
Roboflow implementationRoboflow's ByteTrack implementation does not maintain a removed-track history. It keeps a single live track list and replaces it with the tracklets that still satisfy the lifecycle conditions: self.tracks = _get_alive_tracklets(...)Sources:
Roboflow differs from the original ByteTrack implementation in other lifecycle and association details, so it is not a drop-in behavioral reference. Nevertheless, its lifecycle management confirms that a permanent removed-track history is not required. ByteTrack paperI also checked Algorithm 1 and the track-rebirth explanation in the ByteTrack paper: https://arxiv.org/pdf/2110.06864 The paper describes the lifecycle as follows:
The paper does not define or require a permanent collection of removed tracks. Once a track exceeds the lost-track buffer and is removed from the active/lost track set, its state no longer needs to be retained. ConclusionThis is a valid minimal fix. Keeping only the latest removed tracks preserves the current behavior because their IDs are consumed by the next update, while preventing unbounded memory and cloning costs. All tests passed: 150 unit tests and 5 doc tests. One non-blocking suggestion: since Looks good to merge. Thank you! |
|
@GrumpyChubbyCat Thank you for your contributions again. I have released v0.5.1. Please check it! |
Dear Kadu,
Thank you for
jamtrack-rs! We build on the ByteTrack backend in a cross-hardware streaming/inference SDK of our own —jamtrack-rspowers its tracking path — and while running long-lived pipelines we hit a progressive slowdown that we traced to a single spot inByteTracker::update(). Our SDK isn't public yet — it will be open-sourced once it's ready — but we wanted to contribute the fix back upstream in the meantime.ByteTracker::update()folded every newly-removed track into a permanentremoved_stracksvector:removed_stracksis only read once per frame, to drop just-removed tracks fromlost_stracks. Because track ids are monotonic and a removed track is already absent from bothtracked_stracksandlost_stracks, it can never reappear there, so keeping the full history serves no purpose — a one-frame window is sufficient.Accumulating it meant
joint_stracks/sub_strackscloned an ever-growing vector ofSTrack(each carrying Kalman state) on every frame. On a long-running stream this leaks memory and makes the per-frame cost grow with the total number of tracks ever seen (progressive FPS/RSS degradation).Fix: keep only the tracks removed on the current frame. Tracking output is unchanged (the rest of
update()is untouched); added a regression test that runs many spawn/despawn cycles and assertsremoved_stracksstays bounded while a persistent object keeps a stable track id. Full suite: 150 passed.Happy to adjust anything to match your preferences — thanks again for the library!