Skip to content

v1.11.1: Correctness Fixes for LoRA, TensorRT & Crowd mAP

Choose a tag to compare

@Borda Borda released this 30 Sep 11:17
· 16 commits to release/latest since this release

RF-DETR 1.11.1 is a correctness patch. It turns silent wrong results into loud, correct ones:

  • LoRA backbone — backbone_lora=True checkpoints trained on a random encoder, no error. Now loads correctly.
  • TensorRT inference — wrong-device/dtype/non-contiguous inputs returned wrong detections, no error. Now refused.
  • Crowd-region mAP — val/test mAP overcounted false positives on COCO crowd regions. Now matches pycocotools.

Most users need no action. 4 of the 5 "breaking" changes only affect code that was already getting a wrong result or an uncaught KeyError — this release just stops hiding it. See the Migration guide for exact before/after code.

The one real API change: RFDETRModelModule.clip_gradients → configure_gradient_clipping. Only affects code that calls clip_gradients directly, or relies on super().clip_gradients(...) inside an override.

Everything else is a bug fix or documentation update.

✨ Spotlights

backbone_lora=True checkpoints silently trained a random encoder — now fixed

RFDETR.from_checkpoint(), RFDETRSmall(pretrain_weights=...), and train() all loaded a LoRA checkpoint before wrapping the encoder with LoRA, so none of the stored PEFT-named weights matched — the encoder silently stayed at random initialization, and evaluate() scored near zero with no error.

# Before (1.11.0): silent — encoder never actually reloads
model = RFDETRLarge(pretrain_weights="checkpoint_best_total.pth", backbone_lora=True)
model.evaluate()  # mAP50:95 ≈ 0.0001 — looks broken, no exception

# After (1.11.1): encoder is wrapped before load, weights match
model = RFDETRLarge(pretrain_weights="checkpoint_best_total.pth", backbone_lora=True)
model.evaluate()  # mAP50:95 ≈ 0.6221 — same as the in-memory trained model

Measured: RF-DETR Large @640, LoRA-tuned on 262 COCO person images. (#1543)

last.ckpt and interval checkpoints now carry enough to reload

# Before (1.11.0)
RFDETR.from_checkpoint("output/last.ckpt")  # KeyError: 'args'

# After (1.11.1)
model = RFDETR.from_checkpoint("output/last.ckpt")  # rebuilds trained resolution/architecture

Files written by 1.11.0 and earlier can't be repaired in place — from_checkpoint now raises a named ValueError pointing at checkpoint_best_ema.pth and training_config.json instead. (#1561)

Val/test mAP now matches pycocotools on COCO crowd regions

Detections inside iscrowd=1 regions no longer count as false positives. Numbers shift on crowd-labelled data — Nano COCO val2017: 0.4802 → 0.4842 mAP. Training is unchanged. (#1532)

TRTInference refuses inputs TensorRT would misread

# Before (1.11.0): silent wrong output
trt_model(image_tensor.to(memory_format=torch.channels_last))  # 13 detections → 0, no error

# After (1.11.1): explicit refusal
trt_model(image_tensor.to(memory_format=torch.channels_last))
# ValueError: Input 'input' is not contiguous (strides (...)), and TensorRT reads it as dense
# row-major memory. Pass tensor.contiguous().

(#1546)

RFDETRModelModule.clip_gradients renamed → configure_gradient_clipping (Breaking)

# Before (1.11.0)
class MyModule(RFDETRModelModule):
    def clip_gradients(self, optimizer, gradient_clip_val=None, gradient_clip_algorithm=None): ...


# After (1.11.1)
class MyModule(RFDETRModelModule):
    def configure_gradient_clipping(self, optimizer, gradient_clip_val=None, gradient_clip_algorithm=None): ...

Moves the keypoint fp16 clipping fix onto the Lightning hook meant for it — clip_gradients itself now reverts to Lightning's base implementation. Narrower than it looks: configure_gradient_clipping's non-fused branch calls self.clip_gradients(...) (dynamic dispatch, not super()), so a subclass overriding only clip_gradients with self-contained logic still runs unchanged. Actually affected: code calling clip_gradients directly, a subclass override relying on super().clip_gradients(...) for RF-DETR's old fused-aware logic, and any clip_gradients override on the fused-AdamW path (never reached there, in 1.11.0 or 1.11.1 — see MIGRATION.md for the full breakdown). (#1537, #1549)

🔄 Migration guide

5 breaking changes — 1 API rename, 3 stricter-validation changes, 1 exception-type change. All are covered in full, with before/after code, in MIGRATION.md; none were previously documented in docs/getting-started/migration.md.

A new overridable classmethod, Exporter.check_dependencies (src/rfdetr/export/base.py), lets a custom exporter subclass judge whether the host has what it needs to build, separately from _check_capabilities judging the request. Not documented yet in docs/exports/blueprint.md — only relevant to third-party Exporter subclasses, no action needed for RFDETR.export() callers. (#1542)

📝 Notable changes

🚀 Added

  • rfdetr.export._benchmark (private): measure_latency and measure_memory helpers shared by the new per-hardware export cookbooks (CUDA-event timing on GPU, wall-clock/RSS otherwise). Replaces the ONNX-only rfdetr.export._onnx.inference._onnx_runtime, which is removed. (#1530)
  • decode_pil_image / decode_pil_image_bytes (rfdetr.datasets.io_utils) — decode straight to a PIL image under the same decoder policy as the array-out decoders, without the NumPy round trip. (#1551)
  • CrowdRegion / crowd_regions_from_coco (rfdetr.datasets.coco) — reads iscrowd=1 annotations back from a COCO API for evaluation metrics that need to score them. (#1532)
  • resize_masks_nearest (rfdetr.evaluation.matching) — nearest-neighbor mask resize used by the new crowd-region evaluation path. (#1532)
  • rfdetr.utilities.package (new module): is_installed(name) — checks whether a package is importable without importing it, used by the new fail-fast TensorRT dependency check. (#1542)
  • Exporter.check_dependencies (src/rfdetr/export/base.py) — new overridable classmethod hook; see Migration guide. (#1542)

⚠️ Breaking Changes

  • RFDETRModelModule.clip_gradients is renamed to configure_gradient_clipping; clip_gradients reverts to Lightning's base implementation. Only affects direct callers of clip_gradients, a super().clip_gradients(...) call inside an override, or an override on the fused-AdamW path — a self-contained clip_gradients override still runs on the non-fused path via self.clip_gradients(...) dispatch. See MIGRATION.md. (#1537, #1549)
  • TRTInference now raises ValueError for an input on another device, of another dtype, or non-contiguous, instead of returning wrong detections. (#1546)
  • TensorRTExporter.build_engine now raises ValueError on a dynamic_batch=True ONNX unless its TensorRTConfig sets dynamic_batch=True and max_batch_size. RFDETR.export() is unaffected. (#1542)
  • RFDETR.from_checkpoint() on a checkpoint with no "args" now raises ValueError instead of KeyError: 'args'. (#1561)
  • Loading a backbone_lora=True checkpoint without peft installed now raises ImportError instead of silently loading a random encoder. (#1543)

🌱 Changed

  • Export cookbooks restructured per hardware target (export-cuda/export-cpu/export-mobile/ export-apple), replacing 4 per-format ones; adds first coverage for ONNX, OpenVINO, LiteRT, CoreAI. (#1530)
  • RFDETR.from_checkpoint() no longer restores device from model_config — the loading host's default wins; pass device= explicitly. (#1534, #1561)
  • [coreai] extra pins coreai-torch==0.4.3, widens Python range to <3.15, and declares a uv conflict with [tflite]. (#1527)
  • The e2e_onnxruntime pytest marker is renamed e2e_onnx in pyproject.toml. Contributor-facing only: pytest -m e2e_onnxruntime now silently selects nothing (pytest doesn't error on an unmatched -m) — update local invocations and CI configs to e2e_onnx. (#1530)
  • amp_dtype="auto" now picks fp16 on CUDA GPUs without native bf16 (T4, V100); bf16 only when every selected GPU is Ampere or newer. Loss curves/checkpoints across this change are not comparable on pre-Ampere GPUs. (#1538)
  • Val/test mAP now ignores detections on COCO crowd regions, matching pycocotools. (#1532)
  • TrainConfig.clip_max_norm now rejects a negative value at construction instead of silently training unclipped. (#1537, #1549)

❌ Removed

  • rfdetr.export._onnx.inference._onnx_runtime (private ONNX-only benchmark helper) — superseded by rfdetr.export._benchmark. (#1530)

🔧 Fixed

  • YOLO datasets stored under a directory named images now resolve their labels directory correctly. (#1557)
  • last.ckpt/interval checkpoints record model class, config, class names, and rfdetr version; from_checkpoint no longer loses the dataset's class names. (#1561)
  • COCO training on non-JPEG images (or JPEG without simplejpeg) is back to 1.10 decode speed — up to 27% faster per batch on large BMP files. (#1551)
  • format="coreai" works with coreai-torch 0.4.3. (#1527)
  • from_checkpoint("checkpoint_best_total.pth") rebuilds the trained architecture instead of falling back to class defaults. (#1534)
  • Keypoint fp16 training clips the real gradient (was clipping before GradScaler unscale, silently stalling training). (#1537, #1549)
  • amp_dtype="auto" no longer selects emulated bf16 on T4/V100 — Colab T4 Nano step 443ms → 226ms in fp16. (#1538)
  • Crowd-region detections no longer counted as false positives in COCO eval. (#1532)
  • TRTInference.build_engine no longer truncates an existing .trt file on a failed build. (#1546)
  • An undeserializable TensorRT engine reports a named RuntimeError instead of AttributeError. (#1546)
  • TRTInference(device=...) now loads and runs the engine on that device (was mixing GPUs). (#1546)
  • TensorRT export refusal messages name working export settings instead of a bare flag. (#1546)
  • backbone_lora=True checkpoints reload their trained backbone (was silently random-init; measured val mAP50:95 0.0001 → 0.6221). (#1543)
  • RFDETR.export(format="tensorrt") without tensorrt installed fails before the ONNX export with a named ImportError. (#1542)
  • compile=True with default multi_scale stops recompiling the transformer per resolution. (#1539)
  • COCO segmentation datasets mixing annotated and box-only instances keep every mask. (#1558)
  • Fixed JSON serialization in the COCO val-subset builder. (fd3bb991, no PR — direct commit)

📚 Documentation

  • Documented TrainConfig.eval_backend in the advanced training guide. (#1526)
  • Scoped the static INT8 export limit to TFLite specifically. (#1559)

🏆 Contributors

  • Jirka Borovec (@Borda, LinkedIn) — restructured export cookbooks by hardware target, replacing four per-format notebooks; fixed JSON serialization in the COCO val-subset builder; unified export CI jobs into one matrix.
  • Lương Thanh Hoàng Phú (@lthphuw, LinkedIn) — hardened TRTInference to reject mismatched device/dtype/contiguity instead of returning silently wrong detections; fixed checkpoints to record training args and model config so from_checkpoint no longer raises KeyError; fixed LoRA backbone reload, which had silently stayed at random init.
  • Raghav Rathi (@raghav-rathi, LinkedIn) — fixed keypoint gradient clipping under fp16, which ran before GradScaler unscale and silently stalled training; fixed amp_dtype="auto" selecting emulated bf16 on GPUs without native support; excluded COCO crowd-region detections from val/test mAP.
  • Mohammad Hijjawi (@MohammadHijjawi97, LinkedIn) — fixed COCO segmentation to keep all masks when an image's first annotation has no segmentation field; fixed YOLO labels-directory resolution for datasets nested under an images directory.
  • Christoph Deil (@cdeil, LinkedIn) — fixed CoreAI export compatibility with coreai-torch 0.4.3 after it removed AIProgram.optimize().

Automated contributions from claude[bot], pre-commit-ci[bot], Copilot Autofix powered by AI, copilot-swe-agent[bot], OpenAI Codex, Claude Sonnet 5, Claude Opus 5 (1M context), and Claude Opus 5.5 supported this release.


Full changelog: 1.11.0...1.11.1