Skip to content

Commit 7ec4535

Browse files
JESUSROYETHBordacodex
authored
perf(training): pack per-sample targets across the DataLoader boundary (#1399)
* perf(training): pack per-sample targets across the DataLoader boundary * fix(data): harden packed-target worker handoff --------- Co-authored-by: Jesús Royeth <JESUSROYETH@users.noreply.github.com> Co-authored-by: jirka <6035284+borda@users.noreply.github.com> Co-authored-by: Codex <codex@openai.com>
1 parent aa92039 commit 7ec4535

9 files changed

Lines changed: 590 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
88

99
### Added
1010

11+
- Added `TrainConfig.pack_targets` (default `True`), which concatenates each batch's per-sample target dicts into one tensor per field before they cross the DataLoader worker-to-main boundary, and rebuilds them in `transfer_batch_to_device`. Every tensor a worker returns is moved into its own shared-memory segment and passed to the parent as a file descriptor, so a batch of 16 crosses as 114 objects of which 112 carry a few kilobytes in total; packing takes that to 9 without changing a byte of payload. The rebuilt targets are bit-identical, dtypes included, and the training step receives the same plain list of dicts as before. The maintainer selected the default after submitted detection-loader measurements favored packing. All `RFDETRDataModule` loaders yield batches whose targets are `PackedTargets` rather than a tuple of dicts whenever they pack losslessly, which is visible to direct consumers; a batch that cannot pack losslessly falls back to the original tuple of dicts. Representative detection and segmentation host/device-memory and transfer benchmarks have not yet been collected, so that default's full memory envelope remains a follow-up boundary.
12+
1113
- Added `TrainConfig.eval_batch_size`, decoupling the validation, test and predict dataloaders from the training micro-batch size. The three eval loaders previously always reused the resolved `batch_size`, so lowering it to fit an optimizer step also shrank evaluation batches. Evaluation runs under `no_grad`, which avoids autograd activation storage, but in-fit validation still shares device memory with the model and optimizer state and needs memory for its own forward outputs. The default `None` inherits `batch_size` exactly as before. Unlike `batch_size` it accepts no `"auto"`: an explicit `eval_batch_size` is never probed and stays usable on the `batch_size="auto"` path, while leaving it unset keeps the existing "auto was never resolved" error for eval loaders too. The training dataloader, including its `grad_accum_steps` alignment padding, is unaffected.
1214

1315
- `deploy_to_roboflow()`'s `version` argument is now optional: when omitted, the highest existing dataset version of the target project is resolved automatically via the Roboflow API (falling back to version `1` for a project with no generated versions, where the Roboflow SDK then raises its usual "Version number 1 is not found."). Passing an explicit `version` behaves exactly as before, with no extra API call. ([#1116](https://github.com/roboflow/rf-detr/issues/1116))

docs/learn/train/customization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ module = RFDETRModelModule(model_config, train_config)
5757
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5858
| `on_fit_start` | Seeds RNGs when `train_config.seed` is set. |
5959
| `on_train_batch_start` | Applies multi-scale random resize when `train_config.multi_scale=True`. |
60-
| `transfer_batch_to_device` | Moves `NestedTensor` batches to the target device. |
60+
| `transfer_batch_to_device` | Moves `NestedTensor` batches and targets to the target device. With the default `TrainConfig.pack_targets=True`, it materializes `PackedTargets` into a plain per-sample dict list. |
6161
| `training_step` | Computes loss and logs `train/loss` plus per-term losses. Keypoint models use manual optimization with box-normalized accumulation across microbatches; detection and segmentation use Lightning's automatic optimization path. |
6262
| `validation_step` | Runs forward pass and postprocessing; returns `{results, targets}` for `COCOEvalCallback`. |
6363
| `test_step` | Same as `validation_step`, logs under `test/`. |

docs/learn/train/training-parameters.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,14 @@ The parameters below are available for fine-grained control over training behavi
294294

295295
### DataLoader Tuning
296296

297-
| Parameter | Type | Default | Description |
298-
| -------------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------- |
299-
| `pin_memory` | `bool` | `None` | Pin host memory in the DataLoader for faster GPU transfers. `None` defers to PyTorch Lightning's default. |
300-
| `persistent_workers` | `bool` | `None` | Keep DataLoader worker processes alive between epochs. `None` defers to PyTorch Lightning's default. |
301-
| `prefetch_factor` | `int` | `None` | Number of batches to prefetch per DataLoader worker. `None` uses PyTorch's built-in default. |
297+
| Parameter | Type | Default | Description |
298+
| -------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------ |
299+
| `pin_memory` | `bool` | `None` | Pin host memory in the DataLoader for faster GPU transfers. `None` defers to PyTorch Lightning's default. |
300+
| `persistent_workers` | `bool` | `None` | Keep DataLoader worker processes alive between epochs. `None` defers to PyTorch Lightning's default. |
301+
| `prefetch_factor` | `int` | `None` | Number of batches to prefetch per DataLoader worker. `None` uses PyTorch's built-in default. |
302+
| `pack_targets` | `bool` | `True` | Concatenate target dicts before crossing the DataLoader worker boundary. See the contract below; set `False` to opt out. |
303+
304+
With `pack_targets=True`, train, validation, test, and predict loaders yield batches whose target element is `PackedTargets` whenever packing is lossless. The Lightning `transfer_batch_to_device` hook accepts those batches or an unpacked tuple of target dicts. It moves packed fields to the target device, then materializes them into the same plain per-sample dict list that training, validation, test, and prediction hooks receive on the unpacked path. Batches that cannot be packed losslessly retain their original tuple of dicts.
302305

303306
## Complete Parameter Reference
304307

@@ -354,3 +357,4 @@ Below is a summary table of all training parameters:
354357
| `pin_memory` | bool | None | Pin DataLoader memory. None defers to PyTorch Lightning's default. |
355358
| `persistent_workers` | bool | None | Keep DataLoader workers alive between epochs. None uses PTL default. |
356359
| `prefetch_factor` | int | None | Number of batches prefetched per worker. None uses PyTorch default. |
360+
| `pack_targets` | bool | True | Concatenate target dicts before crossing the DataLoader worker boundary. See DataLoader Tuning; set False to opt out. |

src/rfdetr/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,7 @@ def _coerce_amp_dtype(cls, value: Any) -> Any:
12671267
pin_memory: bool | None = None
12681268
persistent_workers: bool | None = None
12691269
prefetch_factor: int | None = None
1270+
pack_targets: bool = True
12701271

12711272
@field_validator("batch_size", mode="after")
12721273
@classmethod

src/rfdetr/training/module_data.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from rfdetr.datasets.yolo import YoloSplitUnavailableError
2323
from rfdetr.utilities.box_ops import box_xyxy_to_cxcywh
2424
from rfdetr.utilities.logger import get_logger
25-
from rfdetr.utilities.tensors import make_collate_fn
25+
from rfdetr.utilities.tensors import PackedTargets, make_collate_fn
2626

2727
logger = get_logger()
2828

@@ -153,6 +153,7 @@ def __init__(self, model_config: ModelConfig, train_config: TrainConfig) -> None
153153
)
154154
self._collate_fn = make_collate_fn(
155155
block_size=block_size,
156+
pack=train_config.pack_targets,
156157
)
157158

