chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:26:24 +08:00
commit 16031aae96
343 changed files with 88674 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Integration tests: metrics.csv contains all columns used by plot_metrics().
Runs a minimal PTL training loop (1 epoch, 2 batches each) using mocked model internals so no real dataset or GPU is
required. After training, reads the CSVLogger output and asserts that every metric column that ``plot_metrics()`` needs
is present and has at least one non-NaN value.
Also verifies that ``train/loss`` is logged at the same scale as ``val/loss`` (i.e. NOT divided by ``grad_accum_steps``
before logging).
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pandas as pd
import torch
from rfdetr.config import RFDETRBaseConfig, TrainConfig
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, _make_param_dicts, _TinyModel
# ---------------------------------------------------------------------------
# Helpers local to this module
# ---------------------------------------------------------------------------
def _fit_and_read_csv(mc: RFDETRBaseConfig, tc: TrainConfig, criterion=None) -> pd.DataFrame:
"""Run 1 epoch (2 train + 2 val batches) and return the resulting metrics.csv."""
fake_criterion = criterion or _FakeCriterion()
with (
patch("rfdetr.training.module_model.build_model_from_config", return_value=_TinyModel()),
patch(
"rfdetr.training.module_model.build_criterion_from_config",
return_value=(fake_criterion, 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",
max_epochs=1,
limit_train_batches=2,
limit_val_batches=2,
log_every_n_steps=1,
)
trainer.fit(module, datamodule=datamodule)
csv_path = Path(tc.output_dir) / "metrics.csv"
assert csv_path.exists(), "CSVLogger must write metrics.csv to output_dir"
return pd.read_csv(csv_path)
# ---------------------------------------------------------------------------
# Expected columns (must exist and have ≥1 non-NaN row after one epoch)
# ---------------------------------------------------------------------------
_REQUIRED_DETECTION = frozenset(
{
"train/loss",
"train/lr",
"val/loss",
"val/mAP_50",
"val/mAP_50_95",
"val/mAR",
}
)
_REQUIRED_DETECTION_EMA = _REQUIRED_DETECTION | frozenset(
{
"val/ema_mAP_50",
"val/ema_mAP_50_95",
"val/ema_mAR",
}
)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestDetectionMetricsCSV:
"""metrics.csv contains all columns that plot_metrics() needs for detection."""
def test_base_metrics_present_without_ema(self, base_model_config, base_train_config):
"""Without EMA all core val/* columns must appear in metrics.csv with non-NaN data."""
mc = base_model_config()
tc = base_train_config(use_ema=False, run_test=False)
df = _fit_and_read_csv(mc, tc)
missing = _REQUIRED_DETECTION - set(df.columns)
assert not missing, f"Missing columns in metrics.csv: {sorted(missing)}"
all_nan = {c for c in _REQUIRED_DETECTION if df[c].isna().all()}
assert not all_nan, f"Columns with all-NaN values: {sorted(all_nan)}"
def test_ema_metrics_present_with_ema_enabled(self, base_model_config, base_train_config):
"""With use_ema=True the ema_* aliases must also appear in metrics.csv."""
mc = base_model_config()
tc = base_train_config(use_ema=True, run_test=False)
df = _fit_and_read_csv(mc, tc)
missing = _REQUIRED_DETECTION_EMA - set(df.columns)
assert not missing, f"Missing EMA columns in metrics.csv: {sorted(missing)}"
all_nan = {c for c in _REQUIRED_DETECTION_EMA if df[c].isna().all()}
assert not all_nan, f"EMA columns with all-NaN values: {sorted(all_nan)}"
def test_train_loss_is_unscaled(self, base_model_config, base_train_config):
"""Train/loss must be logged at the raw criterion scale, not divided by grad_accum_steps.
With grad_accum_steps=4 the old code divided the logged value by 4, making train/loss ~4× smaller than val/loss.
After the fix the logged value equals the raw weighted criterion output so both losses are on the same scale.
"""
fixed_loss_value = 5.0
grad_accum_steps = 4
class _FixedCriterion:
weight_dict = {"loss_ce": 1.0}
def num_boxes_for_targets(self, outputs, targets):
dummy = outputs.get("dummy", torch.zeros(1))
return torch.ones((), dtype=dummy.dtype, device=dummy.device)
def __call__(self, outputs, targets, num_boxes=None):
# Loss is always fixed_loss_value, connected to model params for gradient.
dummy = outputs.get("dummy", torch.zeros(1))
denominator = self.num_boxes_for_targets(outputs, targets) if num_boxes is None else num_boxes
return {"loss_ce": (dummy.mean() * 0 + fixed_loss_value) / denominator}
mc = base_model_config()
tc = base_train_config(use_ema=False, run_test=False, grad_accum_steps=grad_accum_steps)
df = _fit_and_read_csv(mc, tc, criterion=_FixedCriterion())
logged = df["train/loss"].dropna().mean()
expected_unscaled = fixed_loss_value
expected_if_divided = fixed_loss_value / grad_accum_steps
assert abs(logged - expected_unscaled) < abs(logged - expected_if_divided), (
f"train/loss={logged:.4f} is closer to the grad-accum-divided value "
f"({expected_if_divided:.4f}) than the raw criterion output "
f"({expected_unscaled:.4f}). The division must have been removed."
)