1.9.3
windmill-labs/windmill1.9.3Aug 17, 2026by Borda
AI Summary
Fixes bugs in YOLO test split evaluation, ONNX export dropping multi-label detections, and CUDA sync blocking, with improvements to deterministic tie-breaking.
Key Highlights
- `predict()` no longer blocks on per-image CUDA sync for CUDA tensors
- YOLO-format `evaluate(split="test")` now evaluates real test split
- ONNX/TFLite export no longer drops multi-label detections
- `PostProcess` selection is deterministically tie-broken
- `batch_size="auto"` now accounts for AdamW optimizer state memory
Breaking Changes
- `PostProcess` tie-breaking changed (stable argsort vs topk)
- `RFDETR.predict()` shape-validation errors changed type (RuntimeError -> ValueError)
- `RFDETREMACallback.on_train_epoch_end` is now a no-op
Full Release Notes
RF-DETR 1.9.3 is a maintenance release: no breaking changes, no new top-level public API. It fixes ten user-facing bugs and improves five behavior refinements, spanning training (YOLO test-split evaluation, auto-batch memory estimation, resumed-run metrics history, EMA epoch-boundary double-counting), inference (`predict()` no longer blocks per-image on a CUDA sync for CUDA-tensor inputs), export (ONNX/TFLite no longer silently drop multi-label detections, ONNX Runtime benchmark now honors `--device`), and segmentation (`skip_blocks` path now applies the learned feature projection it was missing during training). Users training on YOLO-format datasets with a `test` split, exporting to ONNX/TFLite, or relying on `batch_size="auto"` benefit most from this release. Callers passing CUDA tensors directly to `predict()` get a small inference speedup from the sync fix; everyone should note the `PostProcess` tie-breaking change described below — non-breaking, but worth a glance if code depends on exact output order. ## ✨ Spotlights / highlights ### `predict()` no longer blocks on a per-image CUDA sync for CUDA-tensor inputs ([#1341](https://github.com/roboflow/rf-detr/pull/1341)) `RFDETR.predict()`'s `[0, 1]` pixel-range validation used to sync the host after every single image. For callers passing CUDA tensors directly (the host-round-trip-free path), range-check results are now collected unsynced across the whole batch and resolved once, so later images' GPU work can overlap the sync from earlier ones. File-path and PIL inputs go through a CPU tensor conversion first and were never blocked by this sync, so they see no change here. ```python from rfdetr import RFDETRSmall model = RFDETRSmall() cuda_images = [img.cuda() for img in preprocessed_tensors] # same call, same output — just no longer stalls the host once per CUDA-tensor image detections = model.predict(cuda_images) ``` ### YOLO-format `evaluate(split="test")` now evaluates the real test split ([#1329](https://github.com/roboflow/rf-detr/pull/1329), [#1343](https://github.com/roboflow/rf-detr/pull/1343)) Previously, requesting the `test` split on a YOLO-format dataset silently evaluated `valid/` instead. Now it resolves the real `test/` split when present, falls back to `valid/` with a logged warning when the split is genuinely absent, and raises when a `test` path is declared in `data.yaml` but unresolvable or invalid. ```python model.train(dataset_dir="my_yolo_dataset", ...) model.evaluate(split="test") # now evaluates test/, not valid/ ``` ### `PostProcess` selection is now deterministically tie-broken ([#1320](https://github.com/roboflow/rf-detr/pull/1320)) Detection selection switched from `torch.topk` to a stable `argsort`, so ties resolve the same way every run: descending score, then ascending flattened query/class index — the same rule now shared with the torch-free export decoders (both sides changed together in this PR). See the migration guide below if your code depends on tie order. ### ONNX/TFLite export no longer silently drops multi-label detections ([#1320](https://github.com/roboflow/rf-detr/pull/1320)) The ONNX and TFLite reference decoders used a per-query `argmax`, keeping at most one class per query — any query scoring above threshold on more than one class quietly lost the rest. Both decoders now flatten `(queries, classes)` pairs and take the top-scoring ones before thresholding, mirroring `PostProcess`'s own selection rule. ### `batch_size="auto"` now accounts for AdamW's optimizer-state memory ([#1342](https://github.com/roboflow/rf-detr/pull/1342)) The auto-batch probe previously ignored AdamW's `exp_avg`/`exp_avg_sq` state, so the probed batch size could overshoot real training memory and OOM on the first optimizer step. It now models that state via a shadow optimizer and warns when a non-AdamW optimizer is configured (the estimate may then be too conservative *or* too permissive, since it no longer directly applies). ## 🔄 Migration guide No breaking changes in this release. Three behavior changes are worth a second look — none require a code change to keep working: - **`PostProcess` detection ordering may shift on score ties.** Selection now uses a stable `argsort` instead of `torch.topk`, so ties resolve by ascending flattened query/class index instead of `topk`'s implementation-defined order. Same detections, same scores — only the order among exact ties can differ, so `detections[0]` may point at a different (but equally-scored) box than before. Ordering among tied scores was never a documented contract. `PostProcess(num_select=<negative>)` now raises `ValueError` at construction instead of being silently accepted — if you were relying on that being a no-op, pass a non-negative value instead. (This selection now sorts the full flattened query/class score axis instead of doing a partial top-k; unmeasured this release, but callers with very large query/class counts on the hot inference path may want to benchmark their own workload.) - **`RFDETR.predict()` shape-validation errors changed type.** Passing a malformed-rank input together with `include_source_image=True` now raises a public `ValueError` with a shape message, where it previously surfaced an internal `RuntimeError` from `permute()`. If you were catching `RuntimeError` around this call, catch `ValueError` instead (or both, for compatibility across versions). - **`RFDETREMACallback.on_train_epoch_end` no longer exists as an override point.** If you subclass `RFDETREMACallback` and rely on its own `on_train_epoch_end` running (or override it yourself), note the epoch-boundary EMA update it performed is gone (that's the #1319 fix above) — calls to `super().on_train_epoch_end(...)` still resolve fine via PyTorch Lightning's base `Callback`, but as a no-op rather than an EMA update. ## 📝 Notable changes ### 🌱 Changed - `HungarianMatcher`'s compact-path safety gate now computes its target-side half (box/label finiteness checks) once per training step instead of once per `matcher()` call. `SetCriterion.forward` invokes `matcher()` separately for the final layer, each auxiliary decoder layer, and the encoder layer with the same `targets`, so the target-side precheck is now precomputed once and reused across all of them, keyed on `targets` object identity plus `pred_boxes` dtype/device and `num_classes` (a mismatch triggers a fresh computation). Matching results are unchanged. Callers must not mutate `targets` in place between precompute and reuse — the identity check cannot detect that. ([#1340](https://github.com/roboflow/rf-detr/pull/1340)) - Per-class confidence-threshold sweeps in evaluation are now O(N log N) instead of O(T·N): one stable ascending sort per class plus `np.searchsorted` into precomputed suffix sums replaces a full rescan per threshold. NaN scores are explicitly masked so they never count as "above threshold". Results are unchanged. ([#1339](https://github.com/roboflow/rf-detr/pull/1339)) - `RFDETR.predict()` no longer blocks the host on a per-image CUDA sync for its `[0, 1]` pixel-range validation. The range-check tensors are now collected unsynced across all images and resolved to Python booleans once, after every image's conversion, range check, and transfer have already been queued, so later images' GPU work can overlap the sync. Error-message precedence per image is unchanged. A malformed-rank input combined with `include_source_image=True` now raises a public `ValueError` with a shape message, where it previously surfaced an internal `RuntimeError` from `permute()`. ([#1341](https://github.com/roboflow/rf-detr/pull/1341)) - `Transformer.forward`'s two-stage query selection now gathers the `torch.topk`-selected rows *before* running the bbox-delta MLP (`enc_out_bbox_embed`), not after — the MLP is pointwise with no cross-token mixing, so it only ever needs at most `num_queries` rows that survive selection, not every one of the `sum(H*W)` encoder positions. ([#1334](https://github.com/roboflow/rf-detr/pull/1334)) - `PostProcess` box/mask/keypoint selection is now deterministically tie-broken: `torch.argsort(..., stable=True)` plus a slice replaces `torch.topk`, so ties resolve by descending score then ascending flattened query/class index — the same rule now shared with the torch-free export decoders (both sides changed together in this PR). Output *ordering* may differ from 1.9.2 when scores tie (same detections, different order; `detections[0]` may change), but ordering among equal scores was never contractual. `PostProcess(num_select=<negative>)` now raises `ValueError` at construction instead of being silently accepted. ([#1320](https://github.com/roboflow/rf-detr/pull/1320)) ### 🔧 Fixed - YOLO-format `evaluate(split="test")` now evaluates the real `test/` split, where it previously silently fell back to `valid/`. When no resolvable `test` split exists, it falls back to `valid/` with a logged warning; when a `test` path is declared in `data.yaml` but unresolvable or invalid, it raises instead of silently relabeling as validation data. COCO-format datasets are unaffected — they never attempt a `test` fallback. ([#1329](https://github.com/roboflow/rf-detr/pull/1329), [#1343](https://github.com/roboflow/rf-detr/pull/1343)) - `metrics.csv` training history now survives a resumed run instead of being wiped on the first write after resume. ([#1325](https://github.com/roboflow/rf-detr/pull/1325)) - `SegmentationHead`'s `skip_blocks` branch now applies the learned `spatial_features_proj` projection, matching the non-skip branch — the projection was being skipped entirely. Affects segmentation training loss trajectories only (encoder auxiliary mask supervision); the export path already applied this projection unconditionally before this change, and `predict()` outputs and exported models are unchanged. ([#1331](https://github.com/roboflow/rf-detr/pull/1331)) - Non-finite keypoint predictions no longer poison the shared box-head gradient via `ref_wh`, in both the decoder and encoder branches. The keypoint loss itself now also masks out non-finite predicted keypoints and non-finite target areas rather than letting them poison the loss. Not yet covered: the matcher's own keypoint cost still lacks the equivalent guard. ([#1336](https://github.com/roboflow/rf-detr/pull/1336)) - `batch_size="auto"` now models AdamW's optimizer-state memory in its probe, preventing an OOM on the first optimizer step when the probed batch size previously overshot real training memory. ([#1342](https://github.com/roboflow/rf-detr/pull/1342)) - ONNX Runtime benchmark now honors the requested `--device`, instead of always binding GPU 0. ([#1346](https://github.com/roboflow/rf-detr/pull/1346)) - Training metric plots now draw a legend on every subplot, not just the loss subplot. ([#1335](https://github.com/roboflow/rf-detr/pull/1335)) - ONNX/TFLite reference decode now mirrors `PostProcess`'s multi-label selection instead of a per-query `argmax`, which silently dropped legitimate detections whenever a query scored above threshold on more than one class. ([#1320](https://github.com/roboflow/rf-detr/pull/1320)) - EMA no longer double-counts the last training step of every epoch, which previously bypassed `ema_update_interval`. ([#1319](https://github.com/roboflow/rf-detr/pull/1319)) - `model.export(format="tflite")` no longer hangs at the ONNX→TFLite step (an Abseil weak-symbol coalescing issue between the `onnx` C extension and TensorFlow). ([#1323](https://github.com/roboflow/rf-detr/pull/1323)) --- ## 🏆 Contributors - **adenstamm** (@adenstamm) — fixed missing legends on training metric plots. - **Aman Harsh** (@amanharshx, [LinkedIn](https://www.linkedin.com/in/amanharshx/)) — fixed a TFLite export hang by preloading TensorFlow before ONNX. - **aryan kolapkar** (@arubittu) — enabled strict mypy checking across the datasets and namespace modules. - **Flo** (@flhoxha) — fixed YOLO-format datasets to evaluate the real test split. - **FootysHands** (@ayo0la) — fixed `metrics.csv` history being lost across resumed training runs. - **Jesús Royeth** (@JESUSROYETH) — the release's most prolific contributor this cycle: the `predict()` CUDA-sync fix, the matcher target-side safety-gate caching, the YOLO test-split fallback, AdamW-aware auto-batch probing, the O(N log N) confidence-threshold sweep, the keypoint/`ref_wh` non-finite guard, the two-stage top-k gather-order fix, the `SegmentationHead` `skip_blocks` fix, the ONNX/TFLite multi-label decode fix, and the EMA epoch-boundary double-count fix. - **jirka** (@Borda, [LinkedIn](https://linkedin.com/in/jirka-borovec)) — pre-commit and documentation maintenance. - **Vlad Voropaev** (@voropaevv, [LinkedIn](https://www.linkedin.com/in/thevladvoropaev/)) — fixed the ONNX Runtime benchmark to honor the requested CUDA device. --- **Full changelog**: https://github.com/roboflow/rf-detr/compare/1.9.2...1.9.3