1.9.2

expressjs/express1.9.2Aug 11, 2026by Borda

AI Summary

A fix-and-performance release featuring a breaking change for hierarchical COCO datasets and significant optimizations to the HungarianMatcher for better speed and memory usage.

Key Highlights

  • Hierarchical COCO datasets now get a correct, shared label mapping
  • HungarianMatcher is ~51% faster and uses ~73% less peak CUDA memory
  • Training resume from `BestModelCallback` now restores per-callback state
  • Rich epoch progress bar is no longer corrupted by log calls

Breaking Changes

  • Hierarchical COCO label filtering: unannotated grouping categories no longer consume a label slot

Full Release Notes

RF-DETR 1.9.2 is a fix-and-performance release: one breaking change, no new public APIs. Hierarchical COCO datasets — most commonly Roboflow exports carrying a synthetic unannotated grouping category — now get a correct, shared label mapping across `train`/`valid`/`test`, closing a case where per-class metrics could silently corrupt on the smaller split. The matcher's compact cost-matrix path is faster and lower-memory on real batches, training-resume from `BestModelCallback`'s lightweight checkpoints now restores per-callback state instead of restarting cold, and a handful of training/eval fixes round it out.

## ✨ Spotlights / highlights

### Breaking: hierarchical COCO datasets get a correct, shared label space

Roboflow COCO exports prepend a synthetic root category (unannotated, `supercategory: "none"`) that every real class lists as its own `supercategory`. It never carried annotations but still consumed a label slot — training such a dataset built an *N+1*-class head instead of *N*.

```python
from rfdetr.datasets.coco import filter_parent_categories, annotated_category_ids

categories = [
    {"id": 0, "name": "project-root", "supercategory": "none"},
    {"id": 1, "name": "car", "supercategory": "project-root"},
    {"id": 2, "name": "truck", "supercategory": "project-root"},
]
anns = {"annotations": [{"category_id": 1}, {"category_id": 2}]}

kept = filter_parent_categories(categories, annotated_category_ids(anns))
[c["name"] for c in kept]
# -> ['car', 'truck']  — the unannotated root no longer takes a label slot
```

For datasets where a *parent* category does carry its own annotations, label indices are now derived once from the `train` split and shared into `valid`/`test`, so a grouping category annotated in one split but not another no longer shifts that split's label indices independently of the others.

> ⚠️ **Checkpoints trained before 1.9.2 keep their old head width and label ordering.** Evaluating one against a re-filtered dataset misaligns per-class metrics (an existing `UserWarning` fires). See the migration notes below.

### Matcher: faster and lower-memory on real COCO batches

`HungarianMatcher`'s detection-only cost matrix is now built padded to each batch's `max(T_i)` target count instead of the cross-image `sum(T_i)`, when a fast eligibility check passes — identical results on eligible batches, automatic full-computation fallback otherwise.

|                      | before     | after      | change        |
| -------------------- | ---------- | ---------- | ------------- |
| Matcher time         | —          | —          | ~51% faster   |
| Peak CUDA memory     | —          | —          | ~73–76% lower |
| Training step (A100) | 288.364 ms | 232.457 ms | —             |

The saving scales with target-count evenness across a batch — a batch where nearly all targets sit in one image sees little to no improvement.

### Training resume restores per-callback state

Resuming from one of `BestModelCallback`'s four lightweight checkpoints (`checkpoint_best_regular.pth`, `checkpoint_best_ema.pth`, `checkpoint_best_total.pth`, `last_ema.pth`) previously restarted best-score tracking, EMA, and early-stopping cold, silently. It now restores that state:

```python
model.train(
    dataset_dir="my_dataset",
    resume="output/checkpoint_best_ema.pth",
    output_dir="output",  # must match the original run for best-score restore
)
```

## 📝 Notable changes

### 🌱 Changed

- **`HungarianMatcher` compact-path speedup** — detection-only cost matrix padded to `max(T_i)` instead of `sum(T_i)`; ~51% faster matcher, ~73–76% lower peak CUDA memory on real COCO batches. Compact path also copies only diagonal cost blocks to CPU and batches its safety-gate sweeps into one sync. (#1297, #1281, #1312)
- **Deterministic-algorithm coverage** — `seed_all()` now also enables `torch.use_deterministic_algorithms(True, warn_only=True)`; ops without a deterministic kernel warn at execution time instead of raising. (#1307)
- **`predict()`** — pins CPU image tensors before the CUDA transfer. (#1313)
- **Two-stage query selection** — avoids materialising repeated top-k gather indices. (#1278)
- **Evaluation matching** — counts labels on host instead of device. (#1276)
- **Keypoint postprocessing** — skips redundant CUDA presence checks. (#1282)

### 🔧 Fixed

- **`BestModelCallback` resume** — resuming from a lightweight checkpoint (`checkpoint_best_*.pth`, `last_ema.pth`) now restores per-callback state instead of restarting best-score tracking, EMA, and early-stopping cold; a warning distinguishes checkpoints that intentionally omit optimizer state from ones that predate callback-state persistence entirely. (#1318)
- **Rich epoch progress bar** — no longer corrupted or duplicated by training-time log calls under `RichProgressBar(leave=True)`. (#1316)
- **`_kp_active_mask` false-positive warning** — loading a pre-keypoint-support checkpoint no longer warns about a deterministic schema buffer as a missing parameter. (#1302)
- **Deferred CUDA-device move** — an index-less `torch.device("cuda")` is normalised to the current device index before comparison, fixing spurious re-moves of every parameter on every call. (#1311)
- **Legacy query-embedding fallback** — only warns when it actually truncates weights. (#1301)
- **`eval_ema_only`** — no longer logs an empty validation pass when the base metric is empty; EMA metrics (`val/ema_mAP_50_95`, `val/ema_mAP_50`, `val/ema_mAR`, per-class AP, `val (ema)` summary table) are computed instead, and `val/F1` is no longer silently dropped. (#1289)
- **`ModelContext.reinitialize_detection_head`** — raises a clear `RuntimeError` instead of `AttributeError: 'NoneType'` after `RFDETR.inference(inplace=True)` clears the weights. (#1283)
- **`evaluate()`** — builds its datamodule from the resolution-override config. (#1280)

### ⚠️ Breaking Changes

- **Hierarchical COCO label filtering** — unannotated grouping categories no longer consume a label slot; `train`/`valid`/`test` now share one label mapping derived from `train`. Checkpoints trained before this change need retraining against the new label space — see [Migration guide](MIGRATION.md). (#1303)

## 🏆 Contributors

- **@JESUSROYETH** — matcher perf (padded cost matrix, diagonal-only CPU copy, batched safety-gate sync), `BestModelCallback` resume-state restore, Rich progress-bar fix, `eval_ema_only` fix, and five other training/inference fixes
- **@atikulmunna** ([LinkedIn](https://www.linkedin.com/in/aimunna/)) — guarded `ModelContext.reinitialize_detection_head` against cleared weights; strict-typed `rfdetr.detr` and `datasets.yolo` module boundaries
- **@Jonathan-Jesni** — strict-typed `rfdetr.datasets.save_grids` module boundary
- **@Borda** ([LinkedIn](https://linkedin.com/in/jirka-borovec)) — hierarchical COCO label-filtering fix and release maintenance

---

**Full changelog**: https://github.com/roboflow/rf-detr/compare/1.9.1...1.9.2