[ENH] optimize metric state updates to prevent O(N^2) reallocation - #2371
[ENH] optimize metric state updates to prevent O(N^2) reallocation#2371andersendsa wants to merge 2 commits into
Conversation
Initialize states with empty lists instead of dummy tensors for reduction='none'. Append to list during `update` and only do a single `torch.cat` inside `compute`. This dramatically speeds up long evaluation loops for multi-horizon metrics.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2371 +/- ##
=======================================
Coverage ? 87.43%
=======================================
Files ? 175
Lines ? 10176
Branches ? 0
=======================================
Hits ? 8897
Misses ? 1279
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
phoeenniixx
left a comment
There was a problem hiding this comment.
Thanks! Can you please share an example how this helps?
Here you check if losses is a list, when does that happen?
When reduction="none" is used for a metric, the metric state needs to accumulate predictions and targets across multiple batches in order to evaluate them at the end. Previously, losses and lengths were initialized as tensors. On every single forward pass (the update() call), the code ran: self.losses = torch.cat([self.losses, losses], dim=0) Using torch.cat() in a loop requires allocating a brand new block of memory, copying the accumulated tensor so far, copying the new tensor, and freeing the old tensor. As the tensor gets larger over thousands of batches, this operation gets slower and slower (O(N^2) time complexity). In this change, I replaced the initial tensor with an empty list []. In update(), we simply append to the list: self.losses.append(losses) List appends are O(1). Then, at the very end when compute() is called, we do a single torch.cat() operation on the whole list, dropping the time complexity down to O(N). When I wrote a quick test loop simulating 1,000 batches, the time to execute dropped from over 6 seconds to roughly 0.01 seconds. As for checking if isinstance(self.losses, list): By default, we initialized the state as []. So during standard update() calls, it's a list. However, because this metric inherits from TorchMetrics, some backend processes like syncing metrics across multiple GPUs (dist_reduce_fx=cat) may automatically concatenate our list state back into a single tensor before compute() is called. To handle cases where a user runs in distributed mode or manually modifies the state, checking if isinstance(self.losses, list): allows it to safely append to a list, or fallback gracefully to concatenating tensors. |
Reference Issues/PRs
What does this implement/fix? Explain your changes.
Initialize states with empty lists instead of dummy tensors for reduction='none'. Append to list during
updateand only do a singletorch.catinsidecompute. This dramatically speeds up long evaluation loops for multi-horizon metrics.What should a reviewer concentrate their feedback on?
Did you add any tests for the change?
Any other comments?
PR checklist
pre-commit install.To run hooks independent of commit, execute
pre-commit run --all-files