项目文件夹

文件
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

433 行
18 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Smoke tests: Trainer(fast_dev_run=2).fit(module, datamodule) — T7.
Verifies that the PTL training loop runs end-to-end without error for both detection and segmentation configurations.
All heavy operations (build_model, build_criterion_and_postprocessors, build_dataset, get_param_dict) are patched so no
real dataset or GPU is required.
Chapter 1 gate: these must pass before Chapter 2 begins.
"""
import sys
from unittest.mock import MagicMock, patch
import pytest
import torch
from pytorch_lightning import Trainer
from rfdetr.config import SegmentationTrainConfig
from rfdetr.training import build_trainer
from rfdetr.training.module_data import RFDETRDataModule
from rfdetr.training.module_model import RFDETRModelModule
from .helpers import (
_fake_postprocess,
_FakeCriterion,
_FakeDataset,
_FakeDatasetWithMasks,
_FakePostProcess,
_make_param_dicts,
_TinyModel,
)
# ---------------------------------------------------------------------------
# Private helpers unique to smoke tests
# ---------------------------------------------------------------------------
def _make_trainer() -> Trainer:
"""Create a Trainer configured for minimal smoke-test runs."""
return Trainer(
fast_dev_run=2,
accelerator="cpu",
enable_progress_bar=False,
enable_model_summary=False,
logger=False,
)
# ---------------------------------------------------------------------------
# Smoke test classes
# ---------------------------------------------------------------------------
class TestDetectionSmoke:
"""Trainer(fast_dev_run=2).fit() must complete without error for detection."""
def test_fit_runs_without_error(self, base_model_config, base_train_config):
"""Full PTL fit loop runs 2 train + 2 val batches without raising."""
mc = base_model_config()
tc = base_train_config()
tiny_model = _TinyModel()
fake_criterion = _FakeCriterion()
fake_postprocess = MagicMock(side_effect=_fake_postprocess)
fake_dataset = _FakeDataset(length=20)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=tiny_model),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(fake_criterion, fake_postprocess),
),
patch("rfdetr.training.module_data.build_dataset", return_value=fake_dataset),
patch(
"rfdetr.training.module_model.get_param_dict",
side_effect=lambda args, model: _make_param_dicts(model),
),
):
module = RFDETRModelModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
_make_trainer().fit(module, datamodule)
def test_training_step_called_expected_times(self, base_model_config, base_train_config):
"""fast_dev_run=2 must run exactly 2 training steps."""
mc = base_model_config()
tc = base_train_config()
tiny_model = _TinyModel()
fake_criterion = _FakeCriterion()
fake_postprocess = MagicMock(side_effect=_fake_postprocess)
fake_dataset = _FakeDataset(length=20)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=tiny_model),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(fake_criterion, fake_postprocess),
),
patch("rfdetr.training.module_data.build_dataset", return_value=fake_dataset),
patch(
"rfdetr.training.module_model.get_param_dict",
side_effect=lambda args, model: _make_param_dicts(model),
),
):
module = RFDETRModelModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
original_training_step = module.training_step
call_count = []
def _counting_training_step(batch, batch_idx):
call_count.append(1)
return original_training_step(batch, batch_idx)
module.training_step = _counting_training_step
_make_trainer().fit(module, datamodule)
assert sum(call_count) == 2
def test_val_step_called_expected_times(self, base_model_config, base_train_config):
"""fast_dev_run=2 must run exactly 2 validation steps."""
mc = base_model_config()
tc = base_train_config()
tiny_model = _TinyModel()
fake_criterion = _FakeCriterion()
fake_postprocess = MagicMock(side_effect=_fake_postprocess)
fake_dataset = _FakeDataset(length=20)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=tiny_model),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(fake_criterion, fake_postprocess),
),
patch("rfdetr.training.module_data.build_dataset", return_value=fake_dataset),
patch(
"rfdetr.training.module_model.get_param_dict",
side_effect=lambda args, model: _make_param_dicts(model),
),
):
module = RFDETRModelModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
original_validation_step = module.validation_step
call_count = []
def _counting_val_step(batch, batch_idx):
call_count.append(1)
return original_validation_step(batch, batch_idx)
module.validation_step = _counting_val_step
_make_trainer().fit(module, datamodule)
assert sum(call_count) == 2
def test_loss_decreases_or_is_finite(self, base_model_config, base_train_config):
"""Training loss must be finite (not NaN/inf) for the run to be valid."""
mc = base_model_config()
tc = base_train_config()
tiny_model = _TinyModel()
fake_postprocess = MagicMock(side_effect=_fake_postprocess)
fake_dataset = _FakeDataset(length=20)
losses = []
def _recording_criterion(outputs, targets, num_boxes=None):
dummy = outputs.get("dummy", torch.zeros(1))
denominator = fake_criterion.num_boxes_for_targets(outputs, targets) if num_boxes is None else num_boxes
loss = dummy.mean() / denominator
losses.append(loss.detach().item())
return {"loss_ce": loss}
fake_criterion = MagicMock(side_effect=_recording_criterion)
fake_criterion.weight_dict = {"loss_ce": 1.0}
fake_criterion.num_boxes_for_targets.return_value = torch.tensor(1.0)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=tiny_model),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(fake_criterion, fake_postprocess),
),
patch("rfdetr.training.module_data.build_dataset", return_value=fake_dataset),
patch(
"rfdetr.training.module_model.get_param_dict",
side_effect=lambda args, model: _make_param_dicts(model),
),
):
module = RFDETRModelModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
_make_trainer().fit(module, datamodule)
assert len(losses) > 0
assert all(torch.isfinite(torch.tensor(v)) for v in losses)
class TestSegmentationSmoke:
"""Trainer(fast_dev_run=2).fit() must complete without error for segmentation."""
def test_fit_runs_without_error(self, base_model_config, seg_train_config):
"""Full PTL fit loop runs 2 train + 2 val batches without raising."""
mc = base_model_config(segmentation_head=True)
tc = seg_train_config()
tiny_model = _TinyModel()
fake_criterion = _FakeCriterion()
fake_postprocess = MagicMock(side_effect=_fake_postprocess)
fake_dataset = _FakeDatasetWithMasks(length=20)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=tiny_model),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(fake_criterion, fake_postprocess),
),
patch("rfdetr.training.module_data.build_dataset", return_value=fake_dataset),
patch(
"rfdetr.training.module_model.get_param_dict",
side_effect=lambda args, model: _make_param_dicts(model),
),
):
module = RFDETRModelModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
_make_trainer().fit(module, datamodule)
def test_segmentation_config_accepted(self, base_model_config, seg_train_config):
"""SegmentationTrainConfig must be accepted by both module and datamodule."""
mc = base_model_config(segmentation_head=True)
tc = seg_train_config()
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=_TinyModel()),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(_FakeCriterion(), MagicMock(side_effect=_fake_postprocess)),
),
patch("rfdetr.training.module_data.build_dataset", return_value=_FakeDatasetWithMasks()),
patch(
"rfdetr.training.module_model.get_param_dict",
side_effect=lambda args, model: _make_param_dicts(model),
),
):
module = RFDETRModelModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
assert isinstance(module.train_config, SegmentationTrainConfig)
assert isinstance(datamodule.train_config, SegmentationTrainConfig)
class TestBuildTrainerSmoke:
"""Smoke tests for the ``build_trainer()`` public factory.
Verifies that the full callback stack wired by ``build_trainer`` runs end-to-end with ``fast_dev_run``, using mocked
internals so no real dataset or GPU is required.
"""
def test_fit_via_build_trainer(self, base_model_config, base_train_config):
"""build_trainer() + trainer.fit(module, datamodule=datamodule) must not raise."""
mc = base_model_config()
tc = base_train_config(use_ema=False, run_test=False)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=_TinyModel()),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(_FakeCriterion(), MagicMock(side_effect=_fake_postprocess)),
),
patch("rfdetr.training.module_data.build_dataset", return_value=_FakeDataset(length=20)),
patch(
"rfdetr.training.module_model.get_param_dict",
side_effect=lambda args, model: _make_param_dicts(model),
),
):
module = RFDETRModelModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
trainer = build_trainer(tc, mc, accelerator="cpu", fast_dev_run=2)
trainer.fit(module, datamodule=datamodule)
class _DDPModule(RFDETRModelModule):
"""RFDETRModelModule subclass for ddp_spawn smoke tests.
Overrides ``configure_optimizers`` so ``get_param_dict`` is never called in child processes. ``ddp_spawn`` forks
child processes that unpack a pickled copy of this module; patches applied in the parent process are not visible in
children, so the real ``get_param_dict`` would be called and would fail on ``_TinyModel`` (no ``.backbone``
attribute).
Must be defined at module level so ``pickle`` can look up the class by qualified name when deserialising in the
child process.
"""
def configure_optimizers(self):
"""Minimal single-group AdamW — bypasses get_param_dict."""
return torch.optim.AdamW(self.parameters(), lr=1e-4)
class _MultiScaleCheckDDPModule(RFDETRModelModule):
"""DDP-safe module that asserts on_train_batch_start mutation reaches training_step.
With multi_scale=True and _FakeDataset's 32×32 images, on_train_batch_start interpolates samples.tensors to a multi-
scale resolution (≥392 for RFDETRBaseConfig resolution=560). This module raises AssertionError in training_step if
the tensor height is still 32, meaning the in-place NestedTensor mutation did not propagate through the PTL batch-
hook chain.
Must be defined at module level so pickle can look up the class by qualified name when ddp_spawn deserialises it in
the child process.
Regression guard for issue #952.
"""
def configure_optimizers(self):
"""Minimal single-group AdamW — bypasses get_param_dict."""
return torch.optim.AdamW(self.parameters(), lr=1e-4)
def training_step(self, batch, batch_idx):
"""Assert resize from on_train_batch_start propagated before calling super."""
samples, _ = batch
h = samples.tensors.shape[2]
if h == 32:
raise AssertionError(
f"training_step received images at original 32-px height (h={h}). "
"on_train_batch_start's in-place NestedTensor mutation did not "
"propagate through the PTL hook chain. "
"Regression of issue #952: resize bypass in DDP batch-hook chain."
)
return super().training_step(batch, batch_idx)
# ---------------------------------------------------------------------------
# Multi-scale hook propagation tests (issue #952 regression)
# ---------------------------------------------------------------------------
class TestMultiScaleHookPropagation:
"""on_train_batch_start resize must propagate to training_step via NestedTensor mutation.
_FakeDataset emits 32×32 images. With multi_scale=True and RFDETRBaseConfig(resolution=560, patch_size=14,
num_windows=4) the computed scales start at 392, so none equal 32. _MultiScaleCheckDDPModule raises AssertionError
in training_step if h==32, making trainer.fit() fail when the in-place mutation does not propagate.
"""
def test_mutation_persists_to_training_step(self, base_model_config, base_train_config):
"""Single-process: training_step must see resized tensors, not original 32×32."""
mc = base_model_config()
tc = base_train_config(multi_scale=True, use_ema=False, run_test=False)
fake_dataset = _FakeDataset(length=20)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=_TinyModel()),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(_FakeCriterion(), _FakePostProcess()),
),
patch("rfdetr.training.module_data.build_dataset", return_value=fake_dataset),
):
module = _MultiScaleCheckDDPModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
trainer = build_trainer(tc, mc, accelerator="cpu", fast_dev_run=2)
trainer.fit(module, datamodule=datamodule)
# Windows CI currently cannot run this smoke test because gloo DDP spawn fails
# with makeDeviceForHostname unsupported-device errors.
@pytest.mark.skipif(sys.platform == "win32", reason="gloo DDP spawn unsupported on Windows CI")
def test_ddp_spawn_fit_runs_without_error(base_model_config, base_train_config):
"""ddp_spawn with 2 CPU workers must run fast_dev_run=2 without error.
``ddp_spawn`` forks child processes, so all objects passed to ``trainer.fit()`` must be picklable. ``MagicMock`` is
NOT picklable; this test uses ``_FakePostProcess``, plain dataset instances, and ``_DDPModule`` (module-level class)
instead.
"""
mc = base_model_config()
tc = base_train_config(use_ema=False, run_test=False, devices=2, strategy="ddp_spawn")
fake_dataset = _FakeDataset(length=20)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=_TinyModel()),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(_FakeCriterion(), _FakePostProcess()),
),
):
module = _DDPModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
# Pre-set datasets: build_dataset mock doesn't survive the spawn boundary.
datamodule._dataset_train = fake_dataset
datamodule._dataset_val = fake_dataset
trainer = build_trainer(tc, mc, accelerator="cpu", fast_dev_run=2)
trainer.fit(module, datamodule=datamodule)
@pytest.mark.skipif(sys.platform == "win32", reason="gloo DDP spawn unsupported on Windows CI")
def test_ddp_spawn_multi_scale_mutation_propagates(base_model_config, base_train_config):
"""ddp_spawn with multi_scale=True must propagate on_train_batch_start resize to training_step.
_MultiScaleCheckDDPModule raises AssertionError in training_step when the NestedTensor height is still 32 (original
_FakeDataset size). If trainer.fit() completes without error the PTL batch-hook reference chain is intact in DDP,
i.e. the in-place mutation in on_train_batch_start is visible in training_step on both workers.
Regression test for issue #952 on CPU DDP (non-Windows): confirms the transforms/resize propagation is not a
Windows-only concern.
"""
mc = base_model_config()
tc = base_train_config(multi_scale=True, use_ema=False, run_test=False, devices=2, strategy="ddp_spawn")
fake_dataset = _FakeDataset(length=20)
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=_TinyModel()),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(_FakeCriterion(), _FakePostProcess()),
),
):
module = _MultiScaleCheckDDPModule(mc, tc)
datamodule = RFDETRDataModule(mc, tc)
# Pre-set datasets: build_dataset mock doesn't survive the spawn boundary.
datamodule._dataset_train = fake_dataset
datamodule._dataset_val = fake_dataset
trainer = build_trainer(tc, mc, accelerator="cpu", fast_dev_run=2)
trainer.fit(module, datamodule=datamodule)