1.10.0
windmill-labs/windmill1.10.0Sep 4, 2026by Borda
AI Summary
A training-focused performance release that significantly speeds up training steps (23%) and inference on large frames (46%) through optimizer parameter merging and CUDA optimizations.
Key Highlights
- Training steps ~23% faster at default batch size (optimizer parameter-group merge)
- `predict()` up to 46% faster on large frames (zero-copy uint8 transfer)
- New GPU batched linear-assignment solver for the Hungarian matcher
- Validation now evaluates one model per epoch (EMA only)
- `grad_accum_steps` defaults to 1 (was 4)
Breaking Changes
- `grad_accum_steps` default changed from 4 to 1
- Optimizer parameter groups collapsed (465 -> 28 for nano), affecting positional indexing
- Validation now reports EMA metrics only, not non-EMA
- Dataset builders now require complete pipeline-option namespace
New Features
- GPU batched linear-assignment solver
Full Release Notes
๐ **RF-DETR 1.10.0 is a training-focused release: a 10k-image dataset trained for 10 epochs on an L4 went from 55 minutes to 31 minutes.**
**Faster, with no code change:**
- **โ23% per training step** at the default batch size, from the optimizer parameter-group merge.
- **Up to โ46% on `predict()`** for large input frames, from moving uint8 widening onto the accelerator.
- **1.25โ2.26ร DataLoader throughput** on input-bound runs, from packed targets and draft JPEG decoding.
**New:** a **GPU batched linear-assignment solver** for the matcher on CUDA, active automatically on eligible devices.
**Six changes alter behavior for callers who change nothing. Read the Migration guide before you retrain:**
- Validation evaluates **one model per epoch instead of two**, so `val/*` metrics now describe the EMA model.
- `grad_accum_steps` defaults to **`1` instead of `4`**, moving the default effective batch from 16 to 4.
- Optimizer parameter groups collapse from **one-per-parameter to one-per-hyperparameter**.
- Dataset builders **validate their config** instead of silently falling back to wrong defaults.
- `log_per_class_metrics` defaults to **`False`**, dropping per-class keys from a default run.
- `compute_val_loss` defaults to **`"auto"`**, computing `val/loss` only when something consumes it.
*This release doesn't restate content already shipped in the 1.9.1โ1.9.4 patch releases (a separate maintenance line); see [those sections](../../CHANGELOG.md) if you're upgrading directly from 1.9.0.*
## โจ Spotlights
The seven changes most likely to affect you, either because they make code you already have faster, or because they change what that code does.
### ๐๏ธ Training steps are about 23% faster at the default batch size
No code change needed. `get_param_dict` used to build one optimizer parameter group per trainable tensor (465 groups on `rfdetr-nano`), which disabled AdamW's `foreach`/`fused` multi-tensor batching entirely. Grouping by the `(lr, weight_decay)` pair each parameter already carried collapses that to 28 groups, and every parameter keeps exactly the learning rate and weight decay it had before.
At the default `batch_size=4`, a full `RFDETR.train()` step on an L4 falls from 223.17 ms to 170.77 ms on `rfdetr-nano`, a drop of **โ23.5%**, with `rfdetr-small` matching at โ23.3%. The saving is a fixed ~50โ60 ms per optimizer step rather than a fraction of it, so its share shrinks as the batch grows: โ17.7% at batch 8, โ9.1% at batch 16. Weights are bit-identical across the two groupings. Every configuration is tabulated under [Training](#-training). ([#1409])
End to end, with the release's evaluation and loader work stacked on top: a 10,000-image dataset trained for 10 epochs on an L4 went from **55 minutes on 1.9.4 to 31 minutes on 1.10.0**, 44% less wall clock for the same run. Full COCO2017 trains at about 10 minutes per epoch on an RTX 6000.
### ๐ฏ `predict()` is up to 46% faster on large frames
Also free. `predict()` used to widen uint8 pixels to float32 *before* the host-to-device copy, so the bus carried four bytes per channel where one would do; it now transfers a zero-copy uint8 view and widens on the accelerator.
On a 2160ร3840 PIL frame, `RFDETRNano.predict()` on an L4 falls from 77.022 ms to 41.319 ms, a drop of **โ46.4%**, while a 640ร640 frame gains only โ6.5%. The gain scales with the *input* frame rather than the model, since the model does identical work either way; an RTX 4060 reaches โ61% on the same 4K frame. Pixels arriving at the model stay byte-identical to `torchvision`'s `to_tensor`. Every frame size and input type is tabulated under [Inference](#-inference), alongside five further `predict()` changes in the same release. ([#1415])
### โ
Validation evaluates one model per epoch
Every validation epoch used to run two full forward passes, EMA weights and non-EMA, and report `val/*` from the non-EMA pass. It now runs one, so the reported metrics describe the model you actually ship and validation costs half the forward work.
```python
from rfdetr import RFDETRSmall
from rfdetr.config import TrainConfig
# 1.10.0 default: one forward pass (EMA), removes the second non-EMA validation pass
model = RFDETRSmall()
model.train(dataset_dir="my_dataset", epochs=50)
# Restore the previous two-forward comparison
model.train(dataset_dir="my_dataset", epochs=50, train_config=TrainConfig(eval_base_model=True))
```
### ๐ข `grad_accum_steps` now defaults to `1`, not `4`
The old default quietly multiplied your batch: `batch_size=4` really trained at an effective 16. Accumulation is now opt-in, so `TrainConfig()` means what it says; any run that relied on the old default needs the explicit argument back to keep its optimization schedule.
```python
TrainConfig() # effective batch = 4 (was 16)
TrainConfig(grad_accum_steps=4) # reproduces the 1.9.x default (effective batch 16)
```
### ๐งฎ GPU batched linear-assignment solver
The Hungarian matcher solved each decoder layer's assignment on the CPU via SciPy, paying a device-to-host sync per layer. On eligible CUDA devices the new `torch-hungarian` backend solves the batched problem on the GPU instead, and SciPy still handles every other device and any problem above the size limit.
```python
pip install 'rfdetr[train]' # pulls torch-hungarian==0.1.0rc0
# No code change needed: the matcher picks the GPU solver automatically on
# CUDA + compute capability >= 8.0 + torch >= 2.4, falling back to SciPy elsewhere.
```
### ๐งฉ Optimizer parameter groups merge (465 โ 28 groups for `rfdetr-nano`)
Same change as the training-step spotlight above, repeated here because it is also breaking: anything that indexes optimizer parameter groups positionally, or sizes a scheduler list to their count, now sees 28 groups where it saw 465.
```python
# A custom lr_scheduler_kwargs list sized to the old per-parameter group count
# needs resizing to the new (much smaller) merged group count.
TrainConfig(lr_scheduler_kwargs={"lr_lambda": [fn] * 28}) # was * 465
```
### ๐๏ธ Dataset builders now require a complete pipeline-option namespace
These low-level builders used to fill a missing pipeline option from a default that did not necessarily match your model, so a wrong `patch_size` produced a quietly mistrained model instead of an error. Only direct callers are affected: `.train()` and `RFDETRDataModule` always passed a complete namespace.
```python
# Before: missing fields silently fell back to (sometimes wrong) defaults
build_roboflow_from_coco(args=partial_namespace)
# After: raises unless every pipeline option is set
partial_namespace.square_resize_div_64 = True
partial_namespace.segmentation_head = False
partial_namespace.multi_scale = True
partial_namespace.expanded_scales = True
partial_namespace.do_random_resize_via_padding = False
partial_namespace.patch_size = model_config.patch_size
partial_namespace.num_windows = model_config.num_windows
build_roboflow_from_coco(args=partial_namespace)
```
## ๐ Migration guide
<!-- Use Draft migration guide content โ do not regenerate independently. -->
Six changes alter behavior for callers who change nothing. What to do about each:
| Change | What to do |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grad_accum_steps` defaults to `1` (was `4`) | Pass `grad_accum_steps=4` to keep the 1.9.x effective batch of 16 |
| Validation evaluates one model per epoch | `val/mAP_*`, `val/mAR`, `val/loss` now describe the evaluated model (EMA by default); pass `eval_base_model=True` for the old two-forward comparison |
| Optimizer parameter groups merge by hyperparameter | Resize any custom `lr_scheduler_kwargs` list sized per-parameter (465 groups for `rfdetr-nano`) to the merged count of 28 |
| Dataset builders require a complete pipeline-option namespace | Direct callers must set every pipeline option; read `patch_size`/`num_windows` from your `ModelConfig` rather than hardcoding them |
| `log_per_class_metrics` defaults `False` (was `True`) | Pass `log_per_class_metrics=True` if a dashboard consumes per-class keys |
| `compute_val_loss` defaults `"auto"` (was `True`) | `"auto"` keeps `val/loss` whenever something consumes it; pass `True` to force it |
`TrainConfig.eval_ema_only` is deprecated (removal in v1.13) and superseded by `eval_base_model`. Checkpoints written with the old per-parameter optimizer layout are regrouped automatically on load, so resuming a 1.9.x run needs no action.
See [MIGRATION.md](./MIGRATION.md) for the full "Upgrade 1.9 โ 1.10" guide with before/after code for each of these.
## โก Performance
**Twenty-seven pull requests in this release are performance work, and none of them trade accuracy for speed**. Every one landed with an output-parity check: bit-identical tensors, byte-identical public detections, or an unchanged COCO metric. The ones with a stage-level or end-to-end number are detailed below; the remaining component-level wins are listed with their measured effect in the [Extensive changelog](#-extensive-changelog).
### ๐ At a glance
Every row is measured rather than projected: the first two come from full training runs, the rest from the benchmark in the PR named beside them.
| What | Result | Source |
| ----------------------------------------------------- | -------------------------- | ----------------------- |
| Training wall clock, 10k images ร 10 epochs, L4 | **55 min โ 31 min (โ44%)** | real 1.9.4 โ 1.10.0 run |
| COCO2017, one epoch, RTX 6000 | **~10 min** | 1.10.0, absolute figure |
| Training step, `batch_size=4`, L4 | **โ23%** | [#1409] |
| DataLoader throughput, 16 pinned workers | **1.67ร** | [#1399] |
| DataLoader throughput, oversized JPEG sources | **up to 2.26ร** | [#1389] |
| Segmentation `loss_masks` | **6.7โ7.1ร** | [#1367] |
| Per-class COCO mAP computation | **3.28ร** | [#1375] |
| `predict()`, 2160ร3840 PIL frame, L4 | **โ46%** | [#1415] |
| `predict()`, CUDA tensor, `include_source_image=True` | **โ50%** | [#1388] |
| ONNX reference decoder, L4 CUDA | **โ5%** | [#1393] |
Three things to know before reading the detail:
- **The percentages are not additive.** Each is measured against that PR's own baseline, in that PR's own configuration, on the hardware named beside it.
- **Several changes deliberately claim nothing end to end.** Where an author measured a full-model number and it came out as noise, that is stated rather than hidden.
- **Fourteen further performance PRs shipped from the separate `1.9.1`โ`1.9.4` maintenance line.** They are excluded from every number below; if you are upgrading directly from 1.9.0 you get those too; see [Also shipped in the 1.9 patch line](#-also-shipped-in-the-19-patch-line).
### ๐๏ธ Training
Training-side work, grouped by where a run spends its wall clock: the optimizer step, the DataLoader boundary, the validation epoch, and the segmentation loss. These are the changes the end-to-end 55 โ 31 minute result is built from, though none was measured in isolation against that run.
- **Optimizer parameter groups merge by hyperparameter ([#1409])**
Full `RFDETR.train()` step on an L4, and the largest single training win in the release. Merging 465 per-parameter optimizer groups into 28 re-enables AdamW's multi-tensor batching, saving a fixed ~50โ60 ms per optimizer step, which is why the percentage falls as the batch grows. Weights stay bit-identical, and checkpoints in the old layout are regrouped on load.
| Model | Batch | Before | After | Change |
| -------------- | ----: | --------: | --------: | ---------: |
| `rfdetr-nano` | 4 | 223.17 ms | 170.77 ms | **โ23.5%** |
| `rfdetr-small` | 4 | 247.54 ms | 189.86 ms | **โ23.3%** |
| `rfdetr-small` | 8 | 357.89 ms | 294.54 ms | **โ17.7%** |
| `rfdetr-small` | 16 | 625.10 ms | 568.24 ms | **โ9.1%** |
- **Packed target transport across the DataLoader boundary ([#1399])**
COCO `train2017`, `RFDETRNano`, batch 16, on a 32-vCPU L4 host. Concatenating the 114 per-batch shared-memory objects down to 9 lifts loader throughput up to **1.67ร** and removes a reproducible `received 0 items of ancdata` crash at high worker counts. Enabled by default via `TrainConfig.pack_targets`; targets verified field by field with zero mismatches.
| Workers | `pin_memory` | Before | Packed | Ratio |
| ------: | ------------ | ----------: | ----------: | ------------------: |
| 8 | on | 228.7 img/s | 285.6 img/s | **1.25ร** |
| 8 | off | 286.3 img/s | 284.0 img/s | 0.99ร (overlapping) |
| 16 | on | 218.4 img/s | 365.6 img/s | **1.67ร** |
| 16 | off | 313.8 img/s | 443.9 img/s | **1.42ร** |
| 32 | on | 212.9 img/s | 337.0 img/s | **1.58ร** |
| 32 | off | 299.7 img/s | 465.9 img/s | **1.56ร** |
- **Draft-decoding oversized JPEGs ([#1389])**
Resolution 512, two workers, batch 8, CPU. `PIL.Image.draft` lets libjpeg decode at the cheapest power-of-two reduction that still covers the pipeline's needs; annotations are rescaled to match, and non-train splits and mask datasets keep full-resolution decoding.
| Source size | Decode box | Before | After | Ratio |
| --------------------- | ---------: | ----------: | ----------: | -------------------------: |
| 2880ร2880 | 600 | 72.2 img/s | 162.9 img/s | **2.26ร** |
| 2880ร2880 multi-scale | 768 | 71.8 img/s | 118.6 img/s | **1.65ร** |
| 1920ร1080, no jitter | 512 | 196.0 img/s | 275.7 img/s | **1.41ร** |
| 720ร720 | 600 | 326.2 img/s | 326.6 img/s | 1.00ร (no reduction legal) |
- **One-pass COCO mAP evaluation ([#1375])**
**3.28ร median speedup** on per-class COCO computation at +0.32% peak RSS, by computing aggregate and per-class AP/AR from one evaluator per IoU type instead of re-running evaluation per class. TorchMetrics is pinned to `>=1.8.2,<1.9.0` as a consequence.
- **Direct matched-mask sampling in `loss_masks` ([#1367])**
**6.7โ7.1ร faster `loss_masks`, bit-identical** (single-thread CPU microbenchmark), by replacing a full bilinear `grid_sample` with a guarded `gather` lookup; edge cases fall back to the unchanged `point_sample` path.
### ๐ฏ Inference
Six changes to `predict()` and two to postprocessing, every one of them about moving fewer bytes or doing less host work rather than changing the model. Detections are unchanged throughout. They touch different stages of the same call (source capture, host-to-device transfer, preprocessing, the forward), so their effects are largely independent, but no combined measurement was taken.
- **Widening uint8 `predict()` inputs on the accelerator ([#1415])**
`RFDETRNano.predict()` on an L4; an RTX 4060 measured up to **โ61%** at 2160ร3840. The conversion now sends one uint8 byte per channel across the bus and widens on the device, cutting transfer and pinned-staging bytes 4ร, and pixels stay byte-identical to `torchvision`'s `to_tensor`.
| Frame | Input | Before | After | Change |
| --------- | ----------- | --------: | --------: | ----------: |
| 640ร640 | PIL | 21.972 ms | 20.538 ms | โ6.53% |
| 720ร1280 | PIL | 24.789 ms | 22.216 ms | โ10.38% |
| 1080ร1920 | PIL | 31.292 ms | 24.263 ms | **โ22.46%** |
| 1080ร1920 | uint8 NumPy | 29.587 ms | 21.491 ms | **โ27.36%** |
| 2160ร3840 | PIL | 77.022 ms | 41.319 ms | **โ46.35%** |
- **Transferring CUDA source images as bytes ([#1388])**
**โ50% on `predict()`** with the default `include_source_image=True` (FP16 Nano, RTX 4060): the multiply-and-truncate to `uint8` now runs on the GPU, so the source-image transfer carries one byte per channel instead of two to eight. Neutral with `include_source_image=False`, which confirms the gain comes from source capture, not model execution; returned arrays keep the same bytes and dtype.
- **Fused uint8 image conversion ([#1390])**
**โ8.6% to โ20.5% on `predict()`** on an RTX 4060 and **โ5.0% to โ8.5%** on an L4, across PIL/NumPy inputs and FP16/FP32. The machine-independent claim is the allocation win: ~4.1 MB less allocation traffic per call and `aten::div` eliminated entirely. Full COCO val2017 mAP unchanged to nine decimals.
- **Skipping known-valid pixel-range scans ([#1387])**
**About โ10.7% on preprocessing-isolated `predict()`** (Nano and Small, FP32 and FP16, RTX 4060; every configuration between โ7.1% and โ14.1%). `to_tensor` already guarantees the `[0, 1]` range for PIL and uint8 NumPy inputs, so both validation reductions are skipped; tensor and non-uint8 NumPy inputs keep them.
- **Padding-mask work skipped when the batch proved it unnecessary ([#1416])**
**About โ3.9% on `LWDETR.forward` and โ3.5% on `predict()`** at batch 1 (nano/small/medium average, L4). The removed work is roughly constant CPU launch overhead, so the gain washes out between batch 4 and 8. Outputs bit-identical.
- **Redundant eval-mode assignments avoided ([#1419])**
**About โ4.4% on `predict()`** at batch 1 (Nano and Small, L4 and RTX 4060), by skipping the module-tree `eval()` walk when the model is already in eval mode, saving 0.43โ0.51 ms of host work per call.
- **Postprocessing**
Both are measured on the postprocessing stage alone. The batch-size hoist ([#1369]) was neutral end to end on that GPU and its author claims no end-to-end win; the preallocated buffer ([#1374]) scales its memory effect with `num_select`: Nano and Small at `num_select=100` see +1.8% instead of a reduction.
| Change | Measured effect |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Mask target sizes read once per batch ([#1369]) | Postprocess-only **โ6.5โ7.7%** at batch 8, **โ9.7โ10.3%** at batch 16 (RTX 4060) |
| Preallocated mask output ([#1374]) | K=300 at 1920ร1080: CPU **โ12%** latency / **โ21%** RSS, CUDA **โ17%** latency / **โ29%** peak memory |
### ๐ฆ Export and the torch-free decoder
The ONNX and torch-free reference decoder runs on NumPy rather than torch, so its hot spots are ordinary array operations. Both entries replace a whole-array operation with a cheaper partitioned or separable one, and both are guarded so the original path still serves the inputs where it wins.
- **Partitioned NumPy top-k selection ([#1393])**
**โ76% to โ88% on the selector** (NumPy 1.26 / 2.2) and **โ5.2% per image end to end on L4 CUDA**; the single-CPU-core end-to-end change crosses zero. A uint64-keyed partition replaces a full-grid `np.lexsort`, guarded to keep `lexsort` for dense selections. Outputs matched exactly over all 5,000 COCO val2017 images.
- **Separable NumPy bilinear resize ([#1394])**
**โ45.9% on the isolated resize and โ3.6% per image** through the full ONNX path, which ONNX Runtime execution dominates; the guarded fallback route is unchanged. Peak allocation for the mask-resize case falls from 563 to 354 MiB. The 5,000-image COCO sweep matched bit-for-bit on both routes.
### ๐ End-to-end projection
Two full runs were timed, rather than projected:
| Run | Result |
| ------------------------------------------------------- | ------------------------------------------------- |
| 10,000-image dataset, 10 epochs, L4, **1.9.4 โ 1.10.0** | **55 min โ 31 min (1.77ร, 44% less wall clock)** |
| COCO2017, one epoch, RTX 6000, 1.10.0 | **~10 min** (absolute figure, not a before/after) |
The L4 run lands above the per-step projection alone, which is consistent with the training-step, validation, and loader changes all contributing to the same epoch.
Everything else below is projected from per-PR benchmarks. The stages involved are largely disjoint (host preprocessing, host-to-device transfer, model forward, optimizer step, postprocessing), but the cumulative effect of stacking them was not separately benchmarked.
| Regime | Projected 1.9 โ 1.10 change | How it was measured, and what limits it | Evidence |
| -------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Detection training, `batch_size=4` (default) | **~ โ23% per training step** | End to end through `RFDETR.train()` on an L4, for both `rfdetr-nano` and `rfdetr-small` | [#1409] |
| Detection training, `batch_size=16` | ~ โ9โ10% per training step | The ~50โ60 ms saving is fixed per optimizer step, so its share falls as the batch grows | [#1409] |
| Validation phase of a training run | One fewer model forward per batch; **3.28ร faster per-class COCO** | The forward saving applies to every validation batch; the 3.28ร is a per-class COCO microbenchmark | [#1380], [#1375], [#1381], [#1379], [#1373], [#1356] |
| Input-bound training, large JPEGs, โฅ16 workers | **1.25โ1.67ร** loader throughput, plus **1.41โ2.26ร** from draft decoding | Batch 16 with 8โ32 workers on a 32-vCPU L4 host ([#1399]); batch 8, resolution 512 on CPU ([#1389]) | [#1399], [#1389] |
| Training at shipped loader defaults (batch 4, 2 workers) | No projected throughput change; **crash fix only** | Deliberately conservative: [#1399]'s 3-epoch fine-tune at 8 workers was neutral because the loader already outran the GPU, and its grid never covered this regime | [#1399] |
| Segmentation training | Substantially cheaper mask loss on the guarded path | Rests on a 6.7โ7.1ร single-thread CPU microbenchmark of the full `loss_masks` call | [#1367] |
| `predict()`, PIL or uint8 NumPy, by frame size | **โ6.5% at 640ร640 to โ46.4% at 2160ร3840** (L4, Nano) | Scales with input pixel count, not model size; an RTX 4060 measured โ18.4% / โ32.3% / โ61.2% for the three largest frames | [#1415] |
| `predict()`, CUDA tensor, `include_source_image=True` | **~ โ50%** | Neutral with `include_source_image=False` | [#1388] |
| `predict()` at batch 1, any input form | About โ3.5% and about โ4.4% from two independent changes | Both shrink toward zero as the batch grows; [#1416] measured โ0.23% at batch 4 | [#1416], [#1419] |
| Segmentation `predict()` returning many masks | **โ12% CPU / โ17% CUDA** latency, โ21% RSS / โ29% peak CUDA memory | Nano and Small at `num_select=100` see +1.8% peak memory rather than a reduction | [#1374] |
| ONNX / torch-free reference decoder | โ5.2% per image on L4 CUDA; โ3.6% through the full ONNX path | Measured through the complete path including ONNX Runtime execution, which dominates the call | [#1393], [#1394] |
**What is not projected:**
- **[#1371], [#1377], [#1385], [#1369]** ship real component-level or allocation-level improvements that their own authors measured as neutral or noise-dominated end to end.
- **Multi-GPU/DDP and MPS** were not exercised for any 1.10.0 number; [#1409]'s measurements are single-GPU CUDA AdamW only.
- **Apple silicon**: none of the 1.10.0 work was measured on it.
### ๐ฉน Also shipped in the 1.9 patch line
Fourteen further performance PRs landed between the `1.9.0` tag and this release, but shipped from the separate `1.9.1`โ`1.9.4` maintenance line. **If you are upgrading directly from 1.9.0 you get these too**, but they are *not* part of the 1.10.0 delta and none of them are counted above. Full entries live in the `## [1.9.1]`โ`## [1.9.4]` sections of [CHANGELOG.md](../../CHANGELOG.md).
<details>
<summary><b>All fourteen, with their measured effect</b></summary>
| Change | Measured effect |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| Matcher cost matrix padded to `max(T_i)`, not `sum(T_i)` ([#1297], [#1281], [#1312]) | Matcher time **โ51%**, peak CUDA memory **โ73โ76%**, training step **288.364 โ 232.457 ms** on an A100 |
| ExecuTorch lowers undelegated `addmm` back into `aten.linear` ([#1262]) | `RFDETRNano` on XNNPACK / Apple silicon **~2.5ร faster**: 119.9 โ 48.3 ms median |
| `predict()` skips upsampling sub-threshold segmentation masks ([#1265]) | `predict()` **~20% faster at 1080p**, neutral at 640 px |
| `PostProcess` selects with `index_select`/`expand` ([#1268]) | Mask postprocess **2.6โ3.0ร faster**; 21โ84 MiB per-image allocation removed |
| Per-class confidence sweeps made O(N log N) instead of O(T\*N) ([#1339]) | One sort per class plus `searchsorted` replaces a full rescan per threshold |
| Matcher safety gate's target half computed once per step ([#1340]) | Precheck no longer repeats across final, auxiliary, and encoder layers |
| `predict()`'s pixel-range validation deferred off the per-image sync ([#1341]) | Later images' GPU work overlaps the range-check sync |
| Two-stage selection gathers top-k rows before the bbox-delta MLP ([#1334]) | MLP runs on at most `num_queries` rows, not every encoder position |
| Two-stage selection avoids repeated top-k gather indices ([#1278]) | Allocation removed from the selection path |
| CPU image tensors pinned before the CUDA transfer ([#1313]) | Transfer moves from pageable to pinned memory |
| Evaluation matching counts labels on the host ([#1276]) | Device-side count removed from matching |
| Keypoint decode skips redundant CUDA presence checks ([#1282]) | Repeated device checks removed from keypoint postprocess |
- The matcher entry's saving **scales with target-count evenness**: a batch where one image holds nearly all the targets sees little to none.
- The ExecuTorch outputs match the previous lowering to about 1e-4; every other entry is output-identical.
- The [#1265] saving scales with image area.
</details>
## ๐ Extensive changelog
Every user-visible change in the release, grouped by kind. The headline performance work is covered in [Performance](#-performance); what appears here and not there is the component-level work whose effect never showed up end to end.
### ๐ Added
New configuration surface and new capabilities. Most are opt-in; `pack_targets` is the exception, shipping enabled because it carries bit-identical values.
- `TrainConfig.pack_targets` (default `True`) packs per-sample target dicts into one tensor per field crossing the DataLoader worker boundary: a batch of 16 crosses as 9 objects instead of 114, bit-identical values. (#1399)
- `TrainConfig.eval_batch_size` decouples validation/test/predict batch size from training `batch_size`. (#1378)
- `TrainConfig.best_model_metric` (`"map"`/`"mar"`) ranks checkpoints and early-stopping by mAR instead of mAP. (#1305)
- Training progress bar restored/extended:
- Restored peak `max_mem`, dropped during the PTL migration. (#974)
- Live free/total GPU memory (`free_mem`). (#1314)
- Restored `train/lr`, including per-group learning rates. (#1310)
- `deploy_to_roboflow()`:
- `version` argument is now optional; it resolves the highest existing dataset version automatically. (#1116)
- Accepts `ROBOFLOW_HOME` as an alias for `RF_HOME`. (#1264)
- Kornia GPU augmentation backend gains seven ops: `ToGray`, `Blur`, `Sharpen`, `Equalize`, `CLAHE`, `Perspective`, `ShiftScaleRotate`. (#1249, #1277, #1330, #1370)
- GPU batched linear-assignment solver, backed by a new `[train]`-extra `torch-hungarian` dependency that is imported lazily. Stacking compatible decoder layers into one cost-matrix construction measured **1.45โ5.8ร on an L4** below the 350,000-element routing limit. (#1368)
### โ ๏ธ Breaking Changes
Defaults that change what an unmodified training script does. Read these before retraining an existing project; each one's restore is in the [Migration guide](#-migration-guide).
- **`grad_accum_steps` defaults to `1`** (was `4`): default effective batch size drops 16 โ 4. (#1378)
- **Validation evaluates one model per epoch**: `val/mAP_*`, `val/mAR`, `val/loss` now report the evaluated model (EMA by default) instead of always the non-EMA weights, saving **one full model forward per validation batch**. (#1380)
- **Optimizer parameter groups merge by hyperparameter**: `rfdetr-nano` goes from 465 groups to 28, and a custom `lr_scheduler_kwargs` list sized per-parameter needs resizing. (#1409)
- **Dataset builders require a complete pipeline-option namespace**: direct calls with an incomplete config now raise instead of silently training on the wrong pipeline. (#1413)
- **`log_per_class_metrics` defaults `False`** (was `True`): per-class `val/*` keys are gone from a default run, and the per-class metric work with them. (#1372)
- **`compute_val_loss` defaults `"auto"`** (was `True`): `val/loss` is computed only when a logger or callback consumes it. (#1372)
### ๐ฑ Changed
Existing behavior that moved. Everything under `predict()` is performance-only with identical detections; the Kornia augmentation defaults at the end are the one entry here that can change training results.
- `predict()`, performance-only with detections unchanged (see [Performance](#-performance) for measurements):
- Skips redundant eval-mode reassignment when the module tree is already in eval mode. (#1419)
- Transfers PIL/uint8 NumPy inputs as bytes and widens on-device instead of on host. (#1415)
- Skips padding-mask work on a batch that proved it unnecessary. (#1416)
- Fuses the CHW/dtype conversion into one allocation. (#1390)
- Converts CUDA source images to `uint8` on-device instead of on CPU. (#1388)
- Skips a now-provably-redundant pixel-range scan. (#1387)
- Single-feature-level fast paths each reuse tensors instead of re-materializing them; bit-identical outputs, and all three explicitly claim no end-to-end speedup because their whole-model timings crossed zero:
- Single-level deformable-attention packing skipped: **โ15.7โ16.9%** on the detection core and **โ14.7โ14.8%** on the keypoint core, one CPU thread. (#1385)
- Singleton-level concatenations skipped: training peak memory **5.5 MB (Nano, batch 4) / 15.9 MB (Large, batch 4)** lower. (#1377)
- Decoder's grouped query reused as the key: **โ5.44โ5.49%** on an isolated attention benchmark. (#1371)
- Evaluation work reduced across the validation epoch:
- One-pass COCO mAP adapter shared by base and EMA: **3.28ร median** on per-class computation. (#1375)
- Hoisted COCO detection score reads: **1.56ร** dataset construction, **1.68ร** annotation loop. (#1379)
- Converted targets reused for the EMA mAP update: one fewer target conversion and `orig_size` transfer per validation batch. (#1381)
- bbox IoU shared per image: repeated class-local IoU launches removed from F1 matching. (#1373)
- mAP state kept on CPU: TorchMetrics per-annotation device-to-host syncs removed. (#1356)
- Segmentation postprocessing reads mask resize targets once per batch and writes into a preallocated buffer; `loss_masks` samples matched labels via direct indexing under size/contiguity guards. (#1369, #1374, #1367)
- Training skips PTL sanity-validation batches by default, compacts per-microbatch loss metrics (17 โ 9 keys), and emits LR metrics only on optimizer updates. (#1360)
- Oversized JPEGs draft-decoded; NumPy export kernels (resize, top-k) made allocation-free; matcher host transfers batched into **one cost-matrix transfer and sync per training step rather than per decoder layer**. (#1389, #1394, #1393, #1361)
- Kornia `GaussianBlur`/`GaussNoise` defaults changed to match Albumentations, which **silently changes augmentation strength** for configs that omit these params on the GPU backend. (#1395)
### ๐๏ธ Deprecated
Both still work and both warn. Replace them now rather than at the removal version named beside each.
- `rfdetr.datasets.aug_config` shim, **removal in v1.12.0**. Use `rfdetr.datasets.aug_configs` (plural). (#1398)
- `TrainConfig.eval_ema_only`, **removal in v1.13**. Superseded by `eval_base_model`. (#1380)
### ๐ง Fixed
Bug fixes, mostly in target handling and dataset acquisition.
- Packed targets materialize directly into per-sample device tensors, removing a transient CUDA allocation. (#1405)
- Empty COCO targets keep `iscrowd`/`area` dtypes matching populated targets, enabling lossless packed-target transport for mixed batches. (#1404)
- `compile=True` no longer aborts training on PyTorch 2.2+: `spatial_shapes` is built from Python ints under compilation, which Dynamo can trace. (#1411)
- Kornia `CLAHE` reads a scalar `clip_limit` as a range, matching Albumentations. (#1350)
- Corrupt COCO zip downloads are now retried instead of failing the dataset build outright. (#1306)
## ๐ Contributors
Everyone who landed a commit in the `1.9.0..HEAD` range for this release. GitHub handles come from merged-PR author data; a handle is omitted where a commit had no separate PR (co-authored or squashed into another contributor's PR).
- **Jesรบs Royeth** (@JESUSROYETH, [LinkedIn](https://www.linkedin.com/in/jesusroyeth/)): the bulk of this release's perf and correctness work: optimizer parameter-group merge, packed-target transport, `predict()` host/device transfer optimizations, one-pass COCO mAP, GPU linear-assignment solver, and the padding-skip fast path.
- **Borda** (@Borda, [LinkedIn](https://www.linkedin.com/in/jirka-borovec/)): release coordination, GPU memory progress-bar restoration, deploy-to-Roboflow version auto-resolution, and this release's own prep.
- **Jonathan Jesni Manissery** (@Jonathan-Jesni): Kornia GPU augmentation backend (ToGray, Blur, Sharpen, Equalize, CLAHE, Perspective, ShiftScaleRotate).
- **Tamil Adhavan S K** (@adhavan18, [LinkedIn](https://www.linkedin.com/in/tamiladhavan)): YOLO test-split evaluation fixes and `metrics.csv` resume-history preservation.
- **Atikul Islam Munna** (@atikulmunna, [LinkedIn](https://www.linkedin.com/in/aimunna/)): mypy-strict typing sweeps across several modules.
- **arubittu**: mypy-strict typing and doctest coverage additions.
- **Aman Harsh** (@amanharshx, [LinkedIn](https://www.linkedin.com/in/amanharshx/)): contributed to the docs and CI surface.
- **Isaac Robinson** (@isaacrob, [LinkedIn](https://www.linkedin.com/in/robinsonish/)): TFLite/ONNX export and inference reference-decoder fixes.
- **Maryyyyyyyam142** (@Maryyyyyyyam142): dataset builders now require a complete pipeline-option namespace instead of silently substituting wrong defaults.
- **Vedanshu Joshi** (@Vedanshu7): corrupt COCO zip download retry logic.
- **Roshan Sharma** (@roshaninfordham): training metric plot legend fix.
- **adenstamm**: CLAHE `clip_limit` scalar-as-range fix.
- **Sahil Mehta** (@sahilmehta17): contributed to the export/inference surface.
- **Jakub Chmura** (@chmjkb): contributed to the augmentation pipeline.
- **Flo** (@flhoxha): contributed to the export pipeline.
- **FootysHands** (@ayo0la): contributed to the training callbacks.
- **unaxEtxeberriaBieleDigital**: Albumentations flip-alias handling (`TimeReverse`/`SquareSymmetry`).
- **Vlad Voropaev** (@voropaevv, [LinkedIn](https://www.linkedin.com/in/thevladvoropaev/)): contributed to the CUDA device-handling path.
- **LeMinhNgan**: contributed to the docs/CI surface.
- **aryan kolapkar**: contributed to this release.
Excluded as non-human: `app/copilot-swe-agent`, `app/pre-commit-ci`, and co-author trailers for Codex/Copilot pair-programming tools.
---
**Full changelog**: https://github.com/roboflow/rf-detr/compare/1.9.0...1.10.0
[#1262]: https://github.com/roboflow/rf-detr/pull/1262
[#1265]: https://github.com/roboflow/rf-detr/pull/1265
[#1268]: https://github.com/roboflow/rf-detr/pull/1268
[#1276]: https://github.com/roboflow/rf-detr/pull/1276
[#1278]: https://github.com/roboflow/rf-detr/pull/1278
[#1281]: https://github.com/roboflow/rf-detr/pull/1281
[#1282]: https://github.com/roboflow/rf-detr/pull/1282
[#1297]: https://github.com/roboflow/rf-detr/pull/1297
[#1312]: https://github.com/roboflow/rf-detr/pull/1312
[#1313]: https://github.com/roboflow/rf-detr/pull/1313
[#1334]: https://github.com/roboflow/rf-detr/pull/1334
[#1339]: https://github.com/roboflow/rf-detr/pull/1339
[#1340]: https://github.com/roboflow/rf-detr/pull/1340
[#1341]: https://github.com/roboflow/rf-detr/pull/1341
[#1356]: https://github.com/roboflow/rf-detr/pull/1356
[#1367]: https://github.com/roboflow/rf-detr/pull/1367
[#1369]: https://github.com/roboflow/rf-detr/pull/1369
[#1371]: https://github.com/roboflow/rf-detr/pull/1371
[#1373]: https://github.com/roboflow/rf-detr/pull/1373
[#1374]: https://github.com/roboflow/rf-detr/pull/1374
[#1375]: https://github.com/roboflow/rf-detr/pull/1375
[#1377]: https://github.com/roboflow/rf-detr/pull/1377
[#1379]: https://github.com/roboflow/rf-detr/pull/1379
[#1380]: https://github.com/roboflow/rf-detr/pull/1380
[#1381]: https://github.com/roboflow/rf-detr/pull/1381
[#1385]: https://github.com/roboflow/rf-detr/pull/1385
[#1387]: https://github.com/roboflow/rf-detr/pull/1387
[#1388]: https://github.com/roboflow/rf-detr/pull/1388
[#1389]: https://github.com/roboflow/rf-detr/pull/1389
[#1390]: https://github.com/roboflow/rf-detr/pull/1390
[#1393]: https://github.com/roboflow/rf-detr/pull/1393
[#1394]: https://github.com/roboflow/rf-detr/pull/1394
[#1399]: https://github.com/roboflow/rf-detr/pull/1399
[#1409]: https://github.com/roboflow/rf-detr/pull/1409
[#1415]: https://github.com/roboflow/rf-detr/pull/1415
[#1416]: https://github.com/roboflow/rf-detr/pull/1416
[#1419]: https://github.com/roboflow/rf-detr/pull/1419