perf(postprocess): read every image's mask target size once per batch - #1369
Merged
Borda merged 3 commits intoAug 18, 2026
Merged
Conversation
JESUSROYETH
requested review from
Borda,
SkalskiP,
isaacrob and
probicheaux
as code owners
August 18, 2026 18:51
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1369 +/- ##
=======================================
- Coverage 86% 85% -1%
=======================================
Files 111 111
Lines 14290 14292 +2
=======================================
- Hits 12261 12183 -78
- Misses 2029 2109 +80 🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
Optimizes CUDA mask postprocessing and COCO target conversion by consolidating per-image device-to-host reads into one per batch.
Changes:
- Batch-converts mask target sizes and COCO original sizes.
- Adds CPU, empty-batch, native-resolution, and CUDA regression tests.
- Documents the performance change.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/rfdetr/models/postprocess.py |
Reads all mask resize targets once per batch. |
src/rfdetr/training/callbacks/coco_eval.py |
Batches orig_size conversion during evaluation. |
tests/models/test_postprocess.py |
Tests synchronization-sensitive conversion behavior. |
tests/training/callbacks/test_coco_eval_callback.py |
Tests batched target-size reads. |
CHANGELOG.md |
Records the optimization. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Changes: - Add heterogeneous non-square mask target sizes to postprocess regression coverage, asserting each image keeps its own output shape and interpolated mask geometry after batched conversion. - Add heterogeneous COCO orig_size coverage, asserting per-image labels and exact absolute box coordinates remain paired with the correct target row. Impact: - Prevents future row-order and H/W-transposition regressions in the batched device-to-host metadata optimization. - Keeps the production implementation unchanged while making the synchronization optimization's positional contract executable. Verification: - uv run --no-sync pytest tests/models/test_postprocess.py tests/training/callbacks/test_coco_eval_callback.py -n 1 -m "not gpu" --timeout=240 — 152 passed. - uv run --no-sync ruff check ... — passed. - uv run --no-sync ruff format --check ... — passed. - uv run --no-sync mypy --strict src/rfdetr/models/postprocess.py src/rfdetr/training/callbacks/coco_eval.py — passed. - pre-commit run --all-files — passed. - git diff --check — passed. Residual limits: - CUDA tests were skipped on the CPU-only host. - Full CPU suite had six unrelated CoreML/ONNX/environment failures, including missing onnxscript. - Benchmark reproducibility and HIGH_RISK specialist provenance remain deferred in the remediation report. --- Co-authored-by: Codex <codex@openai.com>
Borda
approved these changes
Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PostProcess._postprocess_maskscallstarget_sizes[i].tolist()inside the per-image loop, but only on the path whereupsample_masks_to_image_size=True. Whentarget_sizesis on CUDA,.tolist()forces a device-to-host sync, so a batch of B images pays B separate syncs in the middle of the masks work instead of one.This reads the whole
[B, 2]tensor once, before the loop, and only when it's actually going to be used (upsampling requested and the batch isn't empty), then indexes the resulting host-side list per image:The native-resolution path and the empty-batch path already skipped this read and still do — I added a test that checks that directly instead of trusting it by inspection.
COCOEvalCallback._convert_targets(training/callbacks/coco_eval.py) has the identical shape:h, w = t["orig_size"].tolist()inside a per-target loop over the batch's GT dicts. It runs on every validation batch during training (and every training batch too, whencompute_train_metrics=True), so it pays the same per-image sync — on a hotter path than inference-only mask postprocessing. Same fix: stack every target'sorig_sizeonce before the loop, following the pattern this file's EMA branch already uses a few lines above it (torch.stack([t["orig_size"] for t in outputs["targets"]])). I didn't re-run today's perf benchmark for this path — it's a Lightning callback, not the standalone microbenchmark harness_postprocess_maskshas — so this is the same correctness/sync-removal change, not a separately measured number.On an RTX 4060, postprocess-only (101 rounds, 3 separate processes,
score_threshold=0.5matchingpredict()'s default so the per-imagenonzero()sync from score filtering runs in both baseline and candidate): 6.49–7.66% median reduction at batch 8, 9.69–10.31% at batch 16, gap growing with batch, which is what you'd expect from removingB-1mid-loop syncs. I also ran a CPU-only control over the same 101 rounds and it shows overlapping ranges with two sign flips out of six, so this isn't just fewer Python list allocations — it tracks the sync removal.I have to be honest about end-to-end: full forward + postprocess on this same GPU came back neutral, −0.10% to 0.53% at batch 8 and −0.03% to 0.22% at batch 16, inside noise. I'm not claiming an end-to-end win here — the postprocess-only number is the real claim, and on a smaller/laptop GPU the sync cost is apparently not the bottleneck end-to-end. It might be a different story on a GPU where host syncs cost more relative to the forward pass, but I didn't measure that today so I won't assert it.
Equivalence: ran the baseline twice against itself and once against the candidate on 16 real COCO val2017 images, boxes matched by IoU + assignment. Boxes, scores, labels, and masks came back byte-identical across all three runs.
Changes
postprocess.py: prefetchtarget_sizes.tolist()once per batch instead of once per image.training/callbacks/coco_eval.py: same prefetch for_convert_targets's per-targetorig_size.tolist().test_postprocess.py: new parametrized test asserting the exacttolist()call shapes for the upsampled, native-resolution, and empty-batch cases, plus a@pytest.mark.gpuvariant that runs the same call-count assertion with every input tensor actually on CUDA (the real site of the device-to-host sync). Confirmed it fails first against the current code (1 failed, wrong call shapes), then passes with the fix (25 passed).test_coco_eval_callback.py: same shape of test for_convert_targets, parametrized over a multi-target batch and an empty one, plus its own@pytest.mark.gpuCUDA variant. Also confirmed failing first (assert [(2,), (2,), (2,)] == [(3, 2)]), then passing.CHANGELOG.md: added a### Changedentry under[Unreleased].Validation
test_postprocess.py, 129 intest_coco_eval_callback.py).pre-commit run --all-files: all 19 hooks clean, includingmypy --stricton both touched modules.uv run --no-sync pytest src/ tests/ -n 2 -m "not gpu and not coco17 and not e2e_coreml and not e2e_executorch and not e2e_roboflow and not xla and not tpu" --ignore=tests/run_smoke_all_models.py --ignore=tests/legacy/test_checkpoint_compat.py --timeout=420(the exact commandci-tests-cpu.ymlruns): 3981 passed, 78 skipped, 0 failed.-m gputests in both touched test files (real RTX 4060): 4 passed — the 2 pre-existing ones plus the 2 new CUDA-specific regression tests added by this PR.develop.I checked for an existing PR on this — only hit was #921 (OBB support), which still has the per-image read and doesn't touch this. This is independent of #1367, which is training-time (
criterion.py), not this postprocess path.Small, narrow perf change, but flagged as real vs. neutral where it actually is .. hope this helps!