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
229 lines
7.9 KiB
Python
229 lines
7.9 KiB
Python
# ------------------------------------------------------------------------
|
|
# RF-DETR
|
|
# Copyright (c) 2025 Roboflow. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
|
|
# ------------------------------------------------------------------------
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from rfdetr.detr import RFDETR
|
|
from rfdetr.training import auto_batch
|
|
from rfdetr.training.auto_batch import AutoBatchResult
|
|
|
|
|
|
class _TinyModule(torch.nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.w = torch.nn.Parameter(torch.ones(1))
|
|
|
|
|
|
def test_recommend_grad_accum_steps_rounds_up():
|
|
assert auto_batch.recommend_grad_accum_steps(3, 16) == 6
|
|
|
|
|
|
def test_probe_max_micro_batch_uses_exponential_then_binary_search():
|
|
model = _TinyModule()
|
|
criterion = _TinyModule()
|
|
threshold = 7
|
|
|
|
def _fake_probe(*args, **kwargs):
|
|
micro_batch_size = args[2]
|
|
return micro_batch_size <= threshold
|
|
|
|
with (
|
|
patch("rfdetr.training.auto_batch._probe_step", side_effect=_fake_probe),
|
|
patch("rfdetr.training.auto_batch.torch.cuda.empty_cache"),
|
|
):
|
|
safe = auto_batch.probe_max_micro_batch(
|
|
model=model,
|
|
criterion=criterion,
|
|
resolution=64,
|
|
device=torch.device("cuda"),
|
|
num_classes=5,
|
|
amp=False,
|
|
safety_margin=1.0,
|
|
max_micro_batch=32,
|
|
)
|
|
assert safe == threshold
|
|
|
|
|
|
def test_probe_max_micro_batch_raises_if_one_is_not_safe():
|
|
model = _TinyModule()
|
|
criterion = _TinyModule()
|
|
|
|
with (
|
|
patch("rfdetr.training.auto_batch._probe_step", return_value=False),
|
|
patch("rfdetr.training.auto_batch.torch.cuda.empty_cache"),
|
|
pytest.raises(RuntimeError, match="micro_batch_size=1"),
|
|
):
|
|
auto_batch.probe_max_micro_batch(
|
|
model=model,
|
|
criterion=criterion,
|
|
resolution=64,
|
|
device=torch.device("cuda"),
|
|
num_classes=5,
|
|
amp=False,
|
|
)
|
|
|
|
|
|
def test_probe_step_raises_when_loss_keys_do_not_overlap_weight_keys():
|
|
"""_probe_step must fail fast when weighted loss would be empty."""
|
|
|
|
class _DummyCriterion(torch.nn.Module):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.weight_dict = {"loss_bbox": 1.0}
|
|
|
|
def forward(self, outputs, targets):
|
|
return {"loss_ce": torch.tensor(1.0)}
|
|
|
|
class _DummyModel(torch.nn.Module):
|
|
def forward(self, samples, targets):
|
|
return {}
|
|
|
|
model = _DummyModel()
|
|
criterion = _DummyCriterion()
|
|
|
|
with (
|
|
patch(
|
|
"rfdetr.training.auto_batch._make_synthetic_batch",
|
|
return_value=(MagicMock(), []),
|
|
),
|
|
pytest.raises(RuntimeError, match="no overlap between criterion loss_dict and weight_dict keys"),
|
|
):
|
|
auto_batch._probe_step(
|
|
model=model,
|
|
criterion=criterion,
|
|
micro_batch_size=1,
|
|
resolution=64,
|
|
device=torch.device("cpu"),
|
|
num_classes=5,
|
|
amp=False,
|
|
)
|
|
|
|
|
|
def test_resolve_auto_batch_config_requires_cuda():
|
|
model_context = SimpleNamespace(device=torch.device("cpu"), model=MagicMock())
|
|
model_config = SimpleNamespace(resolution=64, num_classes=5, amp=False, segmentation_head=False)
|
|
train_config = SimpleNamespace(batch_size="auto", auto_batch_target_effective=16)
|
|
|
|
with (
|
|
patch("rfdetr.training.auto_batch.torch.cuda.is_available", return_value=False),
|
|
pytest.raises(RuntimeError, match="requires a CUDA device"),
|
|
):
|
|
auto_batch.resolve_auto_batch_config(model_context, model_config, train_config)
|
|
|
|
|
|
def test_resolve_auto_batch_config_returns_expected_values():
|
|
model_context = SimpleNamespace(device=torch.device("cuda"), model=MagicMock())
|
|
model_config = SimpleNamespace(resolution=64, num_classes=5, amp=False, segmentation_head=True)
|
|
train_config = SimpleNamespace(batch_size="auto", auto_batch_target_effective=16)
|
|
criterion = MagicMock()
|
|
criterion.to.return_value = criterion
|
|
|
|
with (
|
|
patch("rfdetr.training.auto_batch.torch.cuda.is_available", return_value=True),
|
|
patch("rfdetr.training.auto_batch.build_criterion_from_config", return_value=(criterion, None)),
|
|
patch("rfdetr.training.auto_batch.probe_max_micro_batch", return_value=5),
|
|
patch("rfdetr.training.auto_batch.torch.cuda.get_device_name", return_value="Fake GPU"),
|
|
):
|
|
result = auto_batch.resolve_auto_batch_config(model_context, model_config, train_config)
|
|
|
|
assert isinstance(result, AutoBatchResult)
|
|
assert result.safe_micro_batch == 5
|
|
assert result.recommended_grad_accum_steps == 4
|
|
assert result.effective_batch_size == 20
|
|
assert result.device_name == "Fake GPU"
|
|
|
|
|
|
@patch("rfdetr.detr.is_main_process", return_value=False)
|
|
@patch("rfdetr.training.auto_batch.resolve_auto_batch_config")
|
|
@patch("rfdetr.training.build_trainer")
|
|
@patch("rfdetr.training.RFDETRDataModule")
|
|
@patch("rfdetr.training.RFDETRModelModule")
|
|
@patch("rfdetr.detr._move_model_context_to_device")
|
|
def test_train_auto_batch_ensures_model_on_device_before_resolve(
|
|
mock_move: MagicMock,
|
|
_mock_module: MagicMock,
|
|
_mock_data_module: MagicMock,
|
|
_mock_build_trainer: MagicMock,
|
|
mock_resolve: MagicMock,
|
|
_mock_is_main: MagicMock,
|
|
) -> None:
|
|
"""Model weights must be moved before resolve_auto_batch_config when batch_size='auto'."""
|
|
auto_result = SimpleNamespace(safe_micro_batch=4, recommended_grad_accum_steps=1, effective_batch_size=4)
|
|
call_order: list[str] = []
|
|
|
|
def _move_side_effect(model: object) -> None:
|
|
call_order.append("ensure")
|
|
|
|
def _resolve_side_effect(**_kwargs: object) -> object:
|
|
call_order.append("resolve")
|
|
return auto_result
|
|
|
|
mock_move.side_effect = _move_side_effect
|
|
mock_resolve.side_effect = _resolve_side_effect
|
|
|
|
train_config = SimpleNamespace(
|
|
batch_size="auto",
|
|
grad_accum_steps=99,
|
|
dataset_dir=None,
|
|
resume=None,
|
|
class_names=None,
|
|
save_dataset_grids=False,
|
|
)
|
|
mock_self = MagicMock()
|
|
mock_self.model_config = SimpleNamespace(model_name=None)
|
|
mock_self.get_train_config.return_value = train_config
|
|
|
|
RFDETR.train(mock_self)
|
|
|
|
assert train_config.batch_size == 4
|
|
assert train_config.grad_accum_steps == 1
|
|
mock_move.assert_called_once_with(mock_self.model)
|
|
mock_resolve.assert_called_once_with(
|
|
model_context=mock_self.model,
|
|
model_config=mock_self.model_config,
|
|
train_config=train_config,
|
|
)
|
|
assert call_order == ["ensure", "resolve"]
|
|
|
|
|
|
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required for segmentation probe")
|
|
def test_probe_step_with_real_segmentation_criterion(tmp_path):
|
|
"""Run one probe step with real segmentation model and criterion so loss_masks and t['masks'] are exercised."""
|
|
from rfdetr._namespace import _namespace_from_configs
|
|
from rfdetr.config import RFDETRSegNanoConfig, SegmentationTrainConfig
|
|
from rfdetr.models.lwdetr import build_criterion_and_postprocessors, build_model
|
|
|
|
mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cuda", num_classes=2)
|
|
tc = SegmentationTrainConfig(
|
|
dataset_dir=str(tmp_path / "ds"),
|
|
output_dir=str(tmp_path / "out"),
|
|
batch_size=2,
|
|
grad_accum_steps=1,
|
|
tensorboard=False,
|
|
)
|
|
args = _namespace_from_configs(mc, tc)
|
|
model = build_model(args)
|
|
criterion, _ = build_criterion_and_postprocessors(args)
|
|
device = torch.device("cuda")
|
|
model = model.to(device)
|
|
criterion = criterion.to(device)
|
|
|
|
ok = auto_batch._probe_step(
|
|
model=model,
|
|
criterion=criterion,
|
|
micro_batch_size=1,
|
|
resolution=mc.resolution,
|
|
device=device,
|
|
num_classes=mc.num_classes,
|
|
amp=False,
|
|
segmentation_head=True,
|
|
)
|
|
assert ok is True
|