perf(inference): skip padding-mask work the batch already proved unnecessary - #1416
Open
JESUSROYETH wants to merge 1 commit into
Open
perf(inference): skip padding-mask work the batch already proved unnecessary#1416JESUSROYETH wants to merge 1 commit into
JESUSROYETH wants to merge 1 commit into
Conversation
JESUSROYETH
requested review from
Borda,
SkalskiP,
isaacrob and
probicheaux
as code owners
August 31, 2026 18:18
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1416 +/- ##
=======================================
Coverage 86% 86%
=======================================
Files 114 114
Lines 14880 14904 +24
=======================================
+ Hits 12835 12860 +25
+ Misses 2045 2044 -1 🚀 New features to boost your workflow:
|
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
nested_tensor_from_tensor_listalready decides, from Python-side shapes alone, whether any image in the batch needed padding. It throws that away. Downstream the all-False mask it produced is then read on every forward pass to rebuild three values that are constants of the batch shape.This records the decision on
NestedTensor.no_paddingand uses it:nested_tensor_from_tensor_list— when the input is already a 4-D batch and nothing needed padding, the padded tensor it would build is the input element for element, so the allocation and the full copy are skipped. When the input is a list, the mask is built as zeros instead of ones plus one write per image.Backbone._level_mask— nearest-neighbour resampling of an all-False mask is all-False at every output size, so the per-levelF.interpolatebecomestorch.zeros.Backbone.forward_exportalready makes exactly this substitution under its own no-padding assumption.PositionEmbeddingSine.forward— for an all-False mask the embedding is a pure function of the mask's shape and device, so it is served from a cache bounded to 8 entries instead of rebuildingcumsum,arange,pow,sin/cos, twostack+flatten,catandpermuteevery call.A batch that carries real padding — a ragged list, or
block_sizedivisor round-up — is untouched and still reads the mask. The default isno_padding=False, so the flag is off unlessnested_tensor_from_tensor_listpositively established there was nothing to pad. Two branches deliberately stay unflagged and so keep the previous behaviour exactly: the ONNX-tracing branch, which returns from_onnx_nested_tensor_from_tensor_listbefore any of this, andRFDETRDataModule.transfer_batch_to_device, which rebuilds the batch after a Kornia geometric transform that can introduce real padding.Why a cache hit is safe
PositionEmbeddingSineholds no parameters and no buffers (list(module.parameters()) == [],list(module.buffers()) == [],state_dict() == {}— asserted intest_module_holds_no_parameters_or_buffers). With the mask contents fixed by the flag, the remaining inputs are shape, device and layout, all of which are in the key, plusscale/temperature/normalize, which are set once in__init__and never reassigned by any call path in this codebase. A cache hit returns the same tensor object on every call, so it is also safe only because every current consumer reads it out-of-place (with_pos_embed'stensor + pos, and theflatten(2).transpose(1, 2)view intransformer.py) rather than mutating it in place. The cache lives in__dict__, not as a buffer, so it stays out ofstate_dictand is never silently moved by.to()— the device is part of the key instead.Results
Ephemeral GCP L4 (
g2-standard-8, torch 2.9.1+cu129), base6674d858. Each cell is the median of 8 alternating baseline/patched fresh processes, each reporting the median of 60 timed calls after 30 warm-ups; the bracket is the min/max across those 8 process medians.LWDETR.forward:Public
RFDETR.predict()on one real COCO image (val2017/000000039769.jpg, 640x480), 6 alternating processes per model, 60 timed calls each:The work removed is CPU launch overhead, which is roughly constant per forward call regardless of batch size, while GPU compute time grows with it. Base already crosses into GPU-bound territory by batch 4; nano, the smallest and fastest model, needs batch 8 — those two rows are reported to show that crossover, not a regression. Small and medium were not separately measured at batch >=4; being between nano and base in compute cost, their crossover point is expected to fall somewhere in between, not necessarily at batch 4.
Correctness
pred_boxesandpred_logitscompared withtorch.equalacross separate baseline and patched processes, max absolute difference0.0in every configuration.predict()returns the same detection count and the same coordinate and confidence checksums on every one of the 36 measured runs.test_position_encoding.py, which fails to import_POS_CACHE_MAX_ENTRIESbefore the patch).Not covered
resolution=512,expanded_scales=True) filling all 8 cache slots retains 30.94 MiB at batch 4 and 123.75 MiB at batch 16 (repro/pos_cache_memory.py). That is bounded — it cannot grow past 8 entries — but it is not free, and a training setup already close to its VRAM ceiling should account for it.transfer_batch_to_devicerebuilds the batch asNestedTensor(img_aug, padding_mask_aug)after a geometric transform that can introduce real padding. But that path is opt-in (augmentation_backend="kornia"/"auto"with CUDA); the defaultaugmentation_backend="cpu"resizes every sample to one fixed square scale before collate (square_resize_div_64=True,multi_scale=True,do_random_resize_via_padding=Falseall default), so those batches collate with the flag set. The per-batch multi-scale step that follows (on_train_batch_start) then resizes the whole batch uniformly in place without touching the flag; a new test confirms it still describes the mask correctly afterwards and that the backbone mask and position-embedding cache match the unflagged path bit for bit on that mutated batch.do_random_resize_via_padding=Trueand COCO validation's variable-sized images stay correctly unflagged for the same reason as Kornia. I don't know if any of this moves measured step time .. the backward pass, optimizer step, and data loading may dwarf the forward-only overhead measured above.