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

264 lines
9.9 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 DepthwiseConvBlock and _DepthwiseConvWithoutCuDNN (segmentation head)."""
from contextlib import contextmanager
import pytest
import torch
from rfdetr.models.heads.segmentation import DepthwiseConvBlock
@pytest.fixture(autouse=True)
def _reset_random_seeds() -> None:
"""Reset random seeds before each test for reproducibility."""
torch.manual_seed(42)
@pytest.mark.parametrize(
"device",
[
pytest.param("cpu", id="cpu"),
pytest.param(
"cuda",
id="gpu",
marks=[
pytest.mark.gpu,
pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is not available",
),
],
),
],
)
def test_depthwise_conv_block_forward(device: str) -> None:
"""DepthwiseConvBlock forward pass produces correct output shape without error."""
block = DepthwiseConvBlock(dim=8).to(device)
x = torch.randn(1, 8, 4, 4, device=device)
y = block(x)
assert y.shape == x.shape
def test_depthwise_conv_forward_disables_cudnn(monkeypatch) -> None:
"""Depthwise conv should execute with cuDNN disabled during forward."""
block = DepthwiseConvBlock(dim=8)
enabled_calls: list[bool] = []
original_flags = torch.backends.cudnn.flags
@contextmanager
def _tracking_flags(*, enabled: bool):
enabled_calls.append(enabled)
with original_flags(enabled=enabled):
yield
monkeypatch.setattr(torch.backends.cudnn, "flags", _tracking_flags)
x = torch.randn(1, 8, 4, 4)
y = block(x)
assert y.shape == x.shape
assert enabled_calls, "torch.backends.cudnn.flags was never called"
assert all(not e for e in enabled_calls)
def test_depthwise_conv_backward_disables_cudnn(monkeypatch) -> None:
"""Backward pass must also run with cuDNN disabled (issue #731).
The previous fix (PR #728) only wrapped the forward pass in a context manager. The backward kernels ran with cuDNN
re-enabled, causing RuntimeError on T4/P100 GPUs.
"""
block = DepthwiseConvBlock(dim=8)
enabled_calls: list[bool] = []
original_flags = torch.backends.cudnn.flags
@contextmanager
def _tracking_flags(*, enabled: bool):
enabled_calls.append(enabled)
with original_flags(enabled=enabled):
yield
monkeypatch.setattr(torch.backends.cudnn, "flags", _tracking_flags)
x = torch.randn(1, 8, 4, 4, requires_grad=True)
y = block(x)
y.sum().backward()
assert x.grad is not None
assert x.grad.shape == x.shape
# cuDNN must be disabled for both forward and backward
assert len(enabled_calls) >= 2
assert all(not e for e in enabled_calls)
@pytest.mark.parametrize(
"device",
[
pytest.param("cpu", id="cpu"),
pytest.param(
"cuda",
id="gpu",
marks=[
pytest.mark.gpu,
pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is not available",
),
],
),
],
)
def test_depthwise_conv_backward_produces_correct_gradients(device: str) -> None:
"""Backward pass through DepthwiseConvBlock produces valid gradients."""
block = DepthwiseConvBlock(dim=8).to(device)
x = torch.randn(1, 8, 4, 4, device=device, requires_grad=True)
y = block(x)
y.sum().backward()
assert x.grad is not None
assert x.grad.shape == x.shape
assert torch.isfinite(x.grad).all()
def test_depthwise_conv_gradients_match_reference() -> None:
"""Custom autograd Function gradients match nn.Conv2d gradients.
Verifies that _DepthwiseConvWithoutCuDNN produces the same gradients as a standard nn.Conv2d forward+backward (run
with cuDNN disabled globally).
"""
torch.manual_seed(42)
dim = 8
block = DepthwiseConvBlock(dim=dim)
# Reference: run standard nn.Conv2d with cuDNN globally disabled
x_ref = torch.randn(1, dim, 4, 4, requires_grad=True)
with torch.backends.cudnn.flags(enabled=False):
y_ref = block.dwconv(x_ref)
y_ref.sum().backward()
x_ref_grad = x_ref.grad.clone()
weight_ref_grad = block.dwconv.weight.grad.clone()
bias_ref_grad = block.dwconv.bias.grad.clone()
# Our implementation via _depthwise_conv. zero_grad() so that the second
# backward does not accumulate into weight.grad from the first run.
block.zero_grad()
x_test = x_ref.detach().clone().requires_grad_(True)
y_test = block._depthwise_conv(x_test)
y_test.sum().backward()
assert torch.allclose(y_ref, y_test, atol=1e-6)
assert torch.allclose(x_ref_grad, x_test.grad, atol=1e-6)
assert torch.allclose(weight_ref_grad, block.dwconv.weight.grad, atol=1e-6)
assert torch.allclose(bias_ref_grad, block.dwconv.bias.grad, atol=1e-6)
def test_depthwise_conv_backward_fp16_grad_output() -> None:
"""Backward must not crash when grad_output is fp16 (AMP 16-mixed on T4/P100).
On T4/P100, trainer resolves amp=True to '16-mixed'. In that mode the backward receives fp16 grad_output while the
saved weight stays fp32. Without explicit dtype casting, conv2d_input raises:
RuntimeError: expected scalar type Half but found Float
"""
dim = 8
block = DepthwiseConvBlock(dim=dim)
x = torch.randn(1, dim, 4, 4, requires_grad=True)
# Simulate 16-mixed backward: forward in fp32, grad_output arrives as fp16
y = block._depthwise_conv(x)
grad_output = torch.ones_like(y, dtype=torch.float16)
y.backward(grad_output)
assert x.grad is not None
assert x.grad.dtype == torch.float32
assert torch.isfinite(x.grad).all()
def test_depthwise_conv_backward_bf16_activation_keeps_grads_fp32() -> None:
"""grad_input and weight.grad must be fp32 when saved activation x is bf16 (issue #959).
Under bf16-mixed AMP, the activation x entering _DepthwiseConvWithoutCuDNN is bf16 while weight stays fp32. The old
code cast grad_input back to x.dtype (bf16), propagating bf16 gradients to fp32 backbone parameters so that
param.grad.dtype became bf16. Fused AdamW then crashed with 'params, grads, exp_avgs, and exp_avg_sqs must have
same dtype, device, and layout' (see issue #959). The fix keeps grad_input in weight.dtype (fp32).
This test drives the backward directly with a bf16 saved activation to reproduce the dtype that is present at
training time without requiring a GPU.
"""
import types
from rfdetr.models.heads.segmentation import _DepthwiseConvWithoutCuDNN
dim = 8
weight = torch.randn(dim, 1, 3, 3, requires_grad=True) # fp32 parameter (never cast by AMP)
x_bf16 = torch.randn(1, dim, 4, 4, dtype=torch.bfloat16) # bf16 activation (cast by AMP forward)
grad_output = torch.ones(1, dim, 4, 4, dtype=torch.bfloat16) # bf16 grad (from bf16 backward)
# Build a minimal context mirroring what ctx would contain after the AMP forward pass.
ctx = types.SimpleNamespace()
ctx.saved_tensors = (x_bf16, weight)
ctx.has_bias = False
ctx.stride = (1, 1)
ctx.padding = (1, 1)
ctx.dilation = (1, 1)
ctx.groups = dim
ctx.needs_input_grad = [True, True, False, False, False, False, False]
grad_input, grad_weight, *_ = _DepthwiseConvWithoutCuDNN.backward(ctx, grad_output)
assert grad_input is not None, "grad_input should not be None when needs_input_grad[0] is True"
assert grad_input.dtype == torch.float32, (
f"grad_input is {grad_input.dtype} — bf16 grad_input propagates to fp32 backbone params "
"and crashes fused AdamW (issue #959)"
)
assert grad_weight is not None, "grad_weight should not be None when needs_input_grad[1] is True"
assert grad_weight.dtype == torch.float32, (
f"grad_weight is {grad_weight.dtype} — weight grad must stay fp32 to match param dtype"
)
def test_depthwise_conv_no_cudnn_bias_none() -> None:
"""_DepthwiseConvWithoutCuDNN forward and backward work correctly with bias=None.
Exercises the ctx.has_bias=False branch in forward and the grad_bias=None return in backward — never reached via
DepthwiseConvBlock (always has bias).
"""
from rfdetr.models.heads.segmentation import _DepthwiseConvWithoutCuDNN
dim = 8
weight = torch.randn(dim, 1, 3, 3, requires_grad=True)
x = torch.randn(1, dim, 4, 4, requires_grad=True)
y = _DepthwiseConvWithoutCuDNN.apply(x, weight, None, (1, 1), (1, 1), (1, 1), dim)
y_ref = torch.nn.functional.conv2d(x.detach(), weight.detach(), None, stride=1, padding=1, dilation=1, groups=dim)
assert torch.allclose(y.detach(), y_ref, atol=1e-6)
y.sum().backward()
assert x.grad is not None
assert x.grad.shape == x.shape
assert torch.isfinite(x.grad).all()
assert weight.grad is not None
assert weight.grad.shape == weight.shape
assert torch.isfinite(weight.grad).all()
@pytest.mark.parametrize(
"layer_scale_init_value", [pytest.param(0, id="no_gamma"), pytest.param(1e-6, id="with_gamma")]
)
def test_depthwise_conv_block_layer_scale(layer_scale_init_value: float) -> None:
"""DepthwiseConvBlock with and without layer scaling produces valid output and gradients.
Exercises the gamma=None (layer_scale_init_value=0) and gamma!=None (layer_scale_init_value>0) branches in
DepthwiseConvBlock.forward().
"""
block = DepthwiseConvBlock(dim=8, layer_scale_init_value=layer_scale_init_value)
x = torch.randn(1, 8, 4, 4, requires_grad=True)
y = block(x)
assert y.shape == x.shape
y.sum().backward()
assert x.grad is not None
assert torch.isfinite(x.grad).all()
if layer_scale_init_value > 0:
assert block.gamma is not None
assert block.gamma.grad is not None