项目文件夹

文件
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

871 行
42 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Regression tests for fine-tuned checkpoint weight destruction.
When a user loads a fine-tuned N-class checkpoint but has ``num_classes`` configured to a LARGER value (e.g. default
90), the second reinit in ``load_pretrain_weights`` (models/weights.py) must NOT erroneously resize the detection head
to ``num_classes + 1``, destroying the loaded weights.
The fix changes the second reinit condition from:
``checkpoint_num_classes != args.num_classes + 1``
to the user-override-aware logic that auto-aligns to the checkpoint when the user did not explicitly set
``num_classes``.
These tests exercise ``rfdetr.models.weights.load_pretrain_weights`` directly, which is the unified function that
replaced the two prior separate implementations (``detr.py:_load_pretrain_weights_into`` and
``module_model.py:RFDETRModelModule._load_pretrain_weights``).
"""
from types import SimpleNamespace
from unittest.mock import MagicMock, call
import pytest
import torch
from rfdetr.config import (
RFDETRBaseConfig,
RFDETRKeypointPreviewConfig,
RFDETRLargeConfig,
RFDETRMediumConfig,
RFDETRNanoConfig,
RFDETRSeg2XLargeConfig,
RFDETRSegLargeConfig,
RFDETRSegMediumConfig,
RFDETRSegNanoConfig,
RFDETRSegPreviewConfig,
RFDETRSegSmallConfig,
RFDETRSegXLargeConfig,
RFDETRSmallConfig,
TrainConfig,
)
from rfdetr.models.weights import load_pretrain_weights
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _make_checkpoint(num_classes=91, num_queries=300, group_detr=13):
"""Build a minimal checkpoint dict with the given class count.
Args:
num_classes: Total classes including background (bias shape).
num_queries: Number of object queries per group.
group_detr: Number of groups.
"""
total_queries = num_queries * group_detr
state = {
"class_embed.weight": torch.randn(num_classes, 256),
"class_embed.bias": torch.randn(num_classes),
"refpoint_embed.weight": torch.randn(total_queries, 4),
"query_feat.weight": torch.randn(total_queries, 256),
"other_layer.weight": torch.randn(10, 10),
}
ckpt_args = SimpleNamespace(
segmentation_head=False,
patch_size=14,
class_names=[],
)
return {"model": state, "args": ckpt_args}
def _make_train_config():
"""Return a minimal TrainConfig for use in load_pretrain_weights.
Returns:
Minimal TrainConfig with placeholder dataset and output dirs.
"""
return TrainConfig(
dataset_dir="/nonexistent/dataset",
output_dir="/nonexistent/output",
epochs=10,
lr=1e-4,
lr_encoder=1.5e-4,
batch_size=2,
weight_decay=1e-4,
lr_drop=8,
warmup_epochs=1.0,
drop_path=0.0,
multi_scale=False,
expanded_scales=False,
do_random_resize_via_padding=False,
grad_accum_steps=1,
tensorboard=False,
)
def _suppress_pretrain_io(monkeypatch) -> None:
"""Suppress download/validate/file-existence side effects on the canonical load path."""
monkeypatch.setattr("rfdetr.models.weights.download_pretrain_weights", lambda *a, **kw: None)
monkeypatch.setattr("rfdetr.models.weights.validate_pretrain_weights", lambda *a, **kw: None)
monkeypatch.setattr("rfdetr.models.weights.validate_checkpoint_compatibility", lambda *a, **kw: None)
monkeypatch.setattr("rfdetr.models.weights.os.path.isfile", lambda _: True)
# ---------------------------------------------------------------------------
# Regression tests: load_pretrain_weights (models/weights.py)
# ---------------------------------------------------------------------------
class TestLoadPretrainWeightsSecondReinit:
"""Regression tests for ``load_pretrain_weights`` in ``rfdetr.models.weights``.
Validates that the second reinitialize_detection_head call only fires when the checkpoint has MORE classes than
configured (backbone pretrain scenario), not when it has fewer (fine-tuned checkpoint scenario).
"""
@pytest.fixture(autouse=True)
def _patch_download(self, monkeypatch):
"""Suppress all download and file-existence side effects."""
_suppress_pretrain_io(monkeypatch)
def test_finetune_checkpoint_preserves_weights(self, monkeypatch):
"""Fine-tuned checkpoint (fewer classes) must NOT trigger second reinit.
Scenario: 2-class fine-tuned checkpoint (bias shape [3]) loaded with
default num_classes=90. The first reinit correctly resizes the head to 3 so load_state_dict works. The second
reinit must NOT resize to 91 — that would destroy the loaded fine-tuned weights.
"""
from rfdetr.models.weights import load_pretrain_weights
mc = RFDETRBaseConfig(pretrain_weights="/fake/weights.pth", device="cpu")
checkpoint = _make_checkpoint(num_classes=3)
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
calls = fake_model.reinitialize_detection_head.call_args_list
assert calls[0] == call(3), f"First reinit should resize to checkpoint size 3, got {calls[0]}"
assert len(calls) == 1, (
f"Expected exactly 1 reinit call (to checkpoint size), but got {len(calls)}: "
f"{calls}. The second reinit to 91 destroys loaded weights."
)
assert mc.num_classes == 2, (
f"mc.num_classes must be auto-aligned to 2 (checkpoint_logits - 1), got {mc.num_classes}"
)
def test_no_mismatch_no_reinit(self, monkeypatch):
"""Checkpoint class count matches config — no reinit at all.
Scenario: COCO checkpoint (91 classes) with num_classes=90. 91 == 90 + 1, so no reinit should fire.
"""
from rfdetr.models.weights import load_pretrain_weights
mc = RFDETRBaseConfig(pretrain_weights="/fake/weights.pth", device="cpu", num_classes=90)
checkpoint = _make_checkpoint(num_classes=91)
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
fake_model.reinitialize_detection_head.assert_not_called()
def test_backbone_pretrain_still_reinits(self, monkeypatch):
"""Backbone pretrain (more classes in checkpoint) must still reinit.
Scenario: COCO 91-class checkpoint loaded for 2-class fine-tuning
(num_classes=2). Both reinits are correct here: first to 91 for load_state_dict, second to 3 for the configured
class count.
"""
from rfdetr.models.weights import load_pretrain_weights
mc = RFDETRBaseConfig(pretrain_weights="/fake/weights.pth", device="cpu", num_classes=2)
checkpoint = _make_checkpoint(num_classes=91)
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
calls = fake_model.reinitialize_detection_head.call_args_list
assert calls == [call(91), call(3)], f"Expected reinit to [91, 3] (expand then trim), got {calls}"
def test_user_override_larger_than_checkpoint_reexpands_head(self, monkeypatch):
"""Explicit larger num_classes must be restored after checkpoint load.
Scenario: 91-class checkpoint loaded with explicit num_classes=93.
Loader must temporarily match checkpoint size for load_state_dict, then expand to 94 logits and keep
args.num_classes unchanged.
"""
from rfdetr.models.weights import load_pretrain_weights
mc = RFDETRBaseConfig(pretrain_weights="/fake/weights.pth", device="cpu", num_classes=93)
checkpoint = _make_checkpoint(num_classes=91)
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
calls = fake_model.reinitialize_detection_head.call_args_list
assert calls == [call(91), call(94)], f"Expected reinit to [91, 94] (load then expand), got {calls}"
assert mc.num_classes == 93, "Explicitly configured num_classes must not be overwritten."
# All non-deprecated model configs (RFDETRLargeDeprecatedConfig and
# RFDETRBaseConfig are excluded; the former is deprecated, the latter
# serves as the base class for the concrete variants below).
@pytest.mark.parametrize(
"config_cls",
[
pytest.param(RFDETRNanoConfig, id="nano"),
pytest.param(RFDETRSmallConfig, id="small"),
pytest.param(RFDETRMediumConfig, id="medium"),
pytest.param(RFDETRLargeConfig, id="large"),
pytest.param(RFDETRSegNanoConfig, id="seg_nano"),
pytest.param(RFDETRSegSmallConfig, id="seg_small"),
pytest.param(RFDETRSegMediumConfig, id="seg_medium"),
pytest.param(RFDETRSegLargeConfig, id="seg_large"),
pytest.param(RFDETRSegXLargeConfig, id="seg_xlarge"),
pytest.param(RFDETRSeg2XLargeConfig, id="seg_2xlarge"),
],
)
def test_eight_class_finetune_checkpoint_auto_aligns_num_classes_and_reinits_once(self, monkeypatch, config_cls):
"""Auto-align ``mc.num_classes`` and avoid a second reinit for 8-class checkpoints.
Scenario (from user bug report): user trains on 8 categories (IDs 0–7).
The checkpoint stores ``class_embed.bias`` with shape [9] (8 user classes + 1 background). Loading without
specifying ``num_classes`` must NOT trigger a second reinit to 91 after temporarily matching the checkpoint size
for ``load_state_dict``.
This test asserts the loader auto-aligns ``mc.num_classes`` to 8 (9 - 1) and fires exactly one reinit call — to
9 (the checkpoint size).
"""
# 8 dataset categories → training builds a model with 8+1=9 logits.
checkpoint = _make_checkpoint(num_classes=9)
mc = config_cls(pretrain_weights="/fake/weights.pth", device="cpu")
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
calls = fake_model.reinitialize_detection_head.call_args_list
assert len(calls) == 1, (
f"Expected exactly 1 reinit call (to checkpoint size 9), but got {len(calls)}: "
f"{calls}. A second reinit to 91 would produce OOB class IDs like 73."
)
assert calls[0] == call(9), f"Reinit must resize to checkpoint's 9 logits, got {calls[0]}"
assert mc.num_classes == 8, (
f"mc.num_classes must be auto-aligned to 8 (checkpoint_logits - 1), got {mc.num_classes}"
)
# ---------------------------------------------------------------------------
# Regression #960: PE interpolation for custom resolution
# ---------------------------------------------------------------------------
PE_KEY = "backbone.0.encoder.encoder.embeddings.position_embeddings"
class TestLoadPretrainWeightsPEInterpolation:
"""Regression tests for #960 — PE must be interpolated when resolution changes.
``load_pretrain_weights`` must bicubic-interpolate the checkpoint's DINOv2 positional embeddings to match the
model's ``positional_encoding_size`` before calling ``load_state_dict``. Without this, any custom ``resolution``
that changes the PE grid size causes a ``RuntimeError: size mismatch``.
"""
@pytest.fixture(autouse=True)
def _patch_download(self, monkeypatch):
"""Suppress all download and file-existence side effects."""
_suppress_pretrain_io(monkeypatch)
@pytest.mark.parametrize(
"src_pe_size, tgt_resolution, patch_size, expected_tgt_pe_size",
[
pytest.param(24, 640, 16, 40, id="nano_24x24_upscale_to_40x40"),
pytest.param(40, 384, 16, 24, id="nano_40x40_downscale_to_24x24"),
pytest.param(32, 640, 16, 40, id="small_32x32_upscale_to_40x40"),
],
)
def test_pe_in_checkpoint_is_interpolated_to_model_resolution(
self, monkeypatch, src_pe_size, tgt_resolution, patch_size, expected_tgt_pe_size
):
"""Checkpoint PE is bicubic-interpolated to match model_config.positional_encoding_size.
Regression for #960: ``load_pretrain_weights`` must not raise ``RuntimeError`` when model resolution differs
from checkpoint resolution. The PE tensor in the checkpoint must be resized in-place before ``load_state_dict``
is called.
"""
mc = RFDETRNanoConfig(
pretrain_weights="/fake/weights.pth",
device="cpu",
resolution=tgt_resolution,
patch_size=patch_size,
)
assert mc.positional_encoding_size == tgt_resolution // patch_size
dim = 384
src_n = src_pe_size * src_pe_size + 1 # patches + class token
checkpoint = _make_checkpoint(num_classes=91)
checkpoint["model"][PE_KEY] = torch.randn(1, src_n, dim).half() # float16 to verify dtype round-trip
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
pe = checkpoint["model"][PE_KEY]
expected_n = expected_tgt_pe_size * expected_tgt_pe_size + 1
assert pe.shape == torch.Size([1, expected_n, dim]), (
f"Expected PE shape [1, {expected_n}, {dim}], got {tuple(pe.shape)}. "
f"PE was not interpolated from {src_pe_size}x{src_pe_size} "
f"to {expected_tgt_pe_size}x{expected_tgt_pe_size}."
)
assert pe.dtype == torch.float16, f"Dtype must be preserved after interpolation, got {pe.dtype}"
def test_matching_pe_shape_is_not_modified(self, monkeypatch):
"""When checkpoint PE matches model expectations, the tensor is not changed.
Ensures PE interpolation is a no-op for same-resolution checkpoints so that normal weight loading is unaffected.
"""
mc = RFDETRNanoConfig(pretrain_weights="/fake/weights.pth", device="cpu")
# Default: positional_encoding_size=24 → PE = [1, 24*24+1, 384] = [1, 577, 384]
dim = 384
original_pe = torch.randn(1, 577, dim)
checkpoint = _make_checkpoint(num_classes=91)
checkpoint["model"][PE_KEY] = original_pe.clone()
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
pe = checkpoint["model"][PE_KEY]
assert pe.shape == torch.Size([1, 577, dim]), "Matching PE shape must not be modified."
assert torch.equal(pe, original_pe), "Matching PE tensor values must not be modified."
def test_base_config_non_formula_pe_is_interpolated_from_smaller_checkpoint(self, monkeypatch):
"""RFDETRBaseConfig PE=37 (not formula-derived) is interpolated when checkpoint differs.
RFDETRBaseConfig.positional_encoding_size=37 is not updated by ``_sync_pe_with_resolution`` because 37 ≠
560//16=35 (not formula-derived). Loading a checkpoint with a smaller PE grid (e.g., 24×24) must still trigger
interpolation to the model's fixed PE=37×37 target.
"""
mc = RFDETRBaseConfig(pretrain_weights="/fake/weights.pth", device="cpu")
assert mc.positional_encoding_size == 37, "RFDETRBaseConfig PE must remain 37 (not formula-derived)"
dim = 384
src_pe_size = 24
src_n = src_pe_size * src_pe_size + 1
checkpoint = _make_checkpoint(num_classes=91)
checkpoint["model"][PE_KEY] = torch.randn(1, src_n, dim)
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
pe = checkpoint["model"][PE_KEY]
expected_n = 37 * 37 + 1
assert pe.shape == torch.Size([1, expected_n, dim]), (
f"Expected PE shape [1, {expected_n}, {dim}] (37×37 grid), got {tuple(pe.shape)}. "
"BaseConfig's non-formula-derived PE must be the interpolation target."
)
def test_non_square_source_pe_logs_warning_and_is_not_modified(self, monkeypatch):
"""Non-square source PE grids are skipped with a warning and left unchanged.
When ``n_source`` is not a perfect square the interpolation is skipped to avoid producing malformed embeddings.
The tensor must remain untouched and a warning must be emitted via the weights module logger.
"""
mc = RFDETRNanoConfig(pretrain_weights="/fake/weights.pth", device="cpu")
# positional_encoding_size=24 → n_target=576 (perfect square, so the
# target-side guard does not trigger; only the source-side guard fires)
dim = 384
# 17 is not a perfect square: isqrt(17)=4, 4*4=16 ≠ 17
non_square_n_source = 17
original_pe = torch.randn(1, non_square_n_source + 1, dim)
checkpoint = _make_checkpoint(num_classes=91)
checkpoint["model"][PE_KEY] = original_pe.clone()
warning_calls: list[tuple] = []
monkeypatch.setattr("rfdetr.models.weights.logger.warning", lambda *a, **kw: warning_calls.append(a))
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
load_pretrain_weights(fake_model, mc)
pe = checkpoint["model"][PE_KEY]
assert torch.equal(pe, original_pe), "Non-square source PE must not be modified."
assert any("not a perfect square" in str(args) for args in warning_calls), (
f"Expected a 'not a perfect square' warning; got calls: {warning_calls}"
)
class TestL1FacadePEInterpolationEndToEnd:
"""Regression for instantiating an RF-DETR L1 facade variant with a custom ``resolution`` and a checkpoint trained
at the variant's default resolution must not raise ``RuntimeError`` from a PE shape mismatch.
In v1.6.5 the L1 facade (``RFDETRLarge``, ``RFDETRNano``, ...) used a private ``_load_pretrain_weights_into`` helper
in ``detr.py`` that bypassed the PE bicubic-interpolation added to ``models.weights.load_pretrain_weights`` Code
that wired the L1 facade through the unified loader landed later (``inference._build_model_context`` calling
``load_pretrain_weights`` from ``models.weights``). This test pins that wiring so a future refactor cannot
reintroduce a divergent loader path that silently skips PE interpolation.
Current coverage: ``RFDETRNano`` (detection) and ``RFDETRSegNano`` (segmentation), upward-interpolation only. When
a third L1 facade variant is added, collapse both methods to a single ``@pytest.mark.parametrize`` over
``(variant_class, default_pe_grid, patch_size, new_resolution)``. Downward-interpolation (high-res checkpoint →
lower-res model) is not currently exercised; add a reverse-direction parametrize row when refactoring.
"""
def test_rfdetr_nano_loads_default_pe_checkpoint_at_custom_resolution(self, tmp_path):
"""Saving an RFDETRNano state_dict at default resolution and loading at a higher resolution must succeed via PE
interpolation in the L1 facade.
Mirrors the user-reported scenario in https://github.com/roboflow/rf-detr/issues/990 (PE size mismatch ``[1,
1937, 384]`` vs ``[1, 6401, 384]`` raised from ``LWDETR.load_state_dict``), reduced to RFDETRNano for test
speed.
"""
from rfdetr import RFDETRNano
# 1. Build a default-resolution RFDETRNano (no pretrain, on CPU) so that
# it produces a state_dict with the variant's native PE grid.
default_model = RFDETRNano(pretrain_weights=None, num_classes=2, device="cpu")
default_pe_grid = default_model.model_config.positional_encoding_size
assert default_pe_grid == 24, "RFDETRNano default PE grid must be 24×24"
patch_size = default_model.model_config.patch_size
default_state = default_model.model.model.state_dict()
default_pe = default_state[PE_KEY]
pe_dim = default_pe.shape[-1]
assert default_pe.shape == torch.Size([1, default_pe_grid * default_pe_grid + 1, pe_dim])
# 2. Persist as a checkpoint that mimics what `model.train()` saves —
# a top-level "model" key plus a SimpleNamespace "args" block.
ckpt_path = tmp_path / "user_finetuned.pth"
torch.save(
{
"model": dict(default_state),
"args": SimpleNamespace(class_names=["a", "b"], patch_size=patch_size),
},
ckpt_path,
)
# 3. Re-instantiate at a NEW resolution. Without PE interpolation in
# the L1 facade path this raises ``RuntimeError: size mismatch for
# backbone.0.encoder.encoder.embeddings.position_embeddings`` from
# LWDETR.load_state_dict — exactly the user-reported failure.
new_resolution = 640
loaded = RFDETRNano(
pretrain_weights=str(ckpt_path),
resolution=new_resolution,
num_classes=2,
device="cpu",
)
# 4. The model_config validator must update PE proportionally to the
# new resolution, and the loaded backbone PE parameter must have the
# interpolated target shape (40 × 40 + 1 = 1601 tokens).
expected_pe_grid = new_resolution // patch_size
assert expected_pe_grid == 40
assert loaded.model_config.positional_encoding_size == expected_pe_grid
loaded_pe = loaded.model.model.state_dict()[PE_KEY]
assert loaded_pe.shape == torch.Size([1, expected_pe_grid * expected_pe_grid + 1, pe_dim]), (
f"Backbone PE was not interpolated to the requested resolution; "
f"got shape {tuple(loaded_pe.shape)}, expected [1, {expected_pe_grid**2 + 1}, {pe_dim}]."
)
def test_rfdetr_seg_nano_loads_default_pe_checkpoint_at_custom_resolution(self, tmp_path):
"""Saving an RFDETRSegNano state_dict at default resolution and loading at a higher resolution must succeed via
PE interpolation in the L1 facade.
Regression for https://github.com/roboflow/rf-detr/issues/1023 — the segmentation model variant
(``RFDETRSegNano``) raised ``RuntimeError: size mismatch for
backbone.0.encoder.encoder.embeddings.position_embeddings`` when instantiated with a non-default ``resolution``
because the L1 facade's checkpoint-loading path did not interpolate positional embeddings for segmentation
models.
"""
from rfdetr import RFDETRSegNano
# 1. Build a default-resolution RFDETRSegNano (no pretrain, on CPU).
# Uses 90 classes to mimic an official COCO-pretrained checkpoint so
# the load path at step 3 exercises both head-reinit (90 → 2 classes)
# and PE interpolation simultaneously.
default_model = RFDETRSegNano(pretrain_weights=None, num_classes=90, device="cpu")
default_pe_grid = default_model.model_config.positional_encoding_size
assert default_pe_grid == 26, "RFDETRSegNano default PE grid must be 26×26 (312 // 12)"
patch_size = default_model.model_config.patch_size
assert patch_size == 12, "RFDETRSegNano patch_size must be 12"
default_state = default_model.model.model.state_dict()
default_pe = default_state[PE_KEY]
pe_dim = default_pe.shape[-1]
assert default_pe.shape == torch.Size([1, default_pe_grid * default_pe_grid + 1, pe_dim])
# 2. Persist as a checkpoint that mimics the official pretrain weights
# format. Saved as .pth (not .pt) so the tmp_path fixture path does
# not trigger ModelWeights registry / MD5 lookup. Top-level keys
# match the real checkpoint: "model" (state_dict) and "args" with
# segmentation_head=True and patch_size=12.
ckpt_path = tmp_path / "rf-detr-seg-nano.pth"
torch.save(
{
"model": dict(default_state),
"args": SimpleNamespace(
class_names=[],
patch_size=patch_size,
segmentation_head=True,
),
},
ckpt_path,
)
# 3. Re-instantiate at a custom resolution with fewer classes. Without
# PE interpolation this raises
# ``RuntimeError: size mismatch for
# backbone.0.encoder.encoder.embeddings.position_embeddings``
# from LWDETR.load_state_dict — exactly the user-reported failure.
# resolution=1008 (user-reported in #1023, 84×84=7057 tokens) is deferred to follow-up parametrization.
new_resolution = 624 # 2× the default 312; divisible by patch_size=12
loaded = RFDETRSegNano(
pretrain_weights=str(ckpt_path),
resolution=new_resolution,
num_classes=2,
device="cpu",
)
# 4. The model_config validator must update PE proportionally to the
# new resolution, and the loaded backbone PE parameter must have the
# interpolated target shape (52 × 52 + 1 = 2705 tokens).
expected_pe_grid = new_resolution // patch_size
assert expected_pe_grid == 52
assert loaded.model_config.positional_encoding_size == expected_pe_grid
loaded_pe = loaded.model.model.state_dict()[PE_KEY]
assert loaded_pe.shape == torch.Size([1, expected_pe_grid * expected_pe_grid + 1, pe_dim]), (
f"Backbone PE was not interpolated to the requested resolution; "
f"got shape {tuple(loaded_pe.shape)}, expected [1, {expected_pe_grid**2 + 1}, {pe_dim}]."
)
# ---------------------------------------------------------------------------
# Deprecation: train_config argument
# ---------------------------------------------------------------------------
class TestLoadPretrainWeightsDeprecation:
"""Passing train_config must emit a DeprecationWarning."""
def test_emits_deprecation_warning_when_train_config_passed(self, monkeypatch):
"""Any non-None train_config triggers a DeprecationWarning."""
from rfdetr.models.weights import load_pretrain_weights
mc = RFDETRBaseConfig(pretrain_weights=None, device="cpu")
tc = _make_train_config()
with pytest.warns(FutureWarning, match="train_config.*deprecated"):
load_pretrain_weights(MagicMock(), mc, tc)
# ---------------------------------------------------------------------------
# Regression #1038: PE interpolation for custom resolution — training path
# ---------------------------------------------------------------------------
class TestModuleLoadPretrainWeightsPEInterpolationCustomResolution:
"""Regression for #1038 — PE interpolation must fire through ``RFDETRModelModule.__init__``.
The L2 training entry path (``RFDETRSegLarge(resolution=1008).train(...)``) constructs an
:class:`~rfdetr.training.module_model.RFDETRModelModule` whose ``__init__`` delegates to
:func:`~rfdetr.models.weights.load_pretrain_weights`. That helper must bicubic-interpolate the checkpoint's DINOv2
positional embeddings to match ``model_config.positional_encoding_size`` before calling ``load_state_dict``.
Without this, any ``model.train()`` call with a custom ``resolution`` that changes the PE grid raises::
RuntimeError: Error(s) in loading state_dict for LWDETR:
size mismatch for backbone.0.encoder.encoder.embeddings.position_embeddings
These tests exercise the construction path end-to-end (mocking only the heavy ``build_model_from_config`` /
``build_criterion_from_config`` calls and disk I/O), so the regression cannot reappear if the in-init delegation to
``load_pretrain_weights`` is removed.
"""
@pytest.fixture(autouse=True)
def _patch_download(self, monkeypatch):
"""Suppress download/validate side effects on the canonical load path."""
_suppress_pretrain_io(monkeypatch)
def _construct_module(self, mc, checkpoint, monkeypatch, tmp_path):
"""Construct an RFDETRModelModule with all heavy work mocked.
Returns the constructed module and the fake nn_model whose ``load_state_dict`` receives the (now-interpolated)
state dict.
"""
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = MagicMock()
# Pretend the head was already aligned so the canonical loader does not
# try to introspect the MagicMock's internals.
fake_model.num_classes = mc.num_classes
monkeypatch.setattr(
"rfdetr.training.module_model.build_model_from_config",
lambda *a, **kw: fake_model,
)
monkeypatch.setattr(
"rfdetr.training.module_model.build_criterion_from_config",
lambda *a, **kw: (MagicMock(), MagicMock()),
)
tc = TrainConfig(
dataset_dir=str(tmp_path / "dataset"),
output_dir=str(tmp_path / "output"),
epochs=1,
lr=1e-4,
lr_encoder=1.5e-4,
batch_size=2,
weight_decay=1e-4,
lr_drop=1,
warmup_epochs=0.0,
drop_path=0.0,
multi_scale=False,
expanded_scales=False,
do_random_resize_via_padding=False,
grad_accum_steps=1,
tensorboard=False,
)
from rfdetr.training.module_model import RFDETRModelModule
module = RFDETRModelModule(mc, tc)
return module, fake_model
@pytest.mark.parametrize(
"config_cls, src_pe_size, tgt_pe_size",
[
# All 7 segmentation variants listed in #1038, plus detection up/downscale.
pytest.param(RFDETRSegNanoConfig, 26, 84, id="seg_nano_upscale_26_to_84"),
pytest.param(RFDETRSegSmallConfig, 32, 84, id="seg_small_upscale_32_to_84"),
pytest.param(RFDETRSegMediumConfig, 36, 84, id="seg_medium_upscale_36_to_84"),
pytest.param(RFDETRSegLargeConfig, 42, 84, id="seg_large_upscale_42_to_84"),
pytest.param(RFDETRSegPreviewConfig, 24, 84, id="seg_preview_upscale_24_to_84"),
pytest.param(RFDETRSegXLargeConfig, 52, 84, id="seg_xlarge_upscale_52_to_84"),
pytest.param(RFDETRSeg2XLargeConfig, 64, 84, id="seg_2xlarge_upscale_64_to_84"),
pytest.param(RFDETRNanoConfig, 24, 40, id="nano_upscale_24_to_40"),
pytest.param(RFDETRNanoConfig, 40, 24, id="nano_downscale_40_to_24"),
],
)
def test_pe_interpolated_in_training_path(self, monkeypatch, config_cls, src_pe_size, tgt_pe_size, tmp_path):
"""Module construction interpolates PE to match ``positional_encoding_size``.
Regression for #1038 — ``RFDETRModelModule.__init__`` must trigger PE interpolation through the canonical loader
so ``load_state_dict`` does not raise ``RuntimeError: size mismatch`` at custom training resolutions.
"""
mc = config_cls(
pretrain_weights="/fake/weights.pth",
device="cpu",
positional_encoding_size=tgt_pe_size,
)
dim = 384
src_n = src_pe_size * src_pe_size + 1
checkpoint = _make_checkpoint(num_classes=mc.num_classes + 1)
checkpoint["model"][PE_KEY] = torch.randn(1, src_n, dim)
_, fake_model = self._construct_module(mc, checkpoint, monkeypatch, tmp_path)
pe = checkpoint["model"][PE_KEY]
expected_n = tgt_pe_size * tgt_pe_size + 1
assert pe.shape == torch.Size([1, expected_n, dim]), (
f"Expected PE shape [1, {expected_n}, {dim}] after interpolation from "
f"{src_pe_size}x{src_pe_size} to {tgt_pe_size}x{tgt_pe_size}, got {tuple(pe.shape)}. "
"PE interpolation must fire during RFDETRModelModule.__init__ via canonical load_pretrain_weights."
)
# load_state_dict was called on the model with the interpolated state dict.
fake_model.load_state_dict.assert_called_once()
def test_matching_pe_not_modified_in_training_path(self, monkeypatch, tmp_path):
"""Same-resolution checkpoint PE is untouched in the training path."""
pe_size = 24
mc = RFDETRNanoConfig(
pretrain_weights="/fake/weights.pth",
device="cpu",
positional_encoding_size=pe_size,
)
dim = 384
original_pe = torch.randn(1, pe_size * pe_size + 1, dim)
checkpoint = _make_checkpoint(num_classes=mc.num_classes + 1)
checkpoint["model"][PE_KEY] = original_pe.clone()
self._construct_module(mc, checkpoint, monkeypatch, tmp_path)
pe = checkpoint["model"][PE_KEY]
assert pe.shape == torch.Size([1, pe_size * pe_size + 1, dim]), "Matching PE must not be modified."
assert torch.equal(pe, original_pe), "Matching PE values must not be modified."
# ---------------------------------------------------------------------------
# Regression: keypoint schema auto-align from checkpoint _kp_active_mask
# ---------------------------------------------------------------------------
def _make_kp_checkpoint(kp_schema: list[int], num_queries: int = 300, group_detr: int = 13) -> dict:
"""Build a minimal keypoint checkpoint encoding *kp_schema* via ``_kp_active_mask``.
Args:
kp_schema: Keypoints-per-class list, e.g. ``[0, 17]`` for bg-first or ``[17]`` for active-first.
num_queries: Number of queries per group.
group_detr: Number of decoder groups.
Returns:
Checkpoint dict with ``model``, ``args``, and schema-derived ``_kp_active_mask``.
"""
num_classes = len(kp_schema)
total_queries = num_queries * group_detr
max_kp = max(kp_schema) if kp_schema else 0
mask = torch.zeros(num_classes, max_kp, dtype=torch.bool)
for idx, n_kp in enumerate(kp_schema):
mask[idx, :n_kp] = True
state = {
"class_embed.weight": torch.randn(num_classes + 1, 256),
"class_embed.bias": torch.randn(num_classes + 1),
"refpoint_embed.weight": torch.randn(total_queries, 4),
"query_feat.weight": torch.randn(total_queries, 256),
"_kp_active_mask": mask,
}
ckpt_args = SimpleNamespace(segmentation_head=False, patch_size=14, class_names=[])
return {"model": state, "args": ckpt_args}
def _make_kp_fake_model(initial_schema: list[int]) -> MagicMock:
"""Build a fake nn_model mock that mimics the GroupPose keypoint model interface.
Args:
initial_schema: Keypoints-per-class schema the model was built with.
Returns:
Configured MagicMock with keypoint schema methods and ``_kp_active_mask`` state.
"""
max_kp = max(initial_schema) if initial_schema else 0
current_schema = list(initial_schema)
mask = torch.zeros(len(initial_schema), max_kp, dtype=torch.bool)
for idx, n in enumerate(initial_schema):
mask[idx, :n] = True
fake_model = MagicMock()
fake_model.state_dict.return_value = {"_kp_active_mask": mask.clone()}
def _reinit_kp(schema):
current_schema[:] = schema
new_max = max(schema) if schema else 0
new_mask = torch.zeros(len(schema), new_max, dtype=torch.bool)
for i, n in enumerate(schema):
new_mask[i, :n] = True
fake_model.state_dict.return_value = {"_kp_active_mask": new_mask}
fake_model.get_num_keypoints_per_class.return_value = list(schema)
fake_model.reinitialize_keypoint_head.side_effect = _reinit_kp
fake_model.get_num_keypoints_per_class.return_value = list(initial_schema)
fake_model.get_num_keypoints_per_class_from_checkpoint = lambda sd: (
[int(n) for n in sd["_kp_active_mask"].sum(dim=1).tolist()] if "_kp_active_mask" in sd else None
)
return fake_model
class TestLoadPretrainWeightsKeypointSchemaAutoAlign:
"""Regression for AP=0 when loading bg-first checkpoint into active-first default config.
Before the fix, ``load_pretrain_weights`` would restore ``mc.num_keypoints_per_class`` to the config default
``[17]`` after loading a ``[0, 17]`` pretrained checkpoint. The detection-head trimming kept rows 0 (background)
and 1 (person) from the checkpoint but the active-first schema treats row 0 as person, producing AP≈0.
The fix auto-aligns ``mc.num_keypoints_per_class`` from the checkpoint ``_kp_active_mask`` when the user did not
explicitly set the field, mirroring the existing ``num_classes`` auto-align pattern.
"""
@pytest.fixture(autouse=True)
def _patch_io(self, monkeypatch):
"""Suppress all download and file-existence side effects."""
_suppress_pretrain_io(monkeypatch)
def test_bg_first_checkpoint_auto_aligns_active_first_config(self, monkeypatch):
"""Loading bg-first [0,17] checkpoint into active-first [17] config auto-aligns schema."""
mc = RFDETRKeypointPreviewConfig(pretrain_weights="/fake/kp.pth", device="cpu")
assert mc.num_keypoints_per_class == [17], "Precondition: config default is active-first [17]."
checkpoint = _make_kp_checkpoint([0, 17])
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = _make_kp_fake_model([17])
load_pretrain_weights(fake_model, mc)
assert mc.num_keypoints_per_class == [0, 17], (
f"expected auto-align to [0, 17], got {mc.num_keypoints_per_class}"
)
assert mc.num_classes == 2, (
f"mc.num_classes must be auto-aligned to 2 (checkpoint has 3 logit slots), got {mc.num_classes}"
)
def test_matching_schema_no_change(self, monkeypatch):
"""Config and checkpoint with same schema leaves mc.num_keypoints_per_class unchanged."""
mc = RFDETRKeypointPreviewConfig(pretrain_weights="/fake/kp.pth", device="cpu")
mc.num_keypoints_per_class = [17]
checkpoint = _make_kp_checkpoint([17])
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = _make_kp_fake_model([17])
load_pretrain_weights(fake_model, mc)
assert mc.num_keypoints_per_class == [17]
def test_user_explicit_schema_not_overridden(self, monkeypatch):
"""Explicit num_keypoints_per_class from user survives even when checkpoint differs."""
mc = RFDETRKeypointPreviewConfig(pretrain_weights="/fake/kp.pth", device="cpu")
# Simulate user explicitly providing num_keypoints_per_class
mc.num_keypoints_per_class = [0, 33]
mc.model_fields_set.add("num_keypoints_per_class")
checkpoint = _make_kp_checkpoint([0, 17])
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = _make_kp_fake_model([0, 33])
load_pretrain_weights(fake_model, mc)
assert mc.num_keypoints_per_class == [0, 33], (
"Explicit user schema must not be overridden by checkpoint auto-align."
)
def test_1d_kp_active_mask_skips_auto_align_with_warning(self, monkeypatch):
"""Malformed 1-D _kp_active_mask skips auto-align and emits a logger.warning."""
mc = RFDETRKeypointPreviewConfig(pretrain_weights="/fake/kp.pth", device="cpu")
assert mc.num_keypoints_per_class == [17], "Precondition: config default is active-first [17]."
checkpoint = _make_kp_checkpoint([0, 17])
# Replace the 2-D mask with a malformed 1-D tensor.
checkpoint["model"]["_kp_active_mask"] = torch.ones(17, dtype=torch.bool)
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = _make_kp_fake_model([17])
# The fake model's get_num_keypoints_per_class_from_checkpoint calls sum(dim=1),
# which raises IndexError on a 1-D tensor; return None to skip that code path.
fake_model.get_num_keypoints_per_class_from_checkpoint = lambda sd: None
warned: list[str] = []
monkeypatch.setattr("rfdetr.models.weights.logger.warning", lambda msg, *args: warned.append(msg % args))
load_pretrain_weights(fake_model, mc)
assert mc.num_keypoints_per_class == [17], (
"Auto-align must not fire for a 1-D _kp_active_mask; config default should be unchanged."
)
assert any("unexpected shape" in msg for msg in warned), (
f"Expected a warning mentioning 'unexpected shape'; got: {warned}"
)
def test_all_zero_kp_active_mask_skips_auto_align_with_warning(self, monkeypatch):
"""All-zero 2-D _kp_active_mask (degenerate) skips auto-align and emits a logger.warning."""
mc = RFDETRKeypointPreviewConfig(pretrain_weights="/fake/kp.pth", device="cpu")
assert mc.num_keypoints_per_class == [17], "Precondition: config default is active-first [17]."
checkpoint = _make_kp_checkpoint([0, 17])
# Replace mask with a valid 2-D shape but all zeros (no active slots).
checkpoint["model"]["_kp_active_mask"] = torch.zeros(2, 17, dtype=torch.bool)
monkeypatch.setattr("rfdetr.models.weights.torch.load", lambda *a, **kw: checkpoint)
fake_model = _make_kp_fake_model([17])
warned: list[str] = []
monkeypatch.setattr("rfdetr.models.weights.logger.warning", lambda msg, *args: warned.append(msg % args))
load_pretrain_weights(fake_model, mc)
assert mc.num_keypoints_per_class == [17], (
"Auto-align must not fire for an all-zero _kp_active_mask; config default should be unchanged."
)
assert any("no active slots" in msg for msg in warned), (
f"Expected a warning mentioning 'no active slots'; got: {warned}"
)