Files
roboflow--rf-detr/tests/inference/test_optimize_for_inference.py
T
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

533 lines
21 KiB
Python

# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Tests for RFDETR.optimize_for_inference()."""
from types import SimpleNamespace
from unittest.mock import patch
import pytest
import torch
from rfdetr.detr import RFDETR
class _FakeModel(torch.nn.Module):
"""Minimal nn.Module that satisfies the optimize_for_inference contract."""
def __init__(self) -> None:
super().__init__()
self.linear = torch.nn.Linear(1, 1)
def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]:
return {"pred_boxes": self.linear(x[:, :1, :1, :1].squeeze(-1).squeeze(-1))}
def export(self) -> None:
pass
class _FakeModelContext:
def __init__(self, device: torch.device | str = torch.device("cpu"), resolution: int = 28) -> None:
self.device = torch.device(device) if not isinstance(device, torch.device) else device
self.resolution = resolution
self.model = _FakeModel()
self.inference_model = None
class _FakeRFDETR(RFDETR):
def maybe_download_pretrain_weights(self) -> None:
return None
def get_model_config(self, **kwargs) -> SimpleNamespace:
return SimpleNamespace(num_channels=3)
def get_model(self, config: SimpleNamespace) -> _FakeModelContext:
return _FakeModelContext()
class TestOptimizeForInferenceDtype:
"""Dtype coercion and validation tests."""
def test_string_dtype_float32_is_accepted(self) -> None:
"""Passing dtype='float32' (str) should be coerced to torch.float32."""
rfdetr = _FakeRFDETR()
with patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model):
rfdetr.optimize_for_inference(compile=False, dtype="float32")
assert rfdetr._optimized_dtype == torch.float32
def test_string_dtype_float16_is_accepted(self) -> None:
"""Passing dtype='float16' (str) should be coerced to torch.float16."""
rfdetr = _FakeRFDETR()
with patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model):
rfdetr.optimize_for_inference(compile=False, dtype="float16")
assert rfdetr._optimized_dtype == torch.float16
def test_torch_dtype_is_passed_through(self) -> None:
"""Passing dtype=torch.float32 directly should work as before."""
rfdetr = _FakeRFDETR()
with patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model):
rfdetr.optimize_for_inference(compile=False, dtype=torch.float32)
assert rfdetr._optimized_dtype == torch.float32
def test_invalid_dtype_type_raises_type_error(self) -> None:
"""Passing an invalid dtype type (e.g. int) should raise TypeError."""
rfdetr = _FakeRFDETR()
with pytest.raises(TypeError, match="dtype must be a torch.dtype or a string name of a dtype"):
rfdetr.optimize_for_inference(compile=False, dtype=42) # type: ignore[arg-type]
def test_invalid_dtype_string_raises_type_error(self) -> None:
"""Passing a non-existent dtype string should raise TypeError with a descriptive message."""
rfdetr = _FakeRFDETR()
with pytest.raises(TypeError, match="dtype must be a torch.dtype or a string name of a dtype"):
rfdetr.optimize_for_inference(compile=False, dtype="not_a_dtype")
def test_valid_torch_attr_that_is_not_dtype_raises_type_error(self) -> None:
"""'Tensor' is a valid torch attribute but not a torch.dtype — should raise TypeError."""
rfdetr = _FakeRFDETR()
with pytest.raises(TypeError, match="dtype must be a torch.dtype or a string name of a dtype"):
rfdetr.optimize_for_inference(compile=False, dtype="Tensor") # type: ignore[arg-type]
@pytest.mark.parametrize("dtype_str", ["float32", "float16", "bfloat16"])
def test_string_dtype_variants_are_accepted(self, dtype_str: str) -> None:
"""Common dtype string names should be accepted and coerced to the matching torch.dtype."""
rfdetr = _FakeRFDETR()
expected = getattr(torch, dtype_str)
with patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model):
rfdetr.optimize_for_inference(compile=False, dtype=dtype_str)
assert rfdetr._optimized_dtype == expected
class TestOptimizeForInferenceCudaDeviceContext:
"""Verify that optimize_for_inference wraps operations in the correct device context."""
@pytest.mark.gpu
@patch("rfdetr.detr._move_model_context_to_device")
@patch("rfdetr.detr.deepcopy")
@patch("torch.cuda.device")
def test_cuda_device_context_manager_is_used_for_cuda_device(
self,
mock_cuda_device,
mock_deepcopy,
_mock_move_model_context_to_device,
) -> None:
"""torch.cuda.device() context should be entered when model is on CUDA."""
rfdetr = _FakeRFDETR()
# Simulate a CUDA device without actually requiring CUDA hardware
rfdetr.model.device = torch.device("cuda", 0)
mock_deepcopy.return_value = rfdetr.model.model
entered_devices: list[torch.device] = []
class _CapturingDeviceCtx:
def __init__(self, captured_device):
entered_devices.append(captured_device)
def __enter__(self):
return self
def __exit__(self, *args):
pass
mock_cuda_device.side_effect = _CapturingDeviceCtx
rfdetr.optimize_for_inference(compile=False, dtype=torch.float32)
assert len(entered_devices) == 1
assert entered_devices[0] == torch.device("cuda", 0)
def test_nullcontext_used_for_cpu_device(self) -> None:
"""contextlib.nullcontext() should be used when model is on CPU (no CUDA init)."""
rfdetr = _FakeRFDETR()
rfdetr.model.device = torch.device("cpu")
# torch.cuda.device should NOT be called for CPU devices
with (
patch("torch.cuda.device") as mock_cuda_device,
patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model),
):
rfdetr.optimize_for_inference(compile=False, dtype=torch.float32)
mock_cuda_device.assert_not_called()
@pytest.mark.gpu
@patch("rfdetr.detr._move_model_context_to_device")
@patch("rfdetr.detr.deepcopy")
@patch("torch.cuda.device")
def test_cuda_device_context_uses_model_device(
self,
mock_cuda_device,
mock_deepcopy,
_mock_move_model_context_to_device,
) -> None:
"""The device passed to torch.cuda.device() should match self.model.device."""
rfdetr = _FakeRFDETR()
expected_device = torch.device("cuda", 2)
rfdetr.model.device = expected_device
mock_deepcopy.return_value = rfdetr.model.model
captured: dict[str, torch.device] = {}
class _CapturingCtx:
def __init__(self, captured_device):
captured["device"] = captured_device
def __enter__(self):
return self
def __exit__(self, *args):
pass
mock_cuda_device.side_effect = _CapturingCtx
rfdetr.optimize_for_inference(compile=False)
assert captured.get("device") == expected_device
class TestOptimizeForInferenceCompile:
"""Tests for the compile=True path (JIT trace)."""
def test_compile_true_calls_jit_trace(self) -> None:
"""torch.jit.trace should be called with the model and a correctly-shaped dummy input."""
rfdetr = _FakeRFDETR()
mock_traced = rfdetr.model.model
with (
patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model),
patch("torch.jit.trace", return_value=mock_traced) as mock_trace,
):
rfdetr.optimize_for_inference(compile=True, batch_size=2)
assert mock_trace.called
dummy_input: torch.Tensor = mock_trace.call_args.args[1]
resolution = rfdetr.model.resolution
assert dummy_input.shape == (2, 3, resolution, resolution)
def test_compile_true_sets_compiled_flags(self) -> None:
"""_optimized_has_been_compiled=True and _optimized_batch_size should be set after compile=True."""
rfdetr = _FakeRFDETR()
with (
patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model),
patch("torch.jit.trace", return_value=rfdetr.model.model),
):
rfdetr.optimize_for_inference(compile=True, batch_size=4)
assert rfdetr._optimized_has_been_compiled is True
assert rfdetr._optimized_batch_size == 4
def test_compile_false_skips_jit_trace(self) -> None:
"""torch.jit.trace should NOT be called when compile=False."""
rfdetr = _FakeRFDETR()
with (
patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model),
patch("torch.jit.trace") as mock_trace,
):
rfdetr.optimize_for_inference(compile=False)
mock_trace.assert_not_called()
assert rfdetr._optimized_has_been_compiled is False
assert rfdetr._optimized_batch_size is None
class TestOptimizeForInferenceState:
"""Verify that optimize_for_inference correctly sets internal state flags."""
def test_is_optimized_inplace_false_before_optimization(self) -> None:
"""is_optimized_inplace is False before any optimization is applied."""
rfdetr = _FakeRFDETR()
assert rfdetr.is_optimized_inplace is False
def test_is_optimized_flag_set(self) -> None:
"""_is_optimized_for_inference should be True after optimization."""
rfdetr = _FakeRFDETR()
with patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model):
rfdetr.optimize_for_inference(compile=False)
assert rfdetr._is_optimized_for_inference is True
def test_inference_model_set(self) -> None:
"""model.inference_model should be set after optimization."""
rfdetr = _FakeRFDETR()
with patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model):
rfdetr.optimize_for_inference(compile=False)
assert rfdetr.model.inference_model is not None
def test_remove_optimized_model_clears_state(self) -> None:
"""remove_optimized_model() should clear all optimization flags."""
rfdetr = _FakeRFDETR()
with patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model):
rfdetr.optimize_for_inference(compile=False)
rfdetr.remove_optimized_model()
assert rfdetr._is_optimized_for_inference is False
assert rfdetr.model.inference_model is None
assert rfdetr._optimized_dtype is None
assert rfdetr._optimized_resolution is None
assert rfdetr._optimized_has_been_compiled is False
assert rfdetr._optimized_batch_size is None
assert rfdetr.is_optimized_inplace is False
class TestOptimizeForInferenceInplace:
"""Tests for the low-memory in-place optimization path."""
def test_inplace_false_keeps_deepcopy_behavior(self) -> None:
"""The default path should still deep-copy the loaded module."""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
copied_model = _FakeModel()
with patch("rfdetr.detr.deepcopy", return_value=copied_model) as mock_deepcopy:
rfdetr.optimize_for_inference(compile=False)
mock_deepcopy.assert_called_once_with(original_model)
assert rfdetr.model.model is original_model
assert rfdetr.model.inference_model is copied_model
assert rfdetr._is_optimized_for_inference is True
assert rfdetr.is_optimized_inplace is False
def test_inplace_true_compile_false_does_not_deepcopy(self) -> None:
"""Inplace=True with compile=False should use the loaded module directly."""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
with patch("rfdetr.detr.deepcopy") as mock_deepcopy:
rfdetr.optimize_for_inference(compile=False, inplace=True)
mock_deepcopy.assert_not_called()
assert rfdetr.model.model is None
assert rfdetr.model.inference_model is original_model
assert rfdetr._is_optimized_for_inference is True
assert rfdetr.is_optimized_inplace is True
def test_remove_optimized_model_after_inplace_warns_and_preserves_state(self) -> None:
"""remove_optimized_model() after inplace optimization issues UserWarning and no-ops."""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
rfdetr.optimize_for_inference(compile=False, inplace=True)
with pytest.warns(UserWarning, match="no effect after inplace optimization"):
rfdetr.remove_optimized_model()
assert rfdetr.model.model is None
assert rfdetr.model.inference_model is original_model
assert rfdetr._is_optimized_for_inference is True
assert rfdetr.is_optimized_inplace is True
def test_second_optimize_after_inplace_raises_runtime_error(self) -> None:
"""Calling optimize_for_inference() again after inplace=True raises RuntimeError."""
rfdetr = _FakeRFDETR()
rfdetr.optimize_for_inference(compile=False, inplace=True)
with pytest.raises(RuntimeError, match="base model has been cleared"):
rfdetr.optimize_for_inference(compile=False)
def test_inplace_true_default_dtype_float32_does_not_cast(self) -> None:
"""Inplace=True with default dtype (float32) leaves weights unchanged — no casting occurs."""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
original_dtype = original_model.linear.weight.dtype
rfdetr.optimize_for_inference(compile=False, inplace=True)
assert rfdetr.model.inference_model is original_model
assert original_model.linear.weight.dtype == original_dtype
assert rfdetr._optimized_dtype == torch.float32
assert rfdetr._optimized_has_been_compiled is False
assert rfdetr._optimized_batch_size is None
@pytest.mark.parametrize(
"dtype",
[
pytest.param(torch.float16, id="float16"),
pytest.param(torch.bfloat16, id="bfloat16"),
],
)
def test_inplace_true_allows_destructive_dtype_casting(self, dtype: torch.dtype) -> None:
"""In-place optimization may cast the original module to the target dtype."""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
rfdetr.optimize_for_inference(compile=False, dtype=dtype, inplace=True)
assert rfdetr.model.model is None
assert rfdetr.model.inference_model is original_model
assert original_model.linear.weight.dtype == dtype
assert rfdetr._optimized_dtype == dtype
def test_inplace_export_failure_keeps_base_model(self) -> None:
"""Export failure in the in-place path should not clear model.model."""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
with (
patch("rfdetr.detr.deepcopy") as mock_deepcopy,
patch.object(original_model, "export", side_effect=RuntimeError("export failed")),
pytest.raises(RuntimeError, match="export failed"),
):
rfdetr.optimize_for_inference(compile=False, inplace=True)
mock_deepcopy.assert_not_called()
assert rfdetr.model.model is original_model
assert rfdetr.model.inference_model is None
assert rfdetr._is_optimized_for_inference is False
assert rfdetr.is_optimized_inplace is False
@pytest.mark.parametrize(
"dtype",
[
pytest.param(torch.int8, id="torch-int8"),
pytest.param("int8", id="string-int8"),
],
)
def test_inplace_non_floating_dtype_raises_before_export(self, dtype: torch.dtype | str) -> None:
"""In-place optimization rejects non-floating dtypes before mutating the base model."""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
with (
patch("rfdetr.detr.deepcopy") as mock_deepcopy,
patch.object(original_model, "export") as mock_export,
pytest.raises(ValueError, match="floating-point torch.dtype"),
):
rfdetr.optimize_for_inference(compile=False, dtype=dtype, inplace=True)
mock_deepcopy.assert_not_called()
mock_export.assert_not_called()
assert rfdetr.model.model is original_model
assert rfdetr.model.inference_model is None
assert rfdetr._is_optimized_for_inference is False
assert rfdetr.is_optimized_inplace is False
def test_inplace_compile_true_raises_before_export_or_trace(self) -> None:
"""In-place optimization rejects compile=True before mutating the base model."""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
with (
patch("rfdetr.detr.deepcopy") as mock_deepcopy,
patch.object(original_model, "export") as mock_export,
patch("torch.jit.trace") as mock_trace,
pytest.raises(ValueError, match="inplace=True.*compile=False"),
):
rfdetr.optimize_for_inference(compile=True, inplace=True)
mock_deepcopy.assert_not_called()
mock_export.assert_not_called()
mock_trace.assert_not_called()
assert rfdetr.model.model is original_model
assert rfdetr.model.inference_model is None
assert rfdetr._is_optimized_for_inference is False
assert rfdetr._optimized_has_been_compiled is False
assert rfdetr._optimized_batch_size is None
assert rfdetr.is_optimized_inplace is False
class TestOptimizeForInferenceExceptionRecovery:
"""Verify state consistency when optimization fails mid-execution."""
def test_deepcopy_failure_leaves_clean_state(self) -> None:
"""If deepcopy raises, inference_model should be None and _is_optimized_for_inference False."""
rfdetr = _FakeRFDETR()
# Simulate a previously-optimized state to confirm remove_optimized_model ran
rfdetr._is_optimized_for_inference = True
rfdetr.model.inference_model = rfdetr.model.model
with (
patch("rfdetr.detr.deepcopy", side_effect=RuntimeError("deepcopy failed")),
pytest.raises(RuntimeError, match="deepcopy failed"),
):
rfdetr.optimize_for_inference(compile=False)
assert rfdetr.model.inference_model is None
assert rfdetr._is_optimized_for_inference is False
def test_export_failure_leaves_is_optimized_false(self) -> None:
"""If export() raises after deepcopy succeeds, _is_optimized_for_inference stays False."""
rfdetr = _FakeRFDETR()
fake_copy = _FakeModel()
with (
patch("rfdetr.detr.deepcopy", return_value=fake_copy),
patch.object(fake_copy, "export", side_effect=RuntimeError("export failed")),
pytest.raises(RuntimeError, match="export failed"),
):
rfdetr.optimize_for_inference(compile=False)
assert rfdetr._is_optimized_for_inference is False
def test_jit_trace_failure_leaves_compiled_flags_false(self) -> None:
"""If jit.trace raises, _optimized_has_been_compiled and _optimized_batch_size stay unset."""
rfdetr = _FakeRFDETR()
with (
patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model),
patch("torch.jit.trace", side_effect=RuntimeError("trace failed")),
pytest.raises(RuntimeError, match="trace failed"),
):
rfdetr.optimize_for_inference(compile=True, batch_size=2)
assert rfdetr._optimized_has_been_compiled is False
assert rfdetr._optimized_batch_size is None
def test_jit_trace_failure_leaves_model_fully_unoptimized(self) -> None:
"""jit.trace failure leaves both _is_optimized_for_inference=False and inference_model=None."""
rfdetr = _FakeRFDETR()
with (
patch("rfdetr.detr.deepcopy", return_value=rfdetr.model.model),
patch("torch.jit.trace", side_effect=RuntimeError("trace failed")),
pytest.raises(RuntimeError, match="trace failed"),
):
rfdetr.optimize_for_inference(compile=True)
assert rfdetr._is_optimized_for_inference is False
assert rfdetr.model.inference_model is None
def test_inplace_export_failure_module_mutations_are_not_undone(self) -> None:
"""RFDETR resets flags on export failure but cannot undo module-level mutations.
Production export() may mutate the module (e.g. forward->forward_export) before raising; those changes are not
reversed by the exception-recovery path.
"""
rfdetr = _FakeRFDETR()
original_model = rfdetr.model.model
mutated: dict[str, bool] = {"happened": False}
def _mutating_export() -> None:
mutated["happened"] = True
raise RuntimeError("export failed mid-mutation")
with (
patch("rfdetr.detr.deepcopy"),
patch.object(original_model, "export", side_effect=_mutating_export),
pytest.raises(RuntimeError, match="export failed mid-mutation"),
):
rfdetr.optimize_for_inference(compile=False, inplace=True)
assert rfdetr._is_optimized_for_inference is False
assert rfdetr.is_optimized_inplace is False
assert rfdetr.model.model is original_model
# The mutation happened and cannot be undone by RFDETR's recovery path
assert mutated["happened"] is True