158159
self._dataset_train: torch.utils.data.Dataset[Any] | None = None
@@ -362,7 +363,8 @@ def train_dataloader(self) -> DataLoader[Any]:
362363
so that PTL can auto-inject ``DistributedSampler`` in DDP mode.
363364
364365
Returns:
365-
DataLoader for the training dataset.
366+
DataLoader for the training dataset. With ``TrainConfig.pack_targets=True`` (the default), its collated
367+
batches contain ``PackedTargets`` for losslessly packable target batches.
366368
"""
367369
dataset: torch.utils.data.Dataset[Any] = self._require_dataset(self._dataset_train, "fit")
368370
batch_size = self._resolve_batch_size()
@@ -417,7 +419,8 @@ def val_dataloader(self) -> DataLoader[Any]:
417419
"""Return the validation DataLoader.
418420
419421
Returns:
420-
DataLoader for the validation dataset with sequential sampling.
422+
DataLoader for the validation dataset with sequential sampling. With ``TrainConfig.pack_targets=True``
423+
(the default), its collated batches contain ``PackedTargets`` for losslessly packable target batches.
421424
"""
422425
dataset = self._require_dataset(self._dataset_val, "validate")
423426
return DataLoader(
@@ -437,7 +440,8 @@ def test_dataloader(self) -> DataLoader[Any]:
437440
"""Return the test DataLoader.
438441
439442
Returns:
440-
DataLoader for the test dataset with sequential sampling.
443+
DataLoader for the test dataset with sequential sampling. With ``TrainConfig.pack_targets=True`` (the
444+
default), its collated batches contain ``PackedTargets`` for losslessly packable target batches.
441445
"""
442446
dataset = self._require_dataset(self._dataset_test, "test")
443447
return DataLoader(
@@ -457,7 +461,8 @@ def predict_dataloader(self) -> DataLoader[Any]:
457461
"""Return the predict DataLoader (reuses the validation dataset, no augmentation).
458462
459463
Returns:
460-
DataLoader for the validation dataset with sequential sampling.
464+
DataLoader for the validation dataset with sequential sampling. With ``TrainConfig.pack_targets=True``
465+
(the default), its collated batches contain ``PackedTargets`` for losslessly packable target batches.
461466
"""
462467
dataset = self._require_dataset(self._dataset_val, "predict")
463468
return DataLoader(
@@ -796,7 +801,9 @@ def transfer_batch_to_device(
796801
``NestedTensor`` must be moved explicitly.
797802
798803
Args:
799-
batch: Tuple of (NestedTensor samples, list of target dicts).
804+
batch: Tuple of ``NestedTensor`` samples and targets that are either a collated ``PackedTargets`` object
805+
or an unpacked tuple of target dicts. Packed targets are materialized to a plain list of dicts after
806+
transfer.
800807
device: Target device.
801808
dataloader_idx: Index of the dataloader providing this batch.
802809
@@ -806,5 +813,10 @@ def transfer_batch_to_device(
806813
samples, targets = batch
807814
non_blocking = device.type == "cuda"
808815
samples = samples.to(device, non_blocking=non_blocking)
809-
targets = [{k: v.to(device, non_blocking=non_blocking) for k, v in t.items()} for t in targets]
816+
if isinstance(targets, PackedTargets):
817+
# One transfer per field instead of one per field per sample, then materialised so that callers can
818+
# mutate the dicts exactly as they do on the unpacked path.
819+
targets = targets.to(device, non_blocking=non_blocking).as_list()
820+
else:
821+
targets = [{k: v.to(device, non_blocking=non_blocking) for k, v in t.items()} for t in targets]
810822
return samples, targets

src/rfdetr/utilities/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
from rfdetr.utilities.state_dict import clean_state_dict, strip_checkpoint
2626
from rfdetr.utilities.tensors import (
2727
NestedTensor,
28+
PackedTargets,
2829
collate_fn,
2930
make_collate_fn,
3031
nested_tensor_from_tensor_list,
32+
pack_targets,
3133
)
3234

3335
__all__ = [
@@ -41,9 +43,11 @@
4143
"save_on_master",
4244
# tensors
4345
"NestedTensor",
46+
"PackedTargets",
4447
"collate_fn",
4548
"make_collate_fn",
4649
"nested_tensor_from_tensor_list",
50+
"pack_targets",
4751
# box_ops (submodule)
4852
"box_ops",
4953
# logger

0 commit comments

Comments
 (0)