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
137 lines
5.3 KiB
Python
137 lines
5.3 KiB
Python
# ------------------------------------------------------------------------
|
|
# RF-DETR
|
|
# Copyright (c) 2025 Roboflow. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
|
|
# ------------------------------------------------------------------------
|
|
"""Tests that unoptimized inference always runs the module in eval mode."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import PIL.Image
|
|
import pytest
|
|
import torch
|
|
|
|
from rfdetr import detr as detr_module
|
|
|
|
from .helpers import _BaseFakeRFDETR
|
|
|
|
|
|
class _FakeModelWithDropout(torch.nn.Module):
|
|
"""Minimal module whose behavior differs between train and eval mode."""
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.dropout = torch.nn.Dropout(p=0.5)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
"""Pass input through dropout, active only in train mode."""
|
|
return self.dropout(x)
|
|
|
|
|
|
class _FakeModelContext:
|
|
"""Minimal model context supplying the attributes predict() and train() need."""
|
|
|
|
def __init__(self) -> None:
|
|
self.device = torch.device("cpu")
|
|
self.resolution = 28
|
|
self.model = _FakeModelWithDropout()
|
|
self.inference_model = None
|
|
|
|
|
|
class _FakeRFDETR(_BaseFakeRFDETR):
|
|
"""Concrete test double: provides a dropout-bearing model for eval-mode tests."""
|
|
|
|
def get_model(self, config: SimpleNamespace) -> _FakeModelContext:
|
|
"""Return a minimal model context with a dropout-bearing module."""
|
|
return _FakeModelContext()
|
|
|
|
|
|
class TestUnoptimizedInferenceEvalMode:
|
|
"""`_ensure_eval_mode_for_unoptimized_inference` must keep the module in eval mode."""
|
|
|
|
def test_eval_mode_reasserted_after_train_round_trip(self) -> None:
|
|
"""Eval mode must be applied to whatever self.model.model currently points to.
|
|
|
|
``train()`` rebinds ``self.model.model`` to a brand-new module left in training mode, so eval must be re-applied
|
|
to the *current* object on every call — not to a cached reference captured at init.
|
|
"""
|
|
rfdetr = _FakeRFDETR()
|
|
|
|
# First inference call: warns once and switches to eval mode.
|
|
rfdetr._ensure_eval_mode_for_unoptimized_inference()
|
|
assert rfdetr.model.model.training is False
|
|
|
|
# Simulate train() rebinding self.model.model to a fresh training-mode module.
|
|
rfdetr.model.model = _FakeModelWithDropout()
|
|
assert rfdetr.model.model.training is True # new object starts in train mode
|
|
|
|
# Every subsequent inference call must re-assert eval on the *new* object.
|
|
rfdetr._ensure_eval_mode_for_unoptimized_inference()
|
|
assert rfdetr.model.model.training is False
|
|
|
|
def test_optimized_model_skips_eval_assertion(self) -> None:
|
|
"""When _is_optimized_for_inference is True, the method must be a no-op.
|
|
|
|
The compiled inference_model snapshot is already in eval mode; calling eval() on the stale self.model.model
|
|
would target the wrong object.
|
|
"""
|
|
rfdetr = _FakeRFDETR()
|
|
rfdetr._is_optimized_for_inference = True
|
|
rfdetr.model.model.train()
|
|
assert rfdetr.model.model.training is True # confirm starting state
|
|
|
|
rfdetr._ensure_eval_mode_for_unoptimized_inference()
|
|
|
|
assert rfdetr.model.model.training is True # must remain unchanged
|
|
|
|
def test_not_optimized_warning_emitted_only_once(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The not-optimized warning is logged at most once across repeated calls."""
|
|
warnings: list[str] = []
|
|
monkeypatch.setattr(detr_module.logger, "warning", lambda msg, *a, **k: warnings.append(msg))
|
|
|
|
rfdetr = _FakeRFDETR()
|
|
rfdetr._ensure_eval_mode_for_unoptimized_inference()
|
|
rfdetr.model.model.train()
|
|
rfdetr._ensure_eval_mode_for_unoptimized_inference()
|
|
rfdetr._ensure_eval_mode_for_unoptimized_inference()
|
|
|
|
assert len(warnings) == 1
|
|
|
|
def test_eval_mode_applied_on_every_call(self) -> None:
|
|
"""Eval() must run on every call, not just when the warning fires.
|
|
|
|
Simulate the code path where the warning has already been emitted
|
|
(``_has_warned_about_not_being_optimized_for_inference=True``) and verify
|
|
that ``eval()`` is still applied to the current module.
|
|
"""
|
|
rfdetr = _FakeRFDETR()
|
|
rfdetr._has_warned_about_not_being_optimized_for_inference = True
|
|
rfdetr.model.model.train()
|
|
|
|
rfdetr._ensure_eval_mode_for_unoptimized_inference()
|
|
|
|
assert rfdetr.model.model.training is False
|
|
|
|
def test_predict_puts_module_in_eval_mode(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Predict() must delegate to _ensure_eval_mode_for_unoptimized_inference, leaving module in eval mode."""
|
|
rfdetr = _FakeRFDETR()
|
|
img = PIL.Image.new("RGB", (640, 640), color=(128, 128, 128))
|
|
|
|
monkeypatch.setattr(
|
|
rfdetr.model.model,
|
|
"forward",
|
|
lambda batch: {"pred_logits": torch.zeros(1, 10, 81), "pred_boxes": torch.zeros(1, 10, 4)},
|
|
)
|
|
monkeypatch.setattr(
|
|
rfdetr.model,
|
|
"postprocess",
|
|
lambda preds, target_sizes: [
|
|
{"scores": torch.zeros(0), "labels": torch.zeros(0, dtype=torch.long), "boxes": torch.zeros(0, 4)}
|
|
],
|
|
raising=False,
|
|
)
|
|
|
|
rfdetr.predict(img)
|
|
|
|
assert rfdetr.model.model.training is False
|