fix: pad non-square batches to backbone block size in collator, not in transforms - #992
Merged
Borda merged 8 commits intoApr 24, 2026
Conversation
…e collate factory Add an optional block_size parameter to nested_tensor_from_tensor_list (and its ONNX variant), and a picklable make_collate_fn(block_size) factory. The DataModule now computes patch_size * num_windows once in __init__ and uses the factory at every DataLoader site. No behavior change for existing callers: block_size=None preserves the pre-change shape and mask.
…collator handles divisibility The previous PR (roboflow#991) added PadIfNeeded to the non-square resize pipeline to satisfy the windowed-attention backbone's divisibility assertion. Now that the batch collator handles the same constraint via block_size, the per-image pad is redundant and leaves its divisor-round-up strip inside each image's reported content region, so downstream attention treats the zero-pad strip as real tokens. Drop the per-image PadIfNeeded, the divisor kwarg on _build_train_resize_config, and the _pad_to_divisor_config helper. The corresponding tests added in roboflow#991 are removed; the val/test wrapper-count expectations in test_augmentations.py are reverted to 2.
Irfan-Hamid-creates
requested review from
Borda,
SkalskiP,
isaacrob and
probicheaux
as code owners
April 23, 2026 23:14
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #992 +/- ##
======================================
- Coverage 80% 80% -0%
======================================
Files 100 100
Lines 8378 8392 +14
======================================
+ Hits 6667 6674 +7
- Misses 1711 1718 +7 🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
Moves backbone divisibility padding from the Albumentations transform pipeline into the batch collator so that all padded pixels (including divisor round-up) are correctly reflected in the NestedTensor mask.
Changes:
- Add
block_size-aware padding tonested_tensor_from_tensor_list(and ONNX variant) plus a picklablemake_collate_fn(block_size)factory. - Wire
RFDETRDataModuleDataLoaders to use the new collator withblock_size = patch_size * num_windows. - Remove transform-level
PadIfNeededbehavior and update/replace related tests and structural assertions.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/rfdetr/utilities/tensors.py |
Adds block-size rounding to NestedTensor creation and introduces make_collate_fn. |
src/rfdetr/training/module_data.py |
Switches DataLoaders to the new block-size-aware collate function. |
src/rfdetr/datasets/coco.py |
Removes transform-level divisor padding and updates resize config/docs accordingly. |
src/rfdetr/utilities/__init__.py |
Re-exports make_collate_fn as part of the public utilities API. |
src/rfdetr/util/misc.py |
Re-exports make_collate_fn via the deprecated module for compatibility. |
tests/utilities/test_tensors.py |
Adds unit tests for block_size rounding and make_collate_fn. |
tests/datasets/test_coco_resize_config.py |
Removes tests that asserted the now-removed PadIfNeeded/divisor behavior. |
tests/datasets/test_augmentations.py |
Updates wrapper-count assertions after removing PadIfNeeded from val/test pipelines. |
Comments suppressed due to low confidence (1)
src/rfdetr/datasets/coco.py:433
- After removing transform-level
PadIfNeeded,make_coco_transformsno longer guarantees that output H/W are divisible bypatch_size * num_windows(the DataLoader collate now handles that). It would help to call this out explicitly in the docstring so users who apply transforms outside the DataModule understand they must usemake_collate_fn/nested_tensor_from_tensor_list(..., block_size=...)to satisfy the backbone constraint.
resolution: Target short-side resolution in pixels. During validation the
longest side is capped at 1333 px to preserve aspect ratio.
multi_scale: If ``True``, sample the resize target from a range of scales
computed by :func:`compute_multi_scale_scales` instead of using a
single fixed size.
expanded_scales: Passed to :func:`compute_multi_scale_scales`; broadens the
scale range when ``multi_scale=True``.
skip_random_resize: When ``multi_scale=True``, use only the largest scale
and skip random selection among multiple scales.
patch_size: Model patch size used by :func:`compute_multi_scale_scales` to
ensure all candidate resolutions are compatible with the backbone.
num_windows: Number of attention windows; used by
:func:`compute_multi_scale_scales` to derive candidate resolutions.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
[resolve roboflow#2] Review comment by @Copilot (PR roboflow#992): "docstrings here say list_of_targets but batch = list(zip(*batch)) makes batch[1] a tuple" --- Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve roboflow#4] /review finding by foundry:qa-specialist (report): "docstring claims picklable but no test verifies pickle.dumps(make_collate_fn(block_size=32))" --- Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve roboflow#5] /review finding by foundry:linting-expert (report): "Optional[int] instead of int | None; project requires-python >= 3.10 and uses modern union syntax elsewhere" --- Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve roboflow#6] /review finding by foundry:doc-scribe (report): "make_coco_transforms no longer guarantees divisibility; users applying transforms outside DataModule must use make_collate_fn" --- Co-authored-by: Claude Code <noreply@anthropic.com>
Borda
approved these changes
Apr 24, 2026
7 tasks
Closed
2 tasks
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.
What does this PR do?
Follow-up to #991. That PR fixed the immediate training crash with
square_resize_div_64=Falseby appendingPadIfNeeded(pad_*_divisor=patch_size*num_windows)to the non-square resize pipeline, so the windowed-attention backbone's divisibility assertion passes. That works, but it leaves a subtle residual issue with theNestedTensormask.This PR moves the divisibility padding from the transform pipeline into the batch collator. Every zero-pad pixel (batch-level AND divisor round-up) is now correctly marked as padding in the mask, so downstream attention ignores it instead of treating it as real content.
Why the previous approach under-counts pad in the mask
PadIfNeededinside the transform pipeline pads each image before it reaches the collator. The collator then builds each image's mask from the per-image shape after transforms, so the padded region is reported as "content" even though it's actually zeros.Concrete trace: mixed landscape + portrait batch
Config:
resolution=896,max_size=1333,patch_size=16,num_windows=2, soblock_size=32.Input batch (H × W):
Each image through the current pipeline:
SmallestMaxSize(896)LongestMaxSize(1333)PadIfNeeded(divisor=32)Now the batch collator:
batch_max_H = max(896, 1344) = 1344batch_max_W = max(1344, 896) = 1344Per-image pad breakdown inside the 1344 × 1344 slot:
Image A (landscape, effective shape 896 × 1344):
PadIfNeeded: inside the mask's "content" region, so mask marks them as content (wrong, they are actually zeros).Image B (portrait, effective shape 1344 × 896):
PadIfNeeded: inside the mask's "content" region, so mask marks them as content (wrong).After this PR, the transform-level
PadIfNeededis gone. Per-image outputs are 889 × 1333 and 1333 × 889 (natural post-resize shapes). The collator computesbatch_max = (1333, 1333)and rounds up to(1344, 1344). Every zero-pad cell (batch-level AND the divisor round-up) is marked pad in the mask. Downstream attention correctly ignores all of it.Fix
block_sizeparameter tonested_tensor_from_tensor_list(and its ONNX variant). When set, roundmax_size[1]/max_size[2]up to the next multiple ofblock_sizebefore allocating the batch tensor. The rounded-up strip is explicitly markedTruein the mask.make_collate_fn(block_size)factory (usesfunctools.partialover a module-level helper so it survives multi-process DataLoaders and DDP spawn).RFDETRDataModule.__init__, computeblock_size = model_config.patch_size * model_config.num_windowsonce and use the factory at every DataLoader site.PadIfNeeded, thedivisorkwarg on_build_train_resize_config, and the_pad_to_divisor_confighelper. Backbone still receives divisibility-compliant input, and the mask is now correct.Related Issue(s): Follow-up to #991. The original training crash it fixed was issue #983.
Type of Change
Testing
Test details:
TestNestedTensorBlockSize(5 tests): verifyblock_size=Nonepreserves old behavior,block_size=32rounds batch-max up, parametrized for several block sizes (32, 56, 64), and that every pad cell (content vs divisor strip) is correctly marked in the mask.TestMakeCollateFn(4 tests): verify the factory's default is backward-compatible, rounds batch-max correctly withblock_size=32, passes targets through unchanged, and that mixed landscape+portrait batches have every pad cell correctly masked.TestBuildTrainResizeConfigDivisorandTestNonSquareResizeDivisibilityRegressionfromtests/datasets/test_coco_resize_config.py(those pinned the removeddivisorkwarg /PadIfNeededstep).tests/datasets/test_augmentations.py: val/test pipeline now has 2 resize wrappers (SmallestMaxSize+LongestMaxSize) instead of 3.tests/datasets/,tests/utilities/,tests/training/) all pass locally.pre-commit runclean on every touched file.Checklist