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
502 lines
20 KiB
Python
502 lines
20 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 transformer utilities, MS deformable attention core, and MSDeformAttn module."""
|
|
|
|
import io
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
from rfdetr.models.ops.functions import ms_deform_attn_core_pytorch
|
|
from rfdetr.models.ops.modules.ms_deform_attn import MSDeformAttn
|
|
from rfdetr.models.transformer import gen_encoder_output_proposals, gen_sineembed_for_position
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_random_seeds() -> None:
|
|
"""Ensure reproducible random state for every test."""
|
|
torch.manual_seed(42)
|
|
torch.cuda.manual_seed_all(42)
|
|
|
|
|
|
_MSDeformInputs = tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, list[tuple[int, int]]]
|
|
|
|
|
|
def _build_ms_deform_inputs(
|
|
bsz: int = 1,
|
|
n_heads: int = 2,
|
|
head_dim: int = 4,
|
|
len_q: int = 3,
|
|
npts: int = 1,
|
|
levels: list[tuple[int, int]] | None = None,
|
|
) -> _MSDeformInputs:
|
|
"""Build minimal valid inputs for ms_deform_attn_core_pytorch.
|
|
|
|
Args:
|
|
bsz: Batch size.
|
|
n_heads: Number of attention heads.
|
|
head_dim: Dimension per head.
|
|
len_q: Number of query elements.
|
|
npts: Number of sampling points per level.
|
|
levels: List of (H, W) int pairs; defaults to [(4, 4), (2, 2)].
|
|
|
|
Returns:
|
|
Tuple of (value, spatial_shapes_tensor, sampling_locations,
|
|
attention_weights, spatial_shapes_hw).
|
|
"""
|
|
if levels is None:
|
|
levels = [(4, 4), (2, 2)]
|
|
nlvl = len(levels)
|
|
|
|
total_hw = sum(ht * wd for ht, wd in levels)
|
|
spatial_shapes_tensor = torch.tensor(levels, dtype=torch.long)
|
|
value = torch.randn(bsz, n_heads, head_dim, total_hw)
|
|
# sampling_locations: (bsz, len_q, n_heads, nlvl, npts, 2) in [0, 1]
|
|
sampling_locations = torch.rand(bsz, len_q, n_heads, nlvl, npts, 2)
|
|
# attention_weights: (bsz, len_q, n_heads, nlvl * npts)
|
|
attention_weights = torch.softmax(torch.randn(bsz, len_q, n_heads, nlvl * npts), dim=-1)
|
|
|
|
return value, spatial_shapes_tensor, sampling_locations, attention_weights, levels
|
|
|
|
|
|
def test_gen_encoder_output_proposals_passes_ij_indexing_to_meshgrid(monkeypatch) -> None:
|
|
"""`gen_encoder_output_proposals` should call `torch.meshgrid` with explicit ij indexing."""
|
|
original_meshgrid = torch.meshgrid
|
|
call_count = 0
|
|
|
|
def _meshgrid_with_indexing_assertion(*args, **kwargs):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if kwargs.get("indexing") != "ij":
|
|
raise AssertionError("torch.meshgrid must be called with indexing='ij'")
|
|
return original_meshgrid(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(torch, "meshgrid", _meshgrid_with_indexing_assertion)
|
|
|
|
memory = torch.randn(1, 4, 8)
|
|
spatial_shapes = torch.tensor([[2, 2]], dtype=torch.long)
|
|
|
|
output_memory, output_proposals = gen_encoder_output_proposals(
|
|
memory,
|
|
spatial_shapes=spatial_shapes,
|
|
)
|
|
|
|
assert call_count == 1
|
|
|
|
|
|
def test_gen_sineembed_for_position_keeps_box_dimensions_in_sin_cos_order() -> None:
|
|
"""4D box positional embeddings must use the pretrained sin/cos order for all dimensions."""
|
|
pos_tensor = torch.tensor([[[0.125, 0.25, 0.5, 0.75]]], dtype=torch.float32)
|
|
dim = 4
|
|
scale = 2 * torch.pi
|
|
dim_t = torch.arange(dim, dtype=pos_tensor.dtype)
|
|
dim_t = 10000 ** (2 * (dim_t // 2) / dim)
|
|
|
|
expected_parts = []
|
|
for coord_idx in (1, 0, 2, 3):
|
|
coord = pos_tensor[:, :, coord_idx] * scale
|
|
encoded = coord[:, :, None] / dim_t
|
|
expected_parts.append(torch.stack((encoded[:, :, 0::2].sin(), encoded[:, :, 1::2].cos()), dim=3).flatten(2))
|
|
expected = torch.cat(expected_parts, dim=2)
|
|
|
|
actual = gen_sineembed_for_position(pos_tensor, dim=dim)
|
|
|
|
torch.testing.assert_close(actual, expected, rtol=1e-4, atol=1e-6)
|
|
|
|
|
|
def test_gen_encoder_output_proposals_rejects_non_square_ij_indexing(monkeypatch) -> None:
|
|
"""Wrong meshgrid indexing (xy vs ij) produces different proposals for non-square spatial shapes."""
|
|
original_meshgrid = torch.meshgrid
|
|
|
|
def _meshgrid_wrong_indexing(*args, **kwargs):
|
|
kwargs["indexing"] = "xy"
|
|
return original_meshgrid(*args, **kwargs)
|
|
|
|
# Use non-square spatial shapes so that ij vs xy indexing produces observably different outputs.
|
|
memory = torch.randn(1, 8, 8)
|
|
spatial_shapes = torch.tensor([[2, 4]], dtype=torch.long)
|
|
|
|
correct_memory, correct_proposals = gen_encoder_output_proposals(memory, spatial_shapes=spatial_shapes)
|
|
|
|
monkeypatch.setattr(torch, "meshgrid", _meshgrid_wrong_indexing)
|
|
|
|
wrong_memory, wrong_proposals = gen_encoder_output_proposals(memory, spatial_shapes=spatial_shapes)
|
|
|
|
assert not torch.allclose(correct_proposals, wrong_proposals), (
|
|
"xy indexing must produce different proposals than ij indexing for non-square spatial shapes"
|
|
)
|
|
|
|
|
|
def test_gen_encoder_output_proposals_accepts_int_tuple_spatial_shapes() -> None:
|
|
"""`gen_encoder_output_proposals` must accept `spatial_shapes` as a tensor of int pairs."""
|
|
batch = 2
|
|
ht, wd = 4, 4
|
|
memory = torch.randn(batch, ht * wd, 8)
|
|
spatial_shapes = torch.tensor([[ht, wd]], dtype=torch.long)
|
|
|
|
output_memory, output_proposals = gen_encoder_output_proposals(memory, spatial_shapes=spatial_shapes)
|
|
|
|
assert output_memory.shape == memory.shape
|
|
assert output_proposals.shape == (batch, ht * wd, 4)
|
|
|
|
|
|
def test_gen_encoder_output_proposals_accepts_python_int_pair_spatial_shapes() -> None:
|
|
"""`gen_encoder_output_proposals` must accept `spatial_shapes` as `list[tuple[int, int]]` with no padding mask.
|
|
|
|
Regression: `Transformer.forward` passes Python int pairs derived from `src.shape`, so the
|
|
export-driven call path uses `list[tuple[int, int]]` rather than a tensor.
|
|
"""
|
|
batch, ht, wd, dim = 2, 4, 4, 8
|
|
memory = torch.randn(batch, ht * wd, dim)
|
|
spatial_shapes = [(ht, wd)] # Python int pairs, as produced by Transformer.forward()
|
|
|
|
output_memory, output_proposals = gen_encoder_output_proposals(
|
|
memory,
|
|
memory_padding_mask=None,
|
|
spatial_shapes=spatial_shapes,
|
|
)
|
|
|
|
assert output_memory.shape == memory.shape
|
|
assert output_proposals.shape == (batch, ht * wd, 4)
|
|
|
|
|
|
class TestMSDeformAttnCorePytorch:
|
|
"""Tests for ms_deform_attn_core_pytorch with Python int pair spatial shapes.
|
|
|
|
Regression suite for torch.export.export compatibility: iterating over a spatial_shapes tensor yields FakeTensor
|
|
scalars during FakeTensor tracing, which cannot be used as Python int split/view sizes. The function now accepts an
|
|
optional ``value_spatial_shapes_hw`` list of Python int pairs that bypasses tensor iteration.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def make_inputs(self) -> _MSDeformInputs:
|
|
"""Default two-level inputs: levels=[(4, 4), (2, 2)]."""
|
|
return _build_ms_deform_inputs()
|
|
|
|
@pytest.fixture
|
|
def single_level_inputs(self) -> _MSDeformInputs:
|
|
"""Single-level inputs: levels=[(8, 8)]."""
|
|
return _build_ms_deform_inputs(levels=[(8, 8)])
|
|
|
|
def test_with_tensor_spatial_shapes(self, make_inputs: _MSDeformInputs) -> None:
|
|
"""Baseline: passing only the tensor spatial_shapes still works."""
|
|
value, spatial_shapes_tensor, sampling_locations, attention_weights, _ = make_inputs
|
|
|
|
output = ms_deform_attn_core_pytorch(value, spatial_shapes_tensor, sampling_locations, attention_weights)
|
|
|
|
bsz, n_heads, head_dim, _ = value.shape
|
|
len_q = sampling_locations.shape[1]
|
|
assert output.shape == (bsz, len_q, n_heads * head_dim)
|
|
|
|
def test_with_python_int_pair_spatial_shapes(self, make_inputs: _MSDeformInputs) -> None:
|
|
"""Regression: value_spatial_shapes_hw list of Python int pairs must be accepted.
|
|
|
|
This is the torch.export.export-compatible code path: tensor scalar values (from iterating over a FakeTensor)
|
|
cannot be used as split/view sizes, so the caller passes explicit Python int pairs via value_spatial_shapes_hw
|
|
instead.
|
|
"""
|
|
value, spatial_shapes_tensor, sampling_locations, attention_weights, levels = make_inputs
|
|
|
|
output = ms_deform_attn_core_pytorch(
|
|
value,
|
|
spatial_shapes_tensor,
|
|
sampling_locations,
|
|
attention_weights,
|
|
value_spatial_shapes_hw=levels,
|
|
)
|
|
|
|
bsz, n_heads, head_dim, _ = value.shape
|
|
len_q = sampling_locations.shape[1]
|
|
assert output.shape == (bsz, len_q, n_heads * head_dim)
|
|
|
|
def test_tensor_and_hw_paths_produce_identical_outputs(self, make_inputs: _MSDeformInputs) -> None:
|
|
"""Python int pair path and tensor iteration path must produce the same result."""
|
|
value, spatial_shapes_tensor, sampling_locations, attention_weights, levels = make_inputs
|
|
|
|
out_tensor_path = ms_deform_attn_core_pytorch(
|
|
value, spatial_shapes_tensor, sampling_locations, attention_weights
|
|
)
|
|
out_hw_path = ms_deform_attn_core_pytorch(
|
|
value,
|
|
spatial_shapes_tensor,
|
|
sampling_locations,
|
|
attention_weights,
|
|
value_spatial_shapes_hw=levels,
|
|
)
|
|
|
|
torch.testing.assert_close(out_tensor_path, out_hw_path)
|
|
|
|
def test_single_level(self, single_level_inputs: _MSDeformInputs) -> None:
|
|
"""Single-level case with Python int pair path must not crash."""
|
|
value, spatial_shapes_tensor, sampling_locations, attention_weights, levels = single_level_inputs
|
|
|
|
output = ms_deform_attn_core_pytorch(
|
|
value,
|
|
spatial_shapes_tensor,
|
|
sampling_locations,
|
|
attention_weights,
|
|
value_spatial_shapes_hw=levels,
|
|
)
|
|
|
|
assert output.shape[0] == 1
|
|
|
|
|
|
class TestMSDeformAttnModule:
|
|
"""Tests for MSDeformAttn.forward covering the export-compatibility changes.
|
|
|
|
Validates the module-level parameter threading and export-mode assert guard introduced in the torch.export.export
|
|
compatibility fix.
|
|
"""
|
|
|
|
_d_model = 32
|
|
_n_heads = 4
|
|
_n_levels = 2
|
|
_n_points = 1
|
|
_hw_pairs: list[tuple[int, int]] = [(4, 4), (2, 2)]
|
|
|
|
def _make_module_inputs(
|
|
self,
|
|
) -> tuple[
|
|
torch.Tensor,
|
|
torch.Tensor,
|
|
torch.Tensor,
|
|
torch.Tensor,
|
|
torch.Tensor,
|
|
list[tuple[int, int]],
|
|
]:
|
|
"""Build minimal valid inputs for MSDeformAttn.forward.
|
|
|
|
Returns:
|
|
Tuple of (query, reference_points, input_flatten,
|
|
input_spatial_shapes, input_level_start_index, hw_pairs).
|
|
"""
|
|
hw_pairs = self._hw_pairs
|
|
total_len = sum(ht * wd for ht, wd in hw_pairs)
|
|
bsz, len_q = 1, 3
|
|
|
|
query = torch.randn(bsz, len_q, self._d_model)
|
|
reference_points = torch.rand(bsz, len_q, self._n_levels, 2)
|
|
input_flatten = torch.randn(bsz, total_len, self._d_model)
|
|
input_spatial_shapes = torch.tensor(hw_pairs, dtype=torch.long)
|
|
# Cumulative start index per level: [0, H0*W0]
|
|
starts = [sum(ht * wd for ht, wd in hw_pairs[:idx]) for idx in range(self._n_levels)]
|
|
input_level_start_index = torch.tensor(starts, dtype=torch.long)
|
|
|
|
return query, reference_points, input_flatten, input_spatial_shapes, input_level_start_index, hw_pairs
|
|
|
|
def test_forward_without_hw_param_backward_compat(self) -> None:
|
|
"""MSDeformAttn.forward without hw param produces correct output shape."""
|
|
module = MSDeformAttn(
|
|
d_model=self._d_model, n_levels=self._n_levels, n_heads=self._n_heads, n_points=self._n_points
|
|
)
|
|
query, ref_pts, input_flatten, spatial_shapes, level_start_index, _ = self._make_module_inputs()
|
|
|
|
output = module(query, ref_pts, input_flatten, spatial_shapes, level_start_index)
|
|
|
|
bsz, len_q, _ = query.shape
|
|
assert output.shape == (bsz, len_q, self._d_model)
|
|
|
|
def test_forward_with_hw_param_produces_correct_shape(self) -> None:
|
|
"""MSDeformAttn.forward with input_spatial_shapes_hw produces correct output shape."""
|
|
module = MSDeformAttn(
|
|
d_model=self._d_model, n_levels=self._n_levels, n_heads=self._n_heads, n_points=self._n_points
|
|
)
|
|
query, ref_pts, input_flatten, spatial_shapes, level_start_index, hw_pairs = self._make_module_inputs()
|
|
|
|
output = module(
|
|
query, ref_pts, input_flatten, spatial_shapes, level_start_index, input_spatial_shapes_hw=hw_pairs
|
|
)
|
|
|
|
bsz, len_q, _ = query.shape
|
|
assert output.shape == (bsz, len_q, self._d_model)
|
|
|
|
def test_export_mode_forward_with_hw_param(self) -> None:
|
|
"""MSDeformAttn.forward in export mode with hw param must not raise."""
|
|
module = MSDeformAttn(
|
|
d_model=self._d_model, n_levels=self._n_levels, n_heads=self._n_heads, n_points=self._n_points
|
|
)
|
|
module.export()
|
|
query, ref_pts, input_flatten, spatial_shapes, level_start_index, hw_pairs = self._make_module_inputs()
|
|
|
|
output = module(
|
|
query, ref_pts, input_flatten, spatial_shapes, level_start_index, input_spatial_shapes_hw=hw_pairs
|
|
)
|
|
|
|
bsz, len_q, _ = query.shape
|
|
assert output.shape == (bsz, len_q, self._d_model)
|
|
|
|
def test_export_flag_set_after_export_call(self) -> None:
|
|
"""Calling .export() must set _export=True, enabling the torch._assert guard path."""
|
|
module = MSDeformAttn(
|
|
d_model=self._d_model, n_levels=self._n_levels, n_heads=self._n_heads, n_points=self._n_points
|
|
)
|
|
assert not module._export
|
|
|
|
module.export()
|
|
|
|
assert module._export
|
|
|
|
|
|
class TestGenEncoderOutputProposalsDynamicBatch:
|
|
"""Regression tests for dynamic batch support in gen_encoder_output_proposals.
|
|
|
|
Ensures that the ONNX-symbolic refactoring (PR #950 / issue #949) does not bake a fixed batch dimension into
|
|
proposals and that output shapes are correct for varying batch sizes.
|
|
"""
|
|
|
|
@pytest.mark.parametrize("batch_size", [1, 2, 4, 8])
|
|
def test_output_shape_invariant_across_batch_sizes(self, batch_size: int) -> None:
|
|
"""Output shapes must scale correctly with batch size, with no baked constants.
|
|
|
|
Args:
|
|
batch_size: Number of images in the batch.
|
|
"""
|
|
ht, wd, dim = 4, 4, 8
|
|
memory = torch.randn(batch_size, ht * wd, dim)
|
|
spatial_shapes = [(ht, wd)]
|
|
|
|
output_memory, output_proposals = gen_encoder_output_proposals(
|
|
memory, memory_padding_mask=None, spatial_shapes=spatial_shapes
|
|
)
|
|
|
|
assert output_memory.shape == (batch_size, ht * wd, dim)
|
|
assert output_proposals.shape == (batch_size, ht * wd, 4)
|
|
|
|
def test_proposals_semantically_equivalent_across_batch_sizes(self) -> None:
|
|
"""Proposals for batch=1 and batch=4 must be identical per image.
|
|
|
|
Regression: if batch_size were baked as a constant, repeating the same image
|
|
N times would produce different proposals for each copy.
|
|
"""
|
|
ht, wd, dim = 4, 4, 8
|
|
memory_single = torch.randn(1, ht * wd, dim)
|
|
memory_multi = memory_single.expand(4, -1, -1).contiguous()
|
|
spatial_shapes = [(ht, wd)]
|
|
|
|
_, proposals_single = gen_encoder_output_proposals(
|
|
memory_single, memory_padding_mask=None, spatial_shapes=spatial_shapes
|
|
)
|
|
_, proposals_multi = gen_encoder_output_proposals(
|
|
memory_multi, memory_padding_mask=None, spatial_shapes=spatial_shapes
|
|
)
|
|
|
|
torch.testing.assert_close(proposals_single.expand(4, -1, -1), proposals_multi)
|
|
|
|
@pytest.mark.parametrize("batch_size", [1, 4])
|
|
def test_output_shape_invariant_with_padding_mask(self, batch_size: int) -> None:
|
|
"""Output shapes must be correct when memory_padding_mask is provided with varying batch sizes.
|
|
|
|
Regression for PR #950 / issue #949: the masked branch used .reshape(-1, h, w, 1) to infer the batch dimension
|
|
dynamically; this test verifies the branch handles varying batch sizes without error.
|
|
|
|
Args:
|
|
batch_size: Number of images in the batch.
|
|
"""
|
|
ht, wd, dim = 4, 4, 8
|
|
total_hw = ht * wd
|
|
memory = torch.randn(batch_size, total_hw, dim)
|
|
# Mask shape: (batch, sum_hw) — True means padding (invalid position)
|
|
memory_padding_mask = torch.zeros(batch_size, total_hw, dtype=torch.bool)
|
|
spatial_shapes = [(ht, wd)]
|
|
|
|
output_memory, output_proposals = gen_encoder_output_proposals(
|
|
memory, memory_padding_mask=memory_padding_mask, spatial_shapes=spatial_shapes
|
|
)
|
|
|
|
assert output_memory.shape == (batch_size, total_hw, dim)
|
|
assert output_proposals.shape == (batch_size, total_hw, 4)
|
|
|
|
@pytest.mark.parametrize("batch_size", [1, 4, 8])
|
|
def test_onnx_export_with_dynamic_batch_axis(self, batch_size: int) -> None:
|
|
"""ONNX export with dynamic batch axis must run inference for batch sizes other than the trace batch.
|
|
|
|
Regression for issue #949: exporting with a fixed trace batch baked `Reshape([8,...])` as a constant ONNX node,
|
|
causing TRT engines to fail at inference for any batch != 8. Skipped when onnx or onnxruntime is not installed.
|
|
"""
|
|
pytest.importorskip("onnx")
|
|
onnxruntime = pytest.importorskip("onnxruntime")
|
|
|
|
ht, wd, dim = 4, 4, 8
|
|
spatial_shapes_list = [(ht, wd)]
|
|
|
|
class _ProposalModule(torch.nn.Module):
|
|
"""Thin wrapper to export gen_encoder_output_proposals via torch.onnx."""
|
|
|
|
def forward(self, memory: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Forward pass delegating to gen_encoder_output_proposals."""
|
|
return gen_encoder_output_proposals(
|
|
memory, memory_padding_mask=None, spatial_shapes=spatial_shapes_list
|
|
)
|
|
|
|
module = _ProposalModule()
|
|
trace_memory = torch.randn(2, ht * wd, dim)
|
|
|
|
buf = io.BytesIO()
|
|
torch.onnx.export(
|
|
module,
|
|
(trace_memory,),
|
|
buf,
|
|
input_names=["memory"],
|
|
output_names=["output_memory", "output_proposals"],
|
|
dynamic_axes={"memory": {0: "batch"}},
|
|
opset_version=17,
|
|
)
|
|
buf.seek(0)
|
|
onnx_bytes = buf.read()
|
|
|
|
session = onnxruntime.InferenceSession(onnx_bytes, providers=["CPUExecutionProvider"])
|
|
memory_np = np.random.randn(batch_size, ht * wd, dim).astype(np.float32)
|
|
out_memory, out_proposals = session.run(None, {"memory": memory_np})
|
|
assert out_memory.shape == (batch_size, ht * wd, dim), f"wrong memory shape for batch={batch_size}"
|
|
assert out_proposals.shape == (batch_size, ht * wd, 4), f"wrong proposals shape for batch={batch_size}"
|
|
|
|
|
|
def test_ms_deform_attn_core_pytorch_export_compatible() -> None:
|
|
"""torch.export.export must succeed on a module using ms_deform_attn_core_pytorch with hw param.
|
|
|
|
Regression test for the FakeTensor tracing failure: iterating over spatial_shapes and using the scalar elements as
|
|
split/view sizes fails during torch.export.export because FakeTensor data is not allocated. Passing
|
|
value_spatial_shapes_hw (concrete Python ints from a module attribute) bypasses the tensor iteration entirely.
|
|
"""
|
|
levels: list[tuple[int, int]] = [(4, 4), (2, 2)]
|
|
bsz, n_heads, head_dim = 1, 2, 4
|
|
total_hw = sum(ht * wd for ht, wd in levels)
|
|
len_q, nlvl, npts = 3, len(levels), 1
|
|
|
|
class _MinimalDeformAttn(torch.nn.Module):
|
|
"""Minimal wrapper to test torch.export.export on the hw-param code path."""
|
|
|
|
def __init__(self, hw: list[tuple[int, int]]) -> None:
|
|
super().__init__()
|
|
self.hw = hw
|
|
|
|
def forward(
|
|
self,
|
|
value: torch.Tensor,
|
|
spatial_shapes: torch.Tensor,
|
|
sampling_locations: torch.Tensor,
|
|
attention_weights: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
"""Forward using concrete Python int pairs for export compatibility."""
|
|
return ms_deform_attn_core_pytorch(
|
|
value,
|
|
spatial_shapes,
|
|
sampling_locations,
|
|
attention_weights,
|
|
value_spatial_shapes_hw=self.hw,
|
|
)
|
|
|
|
value = torch.randn(bsz, n_heads, head_dim, total_hw)
|
|
spatial_shapes = torch.tensor(levels, dtype=torch.long)
|
|
sampling_locations = torch.rand(bsz, len_q, n_heads, nlvl, npts, 2)
|
|
attention_weights = torch.softmax(torch.randn(bsz, len_q, n_heads, nlvl * npts), dim=-1)
|
|
|
|
module = _MinimalDeformAttn(hw=levels)
|
|
|
|
exported = torch.export.export(module, args=(value, spatial_shapes, sampling_locations, attention_weights))
|
|
assert exported is not None
|