perf(postprocess): pick selected rows without materialising repeated index tensors - #1268
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. ❌ Your project check has failed because the head coverage (84%) is below the target coverage (95%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## develop #1268 +/- ##
========================================
+ Coverage 82% 84% +1%
========================================
Files 108 108
Lines 13500 13500
========================================
+ Hits 11121 11314 +193
+ Misses 2379 2186 -193 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟢 Ready to approve
The changes are localized, preserve semantics, and are backed by targeted tests covering the key equivalence contract (including duplicated query selections).
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR optimizes PostProcess query selection by avoiding materializing large repeated index tensors during gathers, reducing per-image allocations and improving CPU postprocessing latency (especially for native-resolution mask outputs).
Changes:
- Replace
repeat()-materialized gather indices withexpand()(stride-0 view) for box selection. - Replace
torch.gather(...repeat(...))withindex_selectfor per-query row selection in masks and keypoints. - Add regression tests ensuring duplicated/out-of-order query indices reproduce source rows verbatim across masks, keypoints, and the ONNX benchmark
post_processcopy.
File summaries
| File | Description |
|---|---|
src/rfdetr/models/postprocess.py |
Eliminates large repeated index tensors in box/mask/keypoint selection (expand + index_select). |
src/rfdetr/export/benchmark.py |
Mirrors the box selection optimization in the ONNX benchmark postprocess. |
tests/models/test_postprocess.py |
Adds coverage for duplicated top-k query indices on the mask native-resolution path. |
tests/models/test_postprocess_keypoints.py |
Adds coverage for duplicated/out-of-order query indices in keypoint gathering. |
tests/inference/test_trt_inference.py |
Adds coverage that the benchmark post_process repeats boxes correctly for duplicated top-k queries. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Close test-coverage findings from the PR 1268 review. Top-k over flattened [Q, C] scores can pick the same query under two classes, so the box/mask/keypoint selection must copy the source row verbatim for every duplicated or out-of-order index; the shipped paths lacked that pin (only the TRT-benchmark box twin had one). * test(postprocess): dupe/out-of-order box pin on the production _gather_and_scale_boxes path (finding: shipped box path untested, only the benchmark.py twin was) * test(postprocess): mask dupe through the upsample=True branch, straddling _MASK_CHUNK (finding: mask dupe test covered only upsample=False) * test(postprocess): keypoint dupe/out-of-order through the class-filtering decode path (finding: keypoint dupe coverage stopped at the raw gather helper) * test(postprocess): CUDA-parity gpu-marked box equivalence (finding: CUDA parity never directly executed) --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
…index tensors (#1268) * perf: pick selected rows without materialising repeated index tensors * test: pin dupe-index selection paths * test: dupe/out-of-order box pin on the production _gather_and_scale_boxes path (finding: shipped box path untested, only the benchmark.py twin was) * test: mask dupe through the upsample=True branch, straddling _MASK_CHUNK (finding: mask dupe test covered only upsample=False) * test: keypoint dupe/out-of-order through the class-filtering decode path (finding: keypoint dupe coverage stopped at the raw gather helper) * test: CUDA-parity gpu-marked box equivalence (finding: CUDA parity never directly executed) --------- Co-authored-by: Jesús Royeth <JESUSROYETH@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: jirka <6035284+Borda@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
What does this PR do?
The three query selections in
PostProcessbuild their gather index withrepeat(), so the index tensor gets materialised at the full size of the data it selects. For masks this is an int64 tensor of shape[K, Hm, Wm]— atnum_select=300that is 21 MiB per image forrfdetr-seg-small(96×96 mask head) and 84 MiB forrfdetr-seg-2xlarge(192×192), allocated and filled on every image just to pick 300 rows.index_selectpicks the same whole rows without building any index tensor, andexpandon the box path replaces the materialised[B, K, 4]index with a stride-0 view. The output does not change — bit for bit (see below).The ONNX benchmark keeps its own copy of the box selection (
post_processinsrc/rfdetr/export/benchmark.py) with the samerepeat(), so it gets the same one-line change.Measured on CPU (torch 2.13, single thread, pinned to one core, median of interleaved A/B runs of the full
PostProcess.forward):A few notes on where this matters:
upsample_masks_to_image_size=False, theeval_masks_head_resolutioncost lever), where the gather is most of the postprocess. That path runs with K=300 on every validation image.score_threshold(perf(inference): skip upsampling masks the caller's threshold discards #1265) K is small and everything is already cheap, nothing changes there.src/rfdetr/models/transformer.py(L381, L389) builds its gather indices with the samerepeat()pattern. I left it alone on purpose: it sits in the model forward under autograd, and its indices top out at[B, K, d_model]— a fraction of the mask case. Happy to cover it here or in a follow-up if you want it.Related Issue(s): none, follow-up of #1265 on the same function.
Type of Change
Testing
forward()compared over the three heads (masks / keypoints / boxes) on CPU and CUDA — 198 output tensors compared byte for byte, including duplicated top-k query indices,Q < num_select, a threshold that keeps zero rows, and bothupsample_masks_to_image_sizebranches. All equal.test_duplicate_query_selection_repeats_the_same_mask_rows,test_gather_keypoints_for_queries_repeats_duplicated_indices,test_post_process_repeats_boxes_for_duplicated_topk_queriesfor the benchmark copy). All pass before and after the change — they test the equivalence, not the implementation.post_processbit-compared (int32 view) over 50 randomised shapes, includingkclamped below and aboveQ*C. All equal.tests/modelsplustests/inference/test_trt_inference.pypass with the patch (643 passed, 5 skipped) andruff check/ruff formatare clean on the touched files .. let me know if you want numbers for any other case.