Skip to content

fix: pad non-square batches to backbone block size in collator, not in transforms - #992

Merged
Borda merged 8 commits into
roboflow:developfrom
Irfan-Hamid-creates:fix/divisor-pad-at-batch-level
Apr 24, 2026
Merged

Borda merged 8 commits into
roboflow:developfrom
Irfan-Hamid-creates:fix/divisor-pad-at-batch-level

Conversation

@Irfan-Hamid-creates

Copy link
Copy Markdown
Contributor

What does this PR do?

Follow-up to #991. That PR fixed the immediate training crash with square_resize_div_64=False by appending PadIfNeeded(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 the NestedTensor mask.

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

PadIfNeeded inside 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, so block_size=32.

Input batch (H × W):

  • Image A (landscape): 800 × 1200
  • Image B (portrait): 1200 × 800

Each image through the current pipeline:

Step Image A (H × W) Image B (H × W)
Input 800 × 1200 1200 × 800
SmallestMaxSize(896) 896 × 1344 1344 × 896
LongestMaxSize(1333) 889 × 1333 1333 × 889
PadIfNeeded(divisor=32) 896 × 1344 1344 × 896

Now the batch collator:

  • batch_max_H = max(896, 1344) = 1344
  • batch_max_W = max(1344, 896) = 1344
  • Batch tensor shape: (2, 3, 1344, 1344)

Per-image pad breakdown inside the 1344 × 1344 slot:

Image A (landscape, effective shape 896 × 1344):

  • Bottom 448 rows: batch-level pad, mask marks pad (correct).
  • 7 extra rows (889 to 896) and 11 cols (1333 to 1344) added by 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):

  • Right 448 cols: batch-level pad, mask marks pad (correct).
  • 11 extra rows (1333 to 1344) and 7 cols (889 to 896) added by PadIfNeeded: inside the mask's "content" region, so mask marks them as content (wrong).

After this PR, the transform-level PadIfNeeded is gone. Per-image outputs are 889 × 1333 and 1333 × 889 (natural post-resize shapes). The collator computes batch_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

  1. Add an optional block_size parameter to nested_tensor_from_tensor_list (and its ONNX variant). When set, round max_size[1]/max_size[2] up to the next multiple of block_size before allocating the batch tensor. The rounded-up strip is explicitly marked True in the mask.
  2. Add a picklable make_collate_fn(block_size) factory (uses functools.partial over a module-level helper so it survives multi-process DataLoaders and DDP spawn).
  3. In RFDETRDataModule.__init__, compute block_size = model_config.patch_size * model_config.num_windows once and use the factory at every DataLoader site.
  4. Drop the per-image PadIfNeeded, the divisor kwarg on _build_train_resize_config, and the _pad_to_divisor_config helper. 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

  • Bug fix (non-breaking change that fixes an issue)

Testing

  • I have tested this change locally
  • I have added/updated tests for this change

Test details:

  • Added TestNestedTensorBlockSize (5 tests): verify block_size=None preserves old behavior, block_size=32 rounds 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.
  • Added TestMakeCollateFn (4 tests): verify the factory's default is backward-compatible, rounds batch-max correctly with block_size=32, passes targets through unchanged, and that mixed landscape+portrait batches have every pad cell correctly masked.
  • Removed TestBuildTrainResizeConfigDivisor and TestNonSquareResizeDivisibilityRegression from tests/datasets/test_coco_resize_config.py (those pinned the removed divisor kwarg / PadIfNeeded step).
  • Reverted two count assertions in tests/datasets/test_augmentations.py: val/test pipeline now has 2 resize wrappers (SmallestMaxSize + LongestMaxSize) instead of 3.
  • 951 tests in (tests/datasets/, tests/utilities/, tests/training/) all pass locally.
  • pre-commit run clean on every touched file.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code where necessary, particularly in hard-to-understand areas
  • My changes generate no new warnings or errors
  • I have updated the documentation accordingly (if applicable)

…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.
@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.19355% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 80%. Comparing base (c255929) to head (c51c227).
⚠️ Report is 1 commits behind head on develop.

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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Borda Borda added the bug Something isn't working label Apr 23, 2026
@Borda
Borda requested a review from Copilot April 23, 2026 23:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 to nested_tensor_from_tensor_list (and ONNX variant) plus a picklable make_collate_fn(block_size) factory.
  • Wire RFDETRDataModule DataLoaders to use the new collator with block_size = patch_size * num_windows.
  • Remove transform-level PadIfNeeded behavior 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_transforms no longer guarantees that output H/W are divisible by patch_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 use make_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.

Comment thread src/rfdetr/utilities/tensors.py
Comment thread src/rfdetr/utilities/tensors.py
Comment thread src/rfdetr/training/module_data.py Outdated
Borda and others added 6 commits April 24, 2026 01:46
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
Borda merged commit b7fb514 into roboflow:develop Apr 24, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants