16031aae96
CPU tests Workflow / Testing (ubuntu-latest, 3.12) (push) Failing after 1s
CPU tests Workflow / Testing (ubuntu-latest, 3.13) (push) Failing after 0s
Mypy Type Check / Type Check (push) Failing after 0s
Docs/Test WorkFlow / Test docs build (push) Failing after 1s
PR Conflict Labeler / labeling (push) Failing after 1s
Dependency resolution / Resolve [tflite] extra — Python 3.12 (push) Failing after 0s
Smoke Tests / try-all-models (ubuntu-latest, 3.10) (push) Failing after 0s
Smoke Tests / try-all-models (ubuntu-latest, 3.13) (push) Failing after 1s
CPU tests Workflow / build-pkg (push) Failing after 1s
CPU tests Workflow / Testing (ubuntu-latest, 3.10) (push) Failing after 0s
CPU tests Workflow / Testing (ubuntu-latest, 3.11) (push) Failing after 0s
Smoke Tests / try-all-models (macos-latest, 3.10) (push) Has been cancelled
Smoke Tests / try-all-models (macos-latest, 3.13) (push) Has been cancelled
Smoke Tests / try-all-models (windows-latest, 3.10) (push) Has been cancelled
Smoke Tests / try-all-models (windows-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / Testing (macos-latest, 3.10) (push) Has been cancelled
CPU tests Workflow / Testing (macos-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / Testing (windows-latest, 3.10) (push) Has been cancelled
CPU tests Workflow / Testing (windows-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / testing-guardian (push) Has been cancelled
GPU tests Workflow / Testing (push) Has been cancelled
323 lines
12 KiB
Python
323 lines
12 KiB
Python
# ------------------------------------------------------------------------
|
|
# RF-DETR
|
|
# Copyright (c) 2025 Roboflow. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
|
|
# ------------------------------------------------------------------------
|
|
"""End-to-end benchmarks for training convergence via the PTL stack.
|
|
|
|
Smoke test (CPU-friendly):
|
|
|
|
* :func:`test_train_fast_dev_run` — ``Trainer.fit`` completes without error on a synthetic dataset.
|
|
|
|
Training convergence (GPU, synthetic dataset, no pretrained weights):
|
|
|
|
* :func:`test_train_convergence_native_ptl` — ``RFDETRModelModule`` + ``Trainer.fit`` reaches ≥ 35 % mAP@50.
|
|
* :func:`test_train_convergence_rfdetr_api` — ``RFDETR.train()`` reaches ≥ 35 % mAP@50.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import torch
|
|
from pytorch_lightning import LightningModule
|
|
|
|
from rfdetr import RFDETRNano
|
|
from rfdetr.config import RFDETRBaseConfig, RFDETRNanoConfig, RFDETRSegNanoConfig, SegmentationTrainConfig, TrainConfig
|
|
from rfdetr.detr import RFDETR
|
|
from rfdetr.training import RFDETRDataModule, RFDETRModelModule, build_trainer
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_ptl_module_from(rfdetr_obj: RFDETR, dataset_dir: Path, output_dir: Path) -> RFDETRModelModule:
|
|
"""Build an :class:`~rfdetr.training.RFDETRModelModule` from an RFDETR instance.
|
|
|
|
Creates the module with the same architecture as *rfdetr_obj*, copies its current weights, and asserts PTL lineage
|
|
before returning.
|
|
|
|
Args:
|
|
rfdetr_obj: A (possibly trained) :class:`~rfdetr.detr.RFDETR` instance.
|
|
dataset_dir: Dataset directory forwarded to :class:`~rfdetr.config.TrainConfig`.
|
|
output_dir: Output directory forwarded to :class:`~rfdetr.config.TrainConfig`.
|
|
|
|
Returns:
|
|
Weight-synced :class:`~rfdetr.training.RFDETRModelModule` in eval mode.
|
|
"""
|
|
train_config = TrainConfig(
|
|
dataset_file="roboflow",
|
|
dataset_dir=str(dataset_dir),
|
|
output_dir=str(output_dir),
|
|
)
|
|
model_config = rfdetr_obj.model_config.model_copy(update={"pretrain_weights": None})
|
|
module = RFDETRModelModule(model_config, train_config)
|
|
module.model.load_state_dict(rfdetr_obj.model.model.state_dict())
|
|
module.model.eval()
|
|
|
|
assert isinstance(module, RFDETRModelModule), f"Expected RFDETRModelModule, got {type(module).__name__}"
|
|
assert isinstance(module, LightningModule), "Module must be a pytorch_lightning.LightningModule"
|
|
return module
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Smoke test (CPU-friendly, no GPU required)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_train_fast_dev_run(
|
|
tmp_path: Path,
|
|
synthetic_shape_dataset_dir: Path,
|
|
) -> None:
|
|
"""Smoke-test the full PTL stack on a real synthetic dataset with fast_dev_run.
|
|
|
|
Uses ``build_trainer(tc, mc, fast_dev_run=2)`` and ``trainer.fit(module, datamodule=datamodule)`` with a real model
|
|
and real data (no mocking). Only asserts the pipeline runs without error; convergence is tested by the GPU-only
|
|
tests below.
|
|
"""
|
|
output_dir = tmp_path / "output"
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(synthetic_shape_dataset_dir / "train" / "_annotations.coco.json") as f:
|
|
num_classes = len(json.load(f)["categories"])
|
|
|
|
mc = RFDETRNanoConfig(num_classes=num_classes, pretrain_weights=None, amp=False)
|
|
tc = TrainConfig(
|
|
dataset_dir=str(synthetic_shape_dataset_dir),
|
|
output_dir=str(output_dir),
|
|
epochs=1,
|
|
batch_size=2,
|
|
num_workers=0,
|
|
use_ema=False,
|
|
run_test=False,
|
|
tensorboard=False,
|
|
multi_scale=False,
|
|
expanded_scales=False,
|
|
do_random_resize_via_padding=False,
|
|
drop_path=0.0,
|
|
grad_accum_steps=1,
|
|
)
|
|
|
|
module = RFDETRModelModule(mc, tc)
|
|
datamodule = RFDETRDataModule(mc, tc)
|
|
trainer = build_trainer(tc, mc, accelerator="auto", fast_dev_run=2)
|
|
trainer.fit(module, datamodule=datamodule)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Training convergence (GPU, synthetic dataset)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.gpu
|
|
@pytest.mark.flaky(reruns=1, only_rerun="AssertionError")
|
|
def test_train_convergence_native_ptl(
|
|
tmp_path: Path,
|
|
synthetic_shape_dataset_dir: Path,
|
|
) -> None:
|
|
"""Native PTL stack converges: ``RFDETRModelModule`` + ``RFDETRDataModule`` + ``Trainer.fit``.
|
|
|
|
Uses ``Trainer.validate`` before and after ``Trainer.fit`` so only Lightning elements are exercised — no
|
|
``engine.evaluate`` or legacy paths.
|
|
|
|
Assertions:
|
|
- ``val/mAP_50`` before training ≤ 5 %.
|
|
- ``val/mAP_50`` after 10 epochs ≥ 35 %.
|
|
"""
|
|
output_dir = tmp_path / "train_output"
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
dataset_dir = synthetic_shape_dataset_dir
|
|
|
|
with open(dataset_dir / "train" / "_annotations.coco.json") as f:
|
|
num_classes = len(json.load(f)["categories"])
|
|
|
|
accelerator = "auto" if torch.cuda.is_available() else "cpu"
|
|
|
|
mc = RFDETRBaseConfig(num_classes=num_classes, pretrain_weights=None, amp=False)
|
|
tc = TrainConfig(
|
|
dataset_file="roboflow",
|
|
dataset_dir=str(dataset_dir),
|
|
output_dir=str(output_dir),
|
|
epochs=10,
|
|
batch_size=4,
|
|
grad_accum_steps=1,
|
|
num_workers=max(1, (os.cpu_count() or 1) // 2),
|
|
lr=1e-3,
|
|
warmup_epochs=1.0,
|
|
use_ema=True,
|
|
multi_scale=False,
|
|
run_test=False,
|
|
tensorboard=False,
|
|
)
|
|
|
|
module = RFDETRModelModule(mc, tc)
|
|
datamodule = RFDETRDataModule(mc, tc)
|
|
|
|
# Pre-training baseline — untrained model should have near-zero mAP.
|
|
pre_trainer = build_trainer(tc, mc, accelerator=accelerator)
|
|
pre_results = pre_trainer.validate(module, datamodule=datamodule)
|
|
map_before = pre_results[0]["val/mAP_50"]
|
|
assert map_before <= 0.05, f"Untrained val mAP {map_before:.3f} should be ≤ 5 %."
|
|
|
|
# Train via native PTL Trainer.fit.
|
|
trainer = build_trainer(tc, mc, accelerator=accelerator)
|
|
trainer.fit(module, datamodule=datamodule)
|
|
|
|
# Post-training validation — model should have converged.
|
|
post_results = trainer.validate(module, datamodule=datamodule)
|
|
map_after = post_results[0]["val/mAP_50"]
|
|
assert map_after >= 0.35, f"val mAP {map_after:.3f} should reach at least 0.35 after Trainer.fit."
|
|
|
|
|
|
@pytest.mark.gpu
|
|
@pytest.mark.flaky(reruns=1, only_rerun="AssertionError")
|
|
def test_train_convergence_rfdetr_api(
|
|
tmp_path: Path,
|
|
synthetic_shape_dataset_dir: Path,
|
|
) -> None:
|
|
"""``RFDETR.train()`` entry-point converges on synthetic data.
|
|
|
|
Exercises the public ``model.train()`` API end-to-end. Pre- and post-training mAP are measured via
|
|
``Trainer.validate`` so the assertion is identical to :func:`test_train_convergence_native_ptl`.
|
|
|
|
Assertions:
|
|
- ``val/mAP_50`` before training ≤ 5 %.
|
|
- ``val/mAP_50`` after 10 epochs ≥ 35 %.
|
|
"""
|
|
output_dir = tmp_path / "train_output"
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
dataset_dir = synthetic_shape_dataset_dir
|
|
|
|
with open(dataset_dir / "train" / "_annotations.coco.json") as f:
|
|
num_classes = len(json.load(f)["categories"])
|
|
|
|
accelerator = "auto" if torch.cuda.is_available() else "cpu"
|
|
device = None if torch.cuda.is_available() else "cpu"
|
|
|
|
model = RFDETRNano(num_classes=num_classes, pretrain_weights=None, amp=False)
|
|
# Use the model's own config so RFDETRDataModule uses the correct resolution.
|
|
# RFDETRNano (patch_size=16, num_windows=2) requires block_size=32 divisibility;
|
|
# its resolution=384 satisfies this, while RFDETRBaseConfig resolution=560 does not.
|
|
mc = model.model_config
|
|
tc = TrainConfig(
|
|
dataset_file="roboflow",
|
|
dataset_dir=str(dataset_dir),
|
|
output_dir=str(output_dir),
|
|
epochs=10,
|
|
batch_size=4,
|
|
grad_accum_steps=1,
|
|
num_workers=max(1, (os.cpu_count() or 1) // 2),
|
|
lr=1e-3,
|
|
warmup_epochs=1.0,
|
|
use_ema=True,
|
|
multi_scale=False,
|
|
run_test=False,
|
|
tensorboard=False,
|
|
)
|
|
|
|
datamodule = RFDETRDataModule(mc, tc)
|
|
|
|
# Pre-training baseline via a temporary PTL module.
|
|
pre_module = _make_ptl_module_from(model, dataset_dir, output_dir)
|
|
pre_trainer = build_trainer(tc, mc, accelerator=accelerator)
|
|
pre_results = pre_trainer.validate(pre_module, datamodule=datamodule)
|
|
map_before = pre_results[0]["val/mAP_50"]
|
|
assert map_before <= 0.05, f"Untrained val mAP {map_before:.3f} should be ≤ 5 %."
|
|
|
|
# Train via the public RFDETR.train() API.
|
|
train_kwargs = dict(
|
|
dataset_file="roboflow",
|
|
dataset_dir=str(dataset_dir),
|
|
output_dir=str(output_dir),
|
|
epochs=10,
|
|
batch_size=4,
|
|
grad_accum_steps=1,
|
|
num_workers=max(1, (os.cpu_count() or 1) // 2),
|
|
lr=1e-3,
|
|
warmup_epochs=1.0,
|
|
use_ema=True,
|
|
multi_scale=False,
|
|
run_test=False,
|
|
tensorboard=False,
|
|
)
|
|
if device is not None:
|
|
train_kwargs["device"] = device
|
|
model.train(**train_kwargs)
|
|
|
|
# Post-training: copy trained weights into a fresh module and validate.
|
|
post_module = _make_ptl_module_from(model, dataset_dir, output_dir)
|
|
post_trainer = build_trainer(tc, mc, accelerator=accelerator)
|
|
post_results = post_trainer.validate(post_module, datamodule=datamodule)
|
|
map_after = post_results[0]["val/mAP_50"]
|
|
assert map_after >= 0.35, f"val mAP {map_after:.3f} should reach at least 0.35 after RFDETR.train()."
|
|
|
|
|
|
@pytest.mark.gpu
|
|
@pytest.mark.flaky(reruns=1, only_rerun="AssertionError")
|
|
def test_train_convergence_segmentation(
|
|
tmp_path: Path,
|
|
synthetic_shape_segmentation_dataset_dir: Path,
|
|
) -> None:
|
|
"""Segmentation PTL stack converges on synthetic polygon data.
|
|
|
|
Mirrors :func:`test_train_convergence_native_ptl` but uses :class:`~rfdetr.config.RFDETRSegNanoConfig` and
|
|
:class:`~rfdetr.config.SegmentationTrainConfig` with a dataset that includes COCO polygon annotations.
|
|
|
|
The mask mAP threshold is deliberately lower than the bbox threshold because segmentation convergence is harder
|
|
within the same epoch budget. Thresholds are calibrated conservatively: the goal is to verify that the segmentation
|
|
training pipeline is functional (loss flows, masks are loaded, both bbox and segm mAP improve) rather than to
|
|
validate final accuracy.
|
|
|
|
Assertions:
|
|
- ``val/mAP_50`` before training ≤ 5 %.
|
|
- ``val/mAP_50`` after 5 epochs ≥ 10 %.
|
|
- ``val/segm_mAP_50`` after 5 epochs ≥ 5 %.
|
|
"""
|
|
output_dir = tmp_path / "train_output_seg"
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
dataset_dir = synthetic_shape_segmentation_dataset_dir
|
|
|
|
with open(dataset_dir / "train" / "_annotations.coco.json") as f:
|
|
num_classes = len(json.load(f)["categories"])
|
|
|
|
accelerator = "auto" if torch.cuda.is_available() else "cpu"
|
|
|
|
mc = RFDETRSegNanoConfig(num_classes=num_classes, pretrain_weights=None, amp=False)
|
|
tc = SegmentationTrainConfig(
|
|
dataset_file="roboflow",
|
|
dataset_dir=str(dataset_dir),
|
|
output_dir=str(output_dir),
|
|
epochs=5,
|
|
batch_size=4,
|
|
grad_accum_steps=1,
|
|
num_workers=max(1, (os.cpu_count() or 1) // 2),
|
|
lr=1e-3,
|
|
warmup_epochs=1.0,
|
|
use_ema=True,
|
|
multi_scale=False,
|
|
run_test=False,
|
|
tensorboard=False,
|
|
)
|
|
|
|
module = RFDETRModelModule(mc, tc)
|
|
datamodule = RFDETRDataModule(mc, tc)
|
|
|
|
# Pre-training baseline — untrained model should have near-zero mAP.
|
|
pre_trainer = build_trainer(tc, mc, accelerator=accelerator)
|
|
pre_results = pre_trainer.validate(module, datamodule=datamodule)
|
|
map_before = pre_results[0]["val/mAP_50"]
|
|
assert map_before <= 0.05, f"Untrained val bbox mAP {map_before:.3f} should be ≤ 5 %."
|
|
|
|
# Train via native PTL Trainer.fit.
|
|
trainer = build_trainer(tc, mc, accelerator=accelerator)
|
|
trainer.fit(module, datamodule=datamodule)
|
|
|
|
# Post-training validation — both bbox and mask mAP should have improved.
|
|
post_results = trainer.validate(module, datamodule=datamodule)
|
|
map_after = post_results[0]["val/mAP_50"]
|
|
segm_map_after = post_results[0]["val/segm_mAP_50"]
|
|
assert map_after >= 0.15, f"val bbox mAP {map_after:.3f} should reach at least 0.15 after Trainer.fit."
|
|
assert segm_map_after >= 0.05, f"val segm mAP {segm_map_after:.3f} should reach at least 0.05 after Trainer.fit."
|