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
264 lines
9.9 KiB
Python
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
|