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,136 @@
|
||||
# ------------------------------------------------------------------------
|
||||
# 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
|
||||
Reference in New Issue
Block a user