Files
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

135 lines
5.0 KiB
Python

# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Unit tests for SetCriterion edge paths: _output_device and num_boxes_for_targets."""
import pytest
import torch
from rfdetr.models.criterion import SetCriterion
class _MatcherStub:
"""Minimal matcher that returns identity indices for every target in the batch."""
def __call__(self, outputs, targets, group_detr=1):
return [(torch.arange(len(t["labels"])), torch.arange(len(t["labels"]))) for t in targets]
def _bare_criterion() -> SetCriterion:
"""Return a SetCriterion with no losses so forward() is a no-op."""
criterion = SetCriterion.__new__(SetCriterion)
criterion.training = True
criterion.group_detr = 1
criterion.sum_group_losses = False
criterion.losses = []
criterion.weight_dict = {}
criterion.matcher = _MatcherStub()
criterion.num_keypoints_per_class = []
return criterion
class TestOutputDevice:
"""Tests for SetCriterion._output_device — probes top-level tensor values only."""
def test_returns_device_of_first_tensor(self):
"""Device inferred from the first tensor value in outputs."""
outputs = {"pred_logits": torch.zeros(1, 1, 1)}
device = SetCriterion._output_device(outputs)
assert device == torch.device("cpu")
def test_raises_when_no_tensor_present(self):
"""ValueError raised when no top-level value is a tensor."""
outputs = {"meta": "string_value", "count": 42}
with pytest.raises(ValueError, match="at least one tensor"):
SetCriterion._output_device(outputs)
def test_skips_non_tensor_values(self):
"""Non-tensor entries at the top level are skipped; first tensor wins."""
outputs = {"meta": "ignored", "pred_logits": torch.zeros(1, 1, 1)}
device = SetCriterion._output_device(outputs)
assert device == torch.device("cpu")
class TestNumBoxesForTargets:
"""Tests for SetCriterion.num_boxes_for_targets — clamp and empty-target edge cases."""
def test_returns_tensor_gte_one(self):
"""Result must be clamped to >= 1.0 to prevent division by zero."""
criterion = _bare_criterion()
outputs = {"pred_logits": torch.zeros(1, 1, 1)}
targets = [{"labels": torch.tensor([0, 1])}]
result = criterion.num_boxes_for_targets(outputs, targets)
assert result.item() >= 1.0
def test_clamps_zero_box_count_to_one(self):
"""Empty targets (no labels) must clamp to 1.0 to avoid zero denominator."""
criterion = _bare_criterion()
outputs = {"pred_logits": torch.zeros(1, 1, 1)}
targets = [{"labels": torch.zeros(0, dtype=torch.int64)}]
result = criterion.num_boxes_for_targets(outputs, targets)
assert result.item() == pytest.approx(1.0)
def test_clamps_empty_target_list(self):
"""Empty target list (batch_size=0 edge case) must also clamp to 1.0."""
criterion = _bare_criterion()
outputs = {"pred_logits": torch.zeros(1, 1, 1)}
targets = []
result = criterion.num_boxes_for_targets(outputs, targets)
assert result.item() == pytest.approx(1.0)
def test_counts_labels_correctly(self):
"""Box count equals total number of labels across all targets in the batch."""
criterion = _bare_criterion()
outputs = {"pred_logits": torch.zeros(1, 1, 1)}
targets = [
{"labels": torch.tensor([0, 1])},
{"labels": torch.tensor([0])},
]
result = criterion.num_boxes_for_targets(outputs, targets)
# 2 + 1 = 3 boxes; single-process so no all-reduce
assert result.item() == pytest.approx(3.0)
class TestLossMasksEmptyMatch:
"""Tests for the dict-path zero-GT branch of SetCriterion.loss_masks."""
def test_dict_path_zero_gt_stays_connected_to_graph(self):
"""Zero-match dict path returns a loss that back-propagates to every segmentation-head output."""
criterion = _bare_criterion()
spatial_features = torch.randn(1, 4, 8, 8, requires_grad=True)
query_features = torch.randn(1, 5, 4, requires_grad=True)
bias = torch.randn(1, requires_grad=True)
outputs = {
"pred_masks": {
"spatial_features": spatial_features,
"query_features": query_features,
"bias": bias,
}
}
empty = torch.empty(0, dtype=torch.long)
indices = [(empty, empty)]
losses = criterion.loss_masks(outputs, targets=[{}], indices=indices, num_boxes=1)
assert losses["loss_mask_ce"].requires_grad
(losses["loss_mask_ce"] + losses["loss_mask_dice"]).backward()
assert spatial_features.grad is not None
assert query_features.grad is not None
assert bias.grad is not None