Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:26:24 +08:00

1157 lines
50 KiB
Python

# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Tests for build_trainer() — PTL Ch3/T5 (callbacks) and Ch4/T1 (precision, loggers, trainer kwargs)."""
import warnings
from unittest.mock import MagicMock, patch
import pytest
from pytorch_lightning.callbacks import ModelCheckpoint
from rfdetr.config import (
KeypointTrainConfig,
RFDETRBaseConfig,
RFDETRKeypointPreviewConfig,
SegmentationTrainConfig,
TrainConfig,
)
from rfdetr.training import build_trainer
from rfdetr.training.callbacks.best_model import BestModelCallback, RFDETREarlyStopping
from rfdetr.training.callbacks.coco_eval import COCOEvalCallback
from rfdetr.training.callbacks.drop_schedule import DropPathCallback
from rfdetr.training.callbacks.ema import RFDETREMACallback
def _mc(**kwargs):
"""Minimal RFDETRBaseConfig for tests."""
defaults = dict(pretrain_weights=None, device="cpu", num_classes=3)
defaults.update(kwargs)
return RFDETRBaseConfig(**defaults)
def _find_resume_checkpoints(trainer):
"""Return ModelCheckpoint callbacks that are NOT BestModelCallback."""
return [cb for cb in trainer.callbacks if isinstance(cb, ModelCheckpoint) and not isinstance(cb, BestModelCallback)]
def _tc(tmp_path, **kwargs):
"""Minimal TrainConfig for tests.
Loggers are disabled by default to avoid requiring optional deps (tensorboard, wandb, mlflow) in the CPU test
environment. Logger-specific tests override these explicitly via kwargs or mocking.
"""
defaults = dict(
dataset_dir=str(tmp_path / "ds"),
output_dir=str(tmp_path / "out"),
epochs=1,
batch_size=2,
num_workers=0,
tensorboard=False,
wandb=False,
mlflow=False,
clearml=False,
)
defaults.update(kwargs)
return TrainConfig(**defaults)
def _kp_tc(tmp_path, **kwargs):
"""Minimal KeypointTrainConfig for tests that exercise keypoint model paths."""
defaults = dict(
dataset_dir=str(tmp_path / "ds"),
output_dir=str(tmp_path / "out"),
epochs=1,
batch_size=2,
num_workers=0,
tensorboard=False,
wandb=False,
mlflow=False,
clearml=False,
)
defaults.update(kwargs)
return KeypointTrainConfig(**defaults)
class TestBuildTrainerReturnType:
"""build_trainer() must return a PTL Trainer."""
def test_returns_trainer_instance(self, tmp_path):
"""Return value must be a pytorch_lightning.Trainer."""
from pytorch_lightning import Trainer
trainer = build_trainer(_tc(tmp_path), _mc())
assert isinstance(trainer, Trainer)
class TestBuildTrainerCallbacks:
"""build_trainer() must wire the correct callback set."""
def test_coco_eval_always_present(self, tmp_path):
"""COCOEvalCallback is always included regardless of config flags."""
trainer = build_trainer(_tc(tmp_path, use_ema=False, early_stopping=False), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert COCOEvalCallback in types
def test_coco_eval_uses_eval_interval_and_per_class_flags(self, tmp_path):
"""COCOEvalCallback receives eval_interval and log_per_class_metrics from TrainConfig."""
trainer = build_trainer(
_tc(tmp_path, use_ema=False, eval_interval=3, log_per_class_metrics=False),
_mc(),
)
coco_cb = next(cb for cb in trainer.callbacks if isinstance(cb, COCOEvalCallback))
assert coco_cb._eval_interval == 3
assert coco_cb._log_per_class_metrics is False
def test_coco_eval_uses_keypoint_oks_sigmas(self, tmp_path):
"""COCOEvalCallback receives custom keypoint OKS sigmas from TrainConfig."""
sigmas = [0.05] * 25
trainer = build_trainer(
_kp_tc(tmp_path, use_ema=False, keypoint_oks_sigmas=sigmas),
RFDETRKeypointPreviewConfig(pretrain_weights=None),
)
coco_cb = next(cb for cb in trainer.callbacks if isinstance(cb, COCOEvalCallback))
assert coco_cb._keypoint_oks_sigmas == sigmas
def test_best_model_always_present(self, tmp_path):
"""BestModelCallback is always included."""
trainer = build_trainer(_tc(tmp_path, use_ema=False), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert BestModelCallback in types
def test_skip_best_epochs_forwarded_to_best_model_callback(self, tmp_path):
"""BestModelCallback receives skip_best_epochs from TrainConfig."""
trainer = build_trainer(_tc(tmp_path, use_ema=False, skip_best_epochs=3), _mc())
best_cb = next(cb for cb in trainer.callbacks if isinstance(cb, BestModelCallback))
assert best_cb._skip_best_epochs == 3
def test_keypoint_best_model_monitors_keypoint_map(self, tmp_path):
"""Keypoint training checkpoints should rank models by keypoint AP, not bbox mAP."""
trainer = build_trainer(_kp_tc(tmp_path, use_ema=True), RFDETRKeypointPreviewConfig(pretrain_weights=None))
best_cb = next(cb for cb in trainer.callbacks if isinstance(cb, BestModelCallback))
assert best_cb.monitor == "val/keypoint_map_50_95"
assert best_cb._monitor_ema == "val/ema_keypoint_map_50_95"
def test_segmentation_best_model_monitors_segmentation_map(self, tmp_path):
"""Segmentation training checkpoints should rank models by segmentation AP, not bbox AP."""
trainer = build_trainer(_tc(tmp_path, use_ema=True), _mc(segmentation_head=True))
best_cb = next(cb for cb in trainer.callbacks if isinstance(cb, BestModelCallback))
assert best_cb.monitor == "val/segm_mAP_50_95"
assert best_cb._monitor_ema == "val/ema_segm_mAP_50_95"
def test_latest_model_checkpoint_present(self, tmp_path):
"""A ModelCheckpoint (not BestModelCallback) with every_n_epochs==1 is included when checkpoint_interval > 1."""
trainer = build_trainer(_tc(tmp_path, use_ema=False, checkpoint_interval=2), _mc())
resume_cbs = _find_resume_checkpoints(trainer)
assert any(cb._every_n_epochs == 1 for cb in resume_cbs)
def test_latest_model_checkpoint_absent_when_checkpoint_interval_one(self, tmp_path):
"""No separate latest checkpoint callback when interval already saves every epoch."""
trainer = build_trainer(_tc(tmp_path, use_ema=False, checkpoint_interval=1), _mc())
resume_cbs = _find_resume_checkpoints(trainer)
assert resume_cbs
assert not any(cb._every_n_epochs == 1 and cb.save_top_k == 1 for cb in resume_cbs)
interval_cb = next(
(cb for cb in resume_cbs if cb._every_n_epochs == 1 and cb.save_top_k == -1),
None,
)
assert interval_cb is not None
assert interval_cb.filename == "checkpoint_{epoch}"
assert str(interval_cb.dirpath) == str(tmp_path / "out")
def test_interval_model_checkpoint_present(self, tmp_path):
"""A ModelCheckpoint (not BestModelCallback) with every_n_epochs==checkpoint_interval is always included."""
tc = _tc(tmp_path, use_ema=False)
trainer = build_trainer(tc, _mc())
resume_cbs = _find_resume_checkpoints(trainer)
assert any(cb._every_n_epochs == tc.checkpoint_interval for cb in resume_cbs)
def test_checkpoint_interval_one_has_single_resume_checkpoint_callback(self, tmp_path):
"""checkpoint_interval=1 config creates only one non-best ModelCheckpoint callback."""
trainer = build_trainer(_tc(tmp_path, use_ema=False, checkpoint_interval=1), _mc())
resume_cbs = _find_resume_checkpoints(trainer)
assert len(resume_cbs) == 1
only_cb = resume_cbs[0]
assert only_cb._every_n_epochs == 1
assert only_cb.save_top_k == -1
@pytest.mark.parametrize(
"checkpoint_interval",
[
pytest.param(1, id="interval_1"),
pytest.param(2, id="interval_2"),
pytest.param(7, id="interval_7"),
],
)
def test_all_model_checkpoints_have_unique_state_keys(self, tmp_path, checkpoint_interval):
"""All ModelCheckpoint callbacks (including BestModelCallback) always have unique state keys."""
trainer = build_trainer(_tc(tmp_path, use_ema=False, checkpoint_interval=checkpoint_interval), _mc())
all_mc_cbs = [cb for cb in trainer.callbacks if isinstance(cb, ModelCheckpoint)]
state_keys = [cb.state_key for cb in all_mc_cbs]
assert len(state_keys) == len(set(state_keys)), (
f"Duplicate state_key with checkpoint_interval={checkpoint_interval}: "
f"{[k for k in state_keys if state_keys.count(k) > 1]}"
)
def test_interval_checkpoint_uses_interval_from_config(self, tmp_path):
"""Interval ModelCheckpoint receives checkpoint_interval=7 from TrainConfig."""
trainer = build_trainer(_tc(tmp_path, use_ema=False, checkpoint_interval=7), _mc())
resume_cbs = _find_resume_checkpoints(trainer)
assert any(cb._every_n_epochs == 7 for cb in resume_cbs)
def test_checkpoint_interval_validation(self, tmp_path):
"""TrainConfig(checkpoint_interval=0) raises ValidationError."""
from pydantic import ValidationError
with pytest.raises(ValidationError):
_tc(tmp_path, checkpoint_interval=0)
def test_ema_callback_when_use_ema_true(self, tmp_path):
"""RFDETREMACallback is added when use_ema=True."""
trainer = build_trainer(_tc(tmp_path, use_ema=True), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert RFDETREMACallback in types
def test_ema_callback_uses_update_interval(self, tmp_path):
"""RFDETREMACallback receives ema_update_interval from TrainConfig."""
trainer = build_trainer(_tc(tmp_path, use_ema=True, ema_update_interval=4), _mc())
ema_cb = next(cb for cb in trainer.callbacks if isinstance(cb, RFDETREMACallback))
assert ema_cb._update_interval_steps == 4
def test_no_ema_callback_when_use_ema_false(self, tmp_path):
"""RFDETREMACallback is absent when use_ema=False."""
trainer = build_trainer(_tc(tmp_path, use_ema=False), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert RFDETREMACallback not in types
def test_drop_path_callback_when_drop_path_nonzero(self, tmp_path):
"""DropPathCallback is added when drop_path > 0."""
trainer = build_trainer(_tc(tmp_path, drop_path=0.1), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert DropPathCallback in types
def test_no_drop_path_callback_when_drop_path_zero(self, tmp_path):
"""DropPathCallback is absent when drop_path == 0."""
trainer = build_trainer(_tc(tmp_path, drop_path=0.0), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert DropPathCallback not in types
def test_early_stopping_when_enabled(self, tmp_path):
"""RFDETREarlyStopping is added when early_stopping=True."""
trainer = build_trainer(_tc(tmp_path, early_stopping=True), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert RFDETREarlyStopping in types
def test_skip_best_epochs_forwarded_to_early_stopping(self, tmp_path):
"""RFDETREarlyStopping receives skip_best_epochs from TrainConfig."""
trainer = build_trainer(_tc(tmp_path, early_stopping=True, skip_best_epochs=4), _mc())
early_stop_cb = next(cb for cb in trainer.callbacks if isinstance(cb, RFDETREarlyStopping))
assert early_stop_cb._skip_best_epochs == 4
def test_keypoint_early_stopping_monitors_keypoint_map(self, tmp_path):
"""Keypoint early stopping should use keypoint AP as the regular metric."""
trainer = build_trainer(
_kp_tc(tmp_path, early_stopping=True, early_stopping_use_ema=True),
RFDETRKeypointPreviewConfig(pretrain_weights=None),
)
early_stop_cb = next(cb for cb in trainer.callbacks if isinstance(cb, RFDETREarlyStopping))
assert early_stop_cb._monitor_regular == "val/keypoint_map_50_95"
assert early_stop_cb._monitor_ema == "val/ema_keypoint_map_50_95"
def test_segmentation_early_stopping_monitors_segmentation_map(self, tmp_path):
"""Segmentation early stopping should use segmentation AP as the regular metric."""
trainer = build_trainer(
_tc(tmp_path, early_stopping=True, early_stopping_use_ema=True),
_mc(segmentation_head=True),
)
early_stop_cb = next(cb for cb in trainer.callbacks if isinstance(cb, RFDETREarlyStopping))
assert early_stop_cb._monitor_regular == "val/segm_mAP_50_95"
assert early_stop_cb._monitor_ema == "val/ema_segm_mAP_50_95"
def test_no_early_stopping_when_disabled(self, tmp_path):
"""RFDETREarlyStopping is absent when early_stopping=False."""
trainer = build_trainer(_tc(tmp_path, early_stopping=False), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert RFDETREarlyStopping not in types
def test_segmentation_config_accepted(self, tmp_path):
"""SegmentationTrainConfig is accepted without error."""
seg_tc = SegmentationTrainConfig(
dataset_dir=str(tmp_path / "ds"),
output_dir=str(tmp_path / "out"),
epochs=1,
batch_size=2,
num_workers=0,
tensorboard=False,
wandb=False,
mlflow=False,
clearml=False,
)
trainer = build_trainer(seg_tc, _mc(segmentation_head=True))
assert isinstance(trainer, __import__("pytorch_lightning").Trainer)
class TestBuildTrainerKeypointDefaults:
"""Verify build_trainer() applies keypoint-specific defaults for noisy fine-tuning metrics."""
def test_keypoint_default_skip_best_epochs_is_ten(self, tmp_path):
"""KeypointTrainConfig defaults skip_best_epochs to 10; build_trainer forwards it to callbacks."""
trainer = build_trainer(
_kp_tc(tmp_path, use_ema=False, early_stopping=True),
RFDETRKeypointPreviewConfig(pretrain_weights=None),
)
best_cb = next(cb for cb in trainer.callbacks if isinstance(cb, BestModelCallback))
early_stop_cb = next(cb for cb in trainer.callbacks if isinstance(cb, RFDETREarlyStopping))
assert best_cb._skip_best_epochs == 10
assert early_stop_cb._skip_best_epochs == 10
def test_keypoint_explicit_skip_best_epochs_overrides_default(self, tmp_path):
"""An explicitly-set skip_best_epochs on a keypoint config overrides the class default of 10."""
trainer = build_trainer(
_kp_tc(tmp_path, use_ema=False, skip_best_epochs=3),
RFDETRKeypointPreviewConfig(pretrain_weights=None),
)
best_cb = next(cb for cb in trainer.callbacks if isinstance(cb, BestModelCallback))
assert best_cb._skip_best_epochs == 3
def test_non_keypoint_default_skip_best_epochs_is_zero(self, tmp_path):
"""For detection models, skip_best_epochs default remains 0."""
trainer = build_trainer(_tc(tmp_path, use_ema=False), _mc())
best_cb = next(cb for cb in trainer.callbacks if isinstance(cb, BestModelCallback))
assert best_cb._skip_best_epochs == 0
def test_keypoint_smooth_alpha_is_half(self, tmp_path):
"""BestModelCallback receives smooth_alpha=0.5 for keypoint models to dampen noisy mAP swings."""
trainer = build_trainer(_kp_tc(tmp_path, use_ema=False), RFDETRKeypointPreviewConfig(pretrain_weights=None))
best_cb = next(cb for cb in trainer.callbacks if isinstance(cb, BestModelCallback))
assert best_cb._smooth_alpha == pytest.approx(0.5)
def test_non_keypoint_smooth_alpha_is_zero(self, tmp_path):
"""Detection / segmentation BestModelCallback keeps smooth_alpha=0.0 (no smoothing)."""
trainer = build_trainer(_tc(tmp_path, use_ema=False), _mc())
best_cb = next(cb for cb in trainer.callbacks if isinstance(cb, BestModelCallback))
assert best_cb._smooth_alpha == 0.0
class TestBuildTrainerPrecision:
"""build_trainer() must resolve training precision from model_config.amp + device caps."""
def test_amp_false_gives_32_true(self, tmp_path):
"""Amp=False always produces '32-true' regardless of device."""
trainer = build_trainer(_tc(tmp_path, use_ema=False), _mc(amp=False))
assert trainer.precision == "32-true"
def test_amp_true_cpu_gives_32_true(self, tmp_path):
"""Amp=True on CPU (no CUDA, no MPS) must fall back to '32-true'."""
import unittest.mock as mock
with (
mock.patch("torch.cuda.is_available", return_value=False),
mock.patch("torch.backends.mps.is_available", return_value=False),
):
trainer = build_trainer(_tc(tmp_path, use_ema=False), _mc(amp=True))
assert trainer.precision == "32-true"
def test_amp_true_explicit_cpu_accelerator_gives_32_true_even_with_mps(self, tmp_path):
"""Amp=True with explicit accelerator='cpu' must produce '32-true' even when MPS is present.
bf16 autocast on macOS CPU (Apple Silicon) is ~13x slower than fp32 — no hardware support for bfloat16 in CPU
kernels causes software emulation. When the caller explicitly opts into CPU (e.g. for test isolation), mixed
precision must not be used.
"""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with (
mock.patch("torch.cuda.is_available", return_value=False),
mock.patch("torch.backends.mps.is_available", return_value=True),
mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer),
):
build_trainer(_tc(tmp_path, use_ema=False), _mc(amp=True), accelerator="cpu")
assert captured["precision"] == "32-true"
def test_amp_true_cuda_no_bf16_gives_16_mixed(self, tmp_path):
"""Amp=True with CUDA but no bf16 support must produce '16-mixed'."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with (
mock.patch("torch.cuda.is_available", return_value=True),
mock.patch("torch.cuda.is_bf16_supported", return_value=False),
mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer),
):
build_trainer(_tc(tmp_path, use_ema=False), _mc(amp=True))
assert captured["precision"] == "16-mixed"
def test_amp_true_cuda_bf16_supported_gives_bf16_mixed(self, tmp_path):
"""Amp=True with CUDA + bf16 hardware produces 'bf16-mixed'."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with (
mock.patch("torch.cuda.is_available", return_value=True),
mock.patch("torch.cuda.is_bf16_supported", return_value=True),
mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer),
):
build_trainer(_tc(tmp_path, use_ema=False), _mc(amp=True))
assert captured["precision"] == "bf16-mixed"
@patch("torch.cuda.is_available", return_value=True)
@patch("torch.cuda.is_bf16_supported", return_value=False)
@patch("rfdetr.training.trainer.Trainer")
def test_amp_true_ddp_notebook_probes_bf16_normally(
self, mock_trainer: MagicMock, _mock_bf16: MagicMock, _mock_cuda: MagicMock, tmp_path
):
"""ddp_notebook uses standard precision probing (spawn makes CUDA init safe).
With spawn-based DDP, child processes start fresh — CUDA init in the parent does not propagate. So
``is_bf16_supported()`` is safe to call and pre-Ampere GPUs correctly get ``16-mixed`` instead of the slower
bf16 emulation path. Simulates pre-Ampere GPU: CUDA available, bf16 NOT supported.
"""
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return MagicMock()
mock_trainer.side_effect = _fake_trainer
build_trainer(
_tc(tmp_path, use_ema=False, strategy="ddp_notebook"),
_mc(amp=True),
)
assert captured["precision"] == "16-mixed"
@pytest.mark.parametrize("strategy_name", ["ddp_notebook", "ddp_spawn"])
def test_ddp_notebook_and_spawn_use_interactive_spawn(self, tmp_path, strategy_name):
"""ddp_notebook and ddp_spawn must be replaced with interactive spawn DDPStrategy.
Fork-based DDP inherits the parent's OpenMP thread pool which is invalid after fork, causing SIGABRT in the
autograd engine. ddp_spawn is blocked by PTL in notebooks without the override.
"""
import unittest.mock as mock
from pytorch_lightning.strategies import DDPStrategy
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(
_tc(tmp_path, use_ema=False, strategy=strategy_name),
_mc(amp=True),
)
strategy_obj = captured["strategy"]
assert isinstance(strategy_obj, DDPStrategy)
assert strategy_obj._start_method == "spawn"
assert strategy_obj._ddp_kwargs.get("find_unused_parameters") is True
@patch("rfdetr.training.trainer._InteractiveSpawnLauncher", None)
def test_ddp_notebook_raises_clear_error_when_private_launcher_is_missing(self, tmp_path):
"""Missing private PTL launcher should raise a targeted compatibility error."""
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return MagicMock()
with patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(
_tc(tmp_path, use_ema=False, strategy="ddp_notebook"),
_mc(amp=True),
)
strategy = captured["strategy"]
strategy.cluster_environment = object()
with pytest.raises(RuntimeError, match="private API"):
strategy._configure_launcher()
class TestBuildTrainerAmpDtype:
"""``TrainConfig.amp_dtype`` (a ``train()`` kwarg) lets callers pin the AMP autocast dtype (fp16 vs bf16) — #1132.
Precision is resolved inside ``build_trainer``; these tests mock the CUDA/MPS capability probes and assert the
Lightning precision string captured at ``Trainer`` construction time.
"""
@staticmethod
def _resolved_precision(tmp_path, *, cuda: bool, bf16: bool = False, mps: bool = False, amp_dtype: str = "auto"):
"""Resolve the Lightning precision string for a mocked device capability and ``amp_dtype``.
Args:
tmp_path: pytest temporary directory fixture.
cuda: Value returned by the mocked ``torch.cuda.is_available``.
bf16: Value returned by the mocked ``torch.cuda.is_bf16_supported``.
mps: Value returned by the mocked ``torch.backends.mps.is_available``.
amp_dtype: The ``TrainConfig.amp_dtype`` value under test.
Returns:
The ``precision`` string passed to the (mocked) ``Trainer``.
"""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with (
mock.patch("torch.cuda.is_available", return_value=cuda),
mock.patch("torch.cuda.is_bf16_supported", return_value=bf16),
mock.patch("torch.backends.mps.is_available", return_value=mps),
mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer),
):
build_trainer(_tc(tmp_path, use_ema=False, amp_dtype=amp_dtype), _mc(amp=True))
return captured["precision"]
def test_amp_dtype_is_a_train_kwarg_not_dropped(self, tmp_path):
"""amp_dtype is a real TrainConfig field (reachable via train(**kwargs)), not silently dropped."""
assert _tc(tmp_path, amp_dtype="fp16").amp_dtype == "fp16"
@pytest.mark.parametrize(
"cuda, bf16, mps, amp_dtype, expected",
[
pytest.param(True, True, False, "auto", "bf16-mixed", id="auto-cuda-bf16"),
pytest.param(True, True, False, "fp16", "16-mixed", id="fp16-cuda-bf16"),
pytest.param(True, True, False, "bf16", "bf16-mixed", id="bf16-cuda-bf16"),
pytest.param(True, False, False, "auto", "16-mixed", id="auto-cuda-no-bf16"),
pytest.param(False, False, True, "fp16", "16-mixed", id="fp16-mps"),
],
)
def test_resolved_precision(self, tmp_path, cuda, bf16, mps, amp_dtype, expected):
"""amp_dtype + hardware caps resolve to the correct Lightning precision string."""
assert self._resolved_precision(tmp_path, cuda=cuda, bf16=bf16, mps=mps, amp_dtype=amp_dtype) == expected
@pytest.mark.parametrize(
"cuda, bf16, mps, amp_dtype, warn_match",
[
pytest.param(True, False, False, "bf16", "bf16", id="bf16-cuda-no-hw-support"),
pytest.param(False, False, True, "bf16", "MPS", id="bf16-mps"),
],
)
def test_resolved_precision_warns(self, tmp_path, cuda, bf16, mps, amp_dtype, warn_match):
"""amp_dtype falls back to '16-mixed' and emits a UserWarning when hardware cannot satisfy the request."""
with pytest.warns(UserWarning, match=warn_match):
precision = self._resolved_precision(tmp_path, cuda=cuda, bf16=bf16, mps=mps, amp_dtype=amp_dtype)
assert precision == "16-mixed"
def test_amp_false_overrides_amp_dtype(self, tmp_path):
"""Amp=False wins over any amp_dtype: precision is '32-true'."""
trainer = build_trainer(_tc(tmp_path, use_ema=False, amp_dtype="fp16"), _mc(amp=False))
assert trainer.precision == "32-true"
def test_cpu_accelerator_ignores_amp_dtype(self, tmp_path):
"""Explicit accelerator='cpu' yields '32-true' regardless of amp_dtype."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(_tc(tmp_path, use_ema=False, amp_dtype="fp16"), _mc(amp=True), accelerator="cpu")
assert captured["precision"] == "32-true"
@pytest.mark.parametrize(
"bad_value",
[
pytest.param("float8", id="string-float8"),
pytest.param(None, id="none"),
pytest.param(42, id="int"),
pytest.param(True, id="bool"),
],
)
def test_invalid_amp_dtype_falls_back_to_auto_with_warning(self, tmp_path, bad_value):
"""An unrecognised or wrong-typed amp_dtype falls back to 'auto' with a warning rather than raising."""
with pytest.warns(UserWarning, match="amp_dtype"):
tc = _tc(tmp_path, amp_dtype=bad_value)
assert tc.amp_dtype == "auto"
class TestBuildTrainerEMAShardingGuard:
"""EMA must be disabled and a UserWarning emitted for sharded strategies.
PTL validates strategy+accelerator compatibility at Trainer construction time, so tests that exercise sharded
strategies mock Trainer to capture the callback list without triggering platform-specific validation.
"""
@pytest.mark.parametrize(
"strategy",
[
pytest.param("fsdp", id="fsdp"),
pytest.param("deepspeed", id="deepspeed"),
pytest.param("deepspeed_stage_2", id="deepspeed_stage_2"),
],
)
def test_ema_disabled_for_sharded_strategy(self, tmp_path, strategy):
"""EMA callback must be absent when a sharded strategy is requested."""
import unittest.mock as mock
tc = _tc(tmp_path, use_ema=True)
# Inject strategy via monkey-patch (field not yet in TrainConfig until T4-2).
tc.__dict__["strategy"] = strategy
captured_callbacks = []
def _fake_trainer(**kwargs):
captured_callbacks.extend(kwargs.get("callbacks", []))
return mock.MagicMock()
with (
mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer),
warnings.catch_warnings(record=True),
):
warnings.simplefilter("always")
build_trainer(tc, _mc())
types = [type(cb) for cb in captured_callbacks]
assert RFDETREMACallback not in types
def test_ema_sharding_emits_user_warning(self, tmp_path):
"""A UserWarning is emitted when EMA is requested with a sharded strategy."""
import unittest.mock as mock
tc = _tc(tmp_path, use_ema=True)
tc.__dict__["strategy"] = "fsdp"
with (
mock.patch("rfdetr.training.trainer.Trainer", return_value=mock.MagicMock()),
warnings.catch_warnings(record=True) as caught,
):
warnings.simplefilter("always")
build_trainer(tc, _mc())
user_warns = [w for w in caught if issubclass(w.category, UserWarning)]
assert any("EMA disabled" in str(w.message) for w in user_warns)
def test_ema_enabled_for_non_sharded_strategy(self, tmp_path):
"""EMA callback must be present for non-sharded strategies."""
trainer = build_trainer(_tc(tmp_path, use_ema=True), _mc())
types = [type(cb) for cb in trainer.callbacks]
assert RFDETREMACallback in types
class TestBuildTrainerLoggers:
"""build_trainer() must wire loggers from TrainConfig flags."""
def test_no_loggers_always_has_csv_logger(self, tmp_path):
"""CSVLogger is always present even when all optional logger flags are off."""
from pytorch_lightning.loggers import CSVLogger
trainer = build_trainer(
_tc(tmp_path, use_ema=False), # _tc already sets all loggers to False
_mc(),
)
assert any(isinstance(lg, CSVLogger) for lg in trainer.loggers)
def test_tensorboard_logger_wired(self, tmp_path):
"""TensorBoardLogger is added when tensorboard=True (dep mocked)."""
import unittest.mock as mock
from pytorch_lightning.loggers import TensorBoardLogger
fake_logger = mock.MagicMock(spec=TensorBoardLogger)
with (
mock.patch("rfdetr.training.trainer._try_import_tensorboard_summary_writer"),
mock.patch("rfdetr.training.trainer.TensorBoardLogger", return_value=fake_logger),
):
trainer = build_trainer(
_tc(tmp_path, tensorboard=True, use_ema=False),
_mc(),
)
assert fake_logger in trainer.loggers
def test_mlflow_logger_wired(self, tmp_path):
"""MLFlowLogger is added when mlflow=True (dep mocked)."""
import unittest.mock as mock
from pytorch_lightning.loggers import MLFlowLogger
fake_logger = mock.MagicMock(spec=MLFlowLogger)
with mock.patch("rfdetr.training.trainer.MLFlowLogger", return_value=fake_logger):
trainer = build_trainer(
_tc(tmp_path, mlflow=True, use_ema=False),
_mc(),
)
assert fake_logger in trainer.loggers
def test_missing_tensorboard_dep_warns_not_crashes(self, tmp_path):
"""If tensorboard package is absent, a warning is logged and training continues."""
import unittest.mock as mock
with mock.patch(
"rfdetr.training.trainer._try_import_tensorboard_summary_writer",
side_effect=ModuleNotFoundError("no module named 'tensorboard'"),
):
with mock.patch("rfdetr.training.trainer._logger") as mock_logger:
trainer = build_trainer(
_tc(tmp_path, tensorboard=True, use_ema=False),
_mc(),
)
mock_logger.warning.assert_called_once()
assert "TensorBoard" in mock_logger.warning.call_args[0][0]
# CSVLogger is always present; TensorBoard was not added due to missing dep
from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger
assert all(not isinstance(lg, TensorBoardLogger) for lg in trainer.loggers)
assert any(isinstance(lg, CSVLogger) for lg in trainer.loggers)
def test_numpy2_tensorboard_incompatibility_warns_not_crashes(self, tmp_path):
"""AttributeError from NumPy 2.0/tensorflow incompatibility falls back to CSV logger."""
import unittest.mock as mock
numpy2_error = AttributeError("`np.float_` was removed in the NumPy 2.0 release. Use `np.float64` instead.")
with mock.patch(
"rfdetr.training.trainer._try_import_tensorboard_summary_writer",
side_effect=numpy2_error,
):
with mock.patch("rfdetr.training.trainer._logger") as mock_logger:
trainer = build_trainer(
_tc(tmp_path, tensorboard=True, use_ema=False),
_mc(),
)
mock_logger.warning.assert_called_once()
assert "TensorBoard" in mock_logger.warning.call_args[0][0]
from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger
assert all(not isinstance(lg, TensorBoardLogger) for lg in trainer.loggers)
assert any(isinstance(lg, CSVLogger) for lg in trainer.loggers)
def test_clearml_flag_raises_not_implemented(self, tmp_path):
"""Clearml=True must raise NotImplementedError (not yet supported)."""
with pytest.raises(NotImplementedError, match="ClearML"):
build_trainer(
_tc(tmp_path, clearml=True, use_ema=False),
_mc(),
)
def test_multiple_loggers_combined(self, tmp_path):
"""Multiple loggers can be wired simultaneously."""
import unittest.mock as mock
from pytorch_lightning.loggers import MLFlowLogger, TensorBoardLogger
fake_tb = mock.MagicMock(spec=TensorBoardLogger)
fake_mlflow = mock.MagicMock(spec=MLFlowLogger)
with (
mock.patch("rfdetr.training.trainer._try_import_tensorboard_summary_writer"),
mock.patch("rfdetr.training.trainer.TensorBoardLogger", return_value=fake_tb),
mock.patch("rfdetr.training.trainer.MLFlowLogger", return_value=fake_mlflow),
):
trainer = build_trainer(
_tc(tmp_path, tensorboard=True, mlflow=True, use_ema=False),
_mc(),
)
assert fake_tb in trainer.loggers
assert fake_mlflow in trainer.loggers
class TestBuildTrainerKwargs:
"""build_trainer() must pass the correct kwargs to Trainer."""
def test_gradient_clip_val_disabled_for_keypoint_manual_optimization(self, tmp_path):
"""Trainer-owned clipping is disabled for keypoint models because RFDETRModelModule clips manually."""
trainer = build_trainer(
_kp_tc(tmp_path, use_ema=False, clip_max_norm=0.25),
_mc(use_grouppose_keypoints=True),
)
assert trainer.gradient_clip_val is None
def test_gradient_clip_val_forwarded_for_detection_automatic_optimization(self, tmp_path):
"""Detection models use Lightning's automatic optimization; trainer-owned clipping must flow through."""
trainer = build_trainer(
_tc(tmp_path, use_ema=False, clip_max_norm=0.25),
_mc(),
)
assert trainer.gradient_clip_val == pytest.approx(0.25)
def test_accumulate_grad_batches_disabled_for_keypoint_manual_optimization(self, tmp_path):
"""Trainer-owned accumulation is disabled for keypoint models because RFDETRModelModule accumulates manually."""
trainer = build_trainer(
_kp_tc(tmp_path, grad_accum_steps=8, use_ema=False),
_mc(use_grouppose_keypoints=True),
)
assert trainer.accumulate_grad_batches == 1
def test_accumulate_grad_batches_forwarded_for_detection_automatic_optimization(self, tmp_path):
"""Detection models use Lightning's automatic optimization; ``accumulate_grad_batches`` must flow through."""
trainer = build_trainer(_tc(tmp_path, grad_accum_steps=8, use_ema=False), _mc())
assert trainer.accumulate_grad_batches == 8
def test_max_epochs(self, tmp_path):
"""max_epochs maps from config.epochs."""
trainer = build_trainer(_tc(tmp_path, epochs=42, use_ema=False), _mc())
assert trainer.max_epochs == 42
def test_log_every_n_steps(self, tmp_path):
"""log_every_n_steps is fixed at 50."""
trainer = build_trainer(_tc(tmp_path, use_ema=False), _mc())
assert trainer.log_every_n_steps == 50
def test_default_root_dir(self, tmp_path):
"""default_root_dir maps from config.output_dir."""
out = str(tmp_path / "my_output")
trainer = build_trainer(_tc(tmp_path, output_dir=out, use_ema=False), _mc())
assert str(trainer.default_root_dir) == out
def test_trainer_kwargs_can_override_precision(self, tmp_path):
"""Explicit trainer kwargs must override default precision without raising."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(
_tc(tmp_path, use_ema=False),
_mc(amp=True),
precision="32-true",
)
assert captured["precision"] == "32-true"
def test_keypoint_trainer_kwargs_cannot_override_manual_optimization_ownership(self, tmp_path):
"""Keypoint accumulation and clipping remain disabled even when passed as trainer kwargs, and the override emits
a UserWarning so the caller can spot the silent coercion."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with (
mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer),
pytest.warns(UserWarning, match="manual optimization"),
):
build_trainer(
_kp_tc(tmp_path, use_ema=False),
_mc(use_grouppose_keypoints=True),
accumulate_grad_batches=8,
gradient_clip_val=0.25,
)
assert captured["accumulate_grad_batches"] == 1
assert captured["gradient_clip_val"] is None
def test_detection_trainer_kwargs_override_takes_effect(self, tmp_path):
"""Detection models use automatic optimization; trainer kwargs must override the built-in defaults."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(
_tc(tmp_path, use_ema=False),
_mc(),
accumulate_grad_batches=8,
gradient_clip_val=0.25,
)
assert captured["accumulate_grad_batches"] == 8
assert captured["gradient_clip_val"] == pytest.approx(0.25)
class TestBuildTrainerSeed:
"""build_trainer() must not mutate global RNG state."""
def test_seed_is_not_applied_in_factory(self, tmp_path):
"""Seeding is deferred to RFDETRModule.on_fit_start (no factory side-effect)."""
import unittest.mock as mock
tc = _tc(tmp_path, use_ema=False, seed=42)
with mock.patch("pytorch_lightning.seed_everything") as mock_seed:
build_trainer(tc, _mc())
mock_seed.assert_not_called()
class TestBuildTrainerDDPFields:
"""build_trainer() must thread devices/num_nodes/strategy from TrainConfig to Trainer."""
def test_devices_threaded_from_train_config(self, tmp_path):
"""TrainConfig.devices is forwarded to Trainer(devices=...)."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, devices=4)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, _mc())
assert captured["devices"] == 4
def test_num_nodes_threaded_from_train_config(self, tmp_path):
"""TrainConfig.num_nodes is forwarded to Trainer(num_nodes=...)."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, num_nodes=2)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, _mc())
assert captured["num_nodes"] == 2
def test_strategy_threaded_from_train_config(self, tmp_path):
"""TrainConfig.strategy is forwarded to Trainer(strategy=...)."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, strategy="auto")
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, _mc())
assert captured["strategy"] == "auto"
def test_default_devices_is_1(self, tmp_path):
"""Default TrainConfig.devices must produce devices=1 (single-GPU default)."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, _mc())
assert captured["devices"] == 1
def test_default_num_nodes_is_1(self, tmp_path):
"""Default TrainConfig.num_nodes must produce num_nodes=1."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, _mc())
assert captured["num_nodes"] == 1
def test_devices_string_accepted(self, tmp_path):
"""TrainConfig.devices accepts a string value (e.g. '0,1')."""
tc = _tc(tmp_path, use_ema=False, devices="auto")
# Should not raise during config construction.
assert tc.devices == "auto"
class TestBuildTrainerKeypointDistributedGuard:
"""Keypoint mode must fail fast for unsupported distributed training settings."""
def test_keypoint_ddp_strategy_raises_clear_error(self, tmp_path):
"""Keypoint mode rejects explicit distributed strategy requests with a clear error."""
tc = _kp_tc(tmp_path, use_ema=False, strategy="ddp")
mc = _mc(use_grouppose_keypoints=True)
with pytest.raises(NotImplementedError, match="Keypoint training currently does not support distributed"):
build_trainer(tc, mc)
def test_keypoint_auto_devices_raises_when_cuda_has_multiple_devices(self, tmp_path):
"""Keypoint mode rejects devices='auto' when it would resolve to multi-GPU execution."""
tc = _kp_tc(tmp_path, use_ema=False, devices="auto")
mc = _mc(use_grouppose_keypoints=True)
with (
patch("rfdetr.training.trainer.torch.cuda.is_available", return_value=True),
patch("rfdetr.training.trainer.torch.cuda.device_count", return_value=2),
pytest.raises(NotImplementedError, match="Keypoint training currently does not support distributed"),
):
build_trainer(tc, mc)
def test_non_keypoint_ddp_strategy_wrapped_with_find_unused_parameters(self, tmp_path):
"""Non-keypoint mode with strategy='ddp' produces DDPStrategy(find_unused_parameters=True)."""
import unittest.mock as mock
from pytorch_lightning.strategies import DDPStrategy
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, strategy="ddp")
mc = _mc(use_grouppose_keypoints=False)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, mc)
strategy_obj = captured["strategy"]
assert isinstance(strategy_obj, DDPStrategy)
assert strategy_obj._ddp_kwargs.get("find_unused_parameters") is True
class TestBuildTrainerDDPFindUnusedParameters:
"""build_trainer() must enable find_unused_parameters for strategy='ddp' on both detection and segmentation."""
def test_auto_strategy_multiple_devices_enables_find_unused_parameters(self, tmp_path):
"""Strategy='auto' + devices > 1 must produce DDPStrategy(find_unused_parameters=True).
This covers the default strategy path where Lightning would otherwise select a distributed strategy without RF-
DETR's unused-parameter guard.
"""
import unittest.mock as mock
from pytorch_lightning.strategies import DDPStrategy
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, strategy="auto", devices=2)
mc = _mc(segmentation_head=False)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, mc)
strategy_obj = captured["strategy"]
assert isinstance(strategy_obj, DDPStrategy)
assert strategy_obj._ddp_kwargs.get("find_unused_parameters") is True
assert captured["devices"] == 2
def test_ddp_segmentation_enables_find_unused_parameters(self, tmp_path):
"""Strategy='ddp' + segmentation_head=True must produce DDPStrategy(find_unused_parameters=True).
One case of the broader unconditional rule: find_unused_parameters is enabled for all strategy='ddp'
requests. The segmentation head's sparse_forward() is one source of conditionally-unused parameters under
DDP.
"""
import unittest.mock as mock
from pytorch_lightning.strategies import DDPStrategy
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, strategy="ddp")
mc = _mc(segmentation_head=True)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, mc)
strategy_obj = captured["strategy"]
assert isinstance(strategy_obj, DDPStrategy)
assert strategy_obj._ddp_kwargs.get("find_unused_parameters") is True
def test_ddp_no_segmentation_enables_find_unused_parameters(self, tmp_path):
"""Strategy='ddp' for detection-only must produce DDPStrategy(find_unused_parameters=True).
Detection models can leave parameters unused under DDP (two-stage group_detr ModuleLists, conditional aux_loss
branches), so find_unused_parameters is enabled unconditionally for strategy='ddp' regardless of
segmentation_head. Regression test for
https://github.com/roboflow/rf-detr/issues/1093.
"""
import unittest.mock as mock
from pytorch_lightning.strategies import DDPStrategy
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, strategy="ddp")
mc = _mc(segmentation_head=False)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, mc)
strategy_obj = captured["strategy"]
assert isinstance(strategy_obj, DDPStrategy)
assert strategy_obj._ddp_kwargs.get("find_unused_parameters") is True
def test_ddp_spawn_segmentation_preserves_find_unused_parameters(self, tmp_path):
"""strategy='ddp_spawn' + segmentation_head=True must keep find_unused_parameters=True.
ddp_spawn is already replaced with an interactive-spawn DDPStrategy that has find_unused_parameters=True for
notebook compatibility. Segmentation must not accidentally drop that flag when the ddp_spawn path is taken
instead of the plain 'ddp' path.
"""
import unittest.mock as mock
from pytorch_lightning.strategies import DDPStrategy
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, strategy="ddp_spawn")
mc = _mc(segmentation_head=True)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, mc)
strategy_obj = captured["strategy"]
assert isinstance(strategy_obj, DDPStrategy)
assert strategy_obj._ddp_kwargs.get("find_unused_parameters") is True
def test_non_ddp_strategy_with_segmentation_is_unchanged(self, tmp_path):
"""Strategies other than 'ddp' must not be wrapped even when segmentation is on."""
import unittest.mock as mock
captured: dict = {}
def _fake_trainer(**kwargs):
captured.update(kwargs)
return mock.MagicMock()
tc = _tc(tmp_path, use_ema=False, strategy="auto")
mc = _mc(segmentation_head=True)
with mock.patch("rfdetr.training.trainer.Trainer", side_effect=_fake_trainer):
build_trainer(tc, mc)
assert captured["strategy"] == "auto"