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
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:
@@ -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."
|
||||
)
|
||||
Reference in New Issue
Block a user