Skip to content

v1.10.0: Faster Training & Inference

Choose a tag to compare

@Borda Borda released this 04 Sep 10:27
· 18 commits to develop since this release
0f432b6

🎉 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 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. (#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, 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.

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.

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.

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.

# 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.

# 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

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 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.

👀 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.11.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.

🏋️ 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.11.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.

All fourteen, with their measured effect
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.

📝 Extensive changelog

Every user-visible change in the release, grouped by kind. The headline performance work is covered in 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.

  • 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 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): 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): 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): YOLO test-split evaluation fixes and metrics.csv resume-history preservation.
  • Atikul Islam Munna (@atikulmunna, LinkedIn): mypy-strict typing sweeps across several modules.
  • arubittu: mypy-strict typing and doctest coverage additions.
  • Aman Harsh (@amanharshx, LinkedIn): contributed to the docs and CI surface.
  • Isaac Robinson (@isaacrob, LinkedIn): 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): 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: 1.9.0...1.10.0