Files
apache--tvm/tests/python/relax/test_op_vision.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

1693 lines
56 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import numpy as np
import pytest
import tvm
import tvm.testing
from tvm.testing import env
pytest.importorskip("scipy") # tvm.topi.testing imports scipy
import tvm.topi.testing
from tvm import relax, tirx
from tvm.ir import Op
from tvm.relax.transform import LegalizeOps
from tvm.script import relax as R
def _check_inference(bb: relax.BlockBuilder, call: relax.Call, expected_ty: relax.Type):
ret = bb.normalize(call)
tvm.ir.assert_structural_equal(ret.ty, expected_ty)
def _assert_relax_op_legalized(mod: tvm.IRModule, op_name: str) -> None:
seen_call_tir = False
seen_original_op = False
def _visit(expr):
nonlocal seen_call_tir, seen_original_op
if isinstance(expr, relax.Call) and isinstance(expr.op, tvm.ir.Op):
if expr.op.name == "relax.call_tir":
seen_call_tir = True
if expr.op.name == op_name:
seen_original_op = True
relax.analysis.post_order_visit(mod["main"].body, _visit)
assert seen_call_tir
assert not seen_original_op
def test_roi_align_op_correctness():
x = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois = relax.Var("rois", R.Tensor((4, 5), "float32"))
assert relax.op.vision.roi_align(x, rois, (7, 7), 1.0).op == Op.get("relax.vision.roi_align")
def test_roi_align_infer_ty():
bb = relax.BlockBuilder()
x0 = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
x1 = relax.Var("x", R.Tensor((2, 32, 32, 3), "float32"))
rois = relax.Var("rois", R.Tensor((5, 5), "float32"))
_check_inference(
bb,
relax.op.vision.roi_align(x0, rois, (7, 7), 0.25),
relax.TensorType((5, 3, 7, 7), "float32"),
)
_check_inference(
bb,
relax.op.vision.roi_align(x1, rois, (5, 7), 1.0, layout="NHWC"),
relax.TensorType((5, 5, 7, 3), "float32"),
)
def test_roi_align_infer_ty_aligned():
bb = relax.BlockBuilder()
x = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois = relax.Var("rois", R.Tensor((5, 5), "float32"))
_check_inference(
bb,
relax.op.vision.roi_align(x, rois, (7, 7), 1.0, aligned=True),
relax.TensorType((5, 3, 7, 7), "float32"),
)
def test_roi_align_infer_ty_shape_var():
bb = relax.BlockBuilder()
n = tirx.Var("n", "int64")
c = tirx.Var("c", "int64")
h = tirx.Var("h", "int64")
w = tirx.Var("w", "int64")
num_roi = tirx.Var("num_roi", "int64")
x = relax.Var("x", R.Tensor((n, c, h, w), "float32"))
rois = relax.Var("rois", R.Tensor((num_roi, 5), "float32"))
_check_inference(
bb,
relax.op.vision.roi_align(x, rois, (7, 7), 0.5),
relax.TensorType((num_roi, c, 7, 7), "float32"),
)
def test_roi_align_wrong_input_ndim():
bb = relax.BlockBuilder()
x0 = relax.Var("x", R.Tensor((2, 3, 32), "float32"))
x1 = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois0 = relax.Var("rois", R.Tensor((4,), "float32"))
rois1 = relax.Var("rois", R.Tensor((4, 5), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.roi_align(x0, rois1, (7, 7), 1.0))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.roi_align(x1, rois0, (7, 7), 1.0))
def test_roi_align_wrong_rois_last_dim():
bb = relax.BlockBuilder()
x = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois = relax.Var("rois", R.Tensor((4, 4), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.roi_align(x, rois, (7, 7), 1.0))
def test_roi_align_wrong_layout():
bb = relax.BlockBuilder()
x = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois = relax.Var("rois", R.Tensor((4, 5), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.roi_align(x, rois, (7, 7), 1.0, layout="HWCN"))
def test_roi_align_legalize():
@tvm.script.ir_module
class ROIAlign:
@R.function
def main(
x: R.Tensor((1, 2, 8, 8), "float32"),
rois: R.Tensor((2, 5), "float32"),
) -> R.Tensor((2, 2, 3, 3), "float32"):
gv: R.Tensor((2, 2, 3, 3), "float32") = R.vision.roi_align(
x,
rois,
pooled_size=(3, 3),
spatial_scale=1.0,
sample_ratio=2,
layout="NCHW",
mode="avg",
)
return gv
mod = LegalizeOps()(ROIAlign)
assert "call_tir" in str(mod)
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TensorType((2, 2, 3, 3), "float32"),
)
def test_roi_align_legalize_aligned():
@tvm.script.ir_module
class ROIAlign:
@R.function
def main(
x: R.Tensor((1, 1, 4, 4), "float32"),
rois: R.Tensor((1, 5), "float32"),
) -> R.Tensor((1, 1, 1, 1), "float32"):
gv: R.Tensor((1, 1, 1, 1), "float32") = R.vision.roi_align(
x,
rois,
pooled_size=(1, 1),
spatial_scale=1.0,
sample_ratio=2,
aligned=True,
layout="NCHW",
mode="avg",
)
return gv
mod = LegalizeOps()(ROIAlign)
assert "call_tir" in str(mod)
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TensorType((1, 1, 1, 1), "float32"),
)
def test_roi_align_legalize_sample_ratio_zero():
@tvm.script.ir_module
class ROIAlign:
@R.function
def main(
x: R.Tensor((1, 2, 8, 8), "float32"),
rois: R.Tensor((1, 5), "float32"),
) -> R.Tensor((1, 2, 2, 2), "float32"):
gv: R.Tensor((1, 2, 2, 2), "float32") = R.vision.roi_align(
x,
rois,
pooled_size=(2, 2),
spatial_scale=1.0,
sample_ratio=0,
layout="NCHW",
mode="avg",
)
return gv
mod = LegalizeOps()(ROIAlign)
assert "call_tir" in str(mod)
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TensorType((1, 2, 2, 2), "float32"),
)
def test_get_valid_counts_op_correctness():
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
assert relax.op.vision.get_valid_counts(data, 0.5).op == Op.get("relax.vision.get_valid_counts")
def test_get_valid_counts_infer_ty():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
_check_inference(
bb,
relax.op.vision.get_valid_counts(data, score_threshold=0.5, id_index=0, score_index=1),
relax.TupleType(
[
relax.TensorType((2,), "int32"),
relax.TensorType((2, 10, 6), "float32"),
relax.TensorType((2, 10), "int32"),
]
),
)
def test_get_valid_counts_infer_ty_shape_var():
bb = relax.BlockBuilder()
n = tirx.Var("n", "int64")
m = tirx.Var("m", "int64")
k = tirx.Var("k", "int64")
data = relax.Var("data", R.Tensor((n, m, k), "float32"))
_check_inference(
bb,
relax.op.vision.get_valid_counts(data, score_threshold=0.0),
relax.TupleType(
[
relax.TensorType((n,), "int32"),
relax.TensorType((n, m, k), "float32"),
relax.TensorType((n, m), "int32"),
]
),
)
def test_get_valid_counts_wrong_ndim():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((10, 6), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.get_valid_counts(data))
def test_get_valid_counts_invalid_indices():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.get_valid_counts(data, score_index=6))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.get_valid_counts(data, id_index=6))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.get_valid_counts(data, id_index=-2))
def test_nms_op_correctness():
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((2,), "int32"))
indices = relax.Var("indices", R.Tensor((2, 10), "int32"))
assert relax.op.vision.non_max_suppression(data, valid_count, indices).op == Op.get(
"relax.vision.non_max_suppression"
)
def test_nms_infer_ty_return_indices():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((2,), "int32"))
indices = relax.Var("indices", R.Tensor((2, 10), "int32"))
_check_inference(
bb,
relax.op.vision.non_max_suppression(data, valid_count, indices, return_indices=True),
relax.TupleType(
[
relax.TensorType((2, 10), "int32"),
relax.TensorType((2, 1), "int32"),
]
),
)
def test_nms_infer_ty_return_indices_soft_nms():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((2,), "int32"))
indices = relax.Var("indices", R.Tensor((2, 10), "int32"))
_check_inference(
bb,
relax.op.vision.non_max_suppression(
data, valid_count, indices, return_indices=True, soft_nms_sigma=0.5
),
relax.TupleType(
[
relax.TensorType((2, 10, 6), "float32"),
relax.TensorType((2, 10), "int32"),
relax.TensorType((2, 1), "int32"),
]
),
)
def test_nms_infer_ty_return_data():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((2,), "int32"))
indices = relax.Var("indices", R.Tensor((2, 10), "int32"))
_check_inference(
bb,
relax.op.vision.non_max_suppression(data, valid_count, indices, return_indices=False),
relax.TensorType((2, 10, 6), "float32"),
)
def test_nms_infer_ty_return_data_shape_var():
bb = relax.BlockBuilder()
batch_size = tirx.Var("batch_size", "int64")
num_anchors = tirx.Var("num_anchors", "int64")
elem_length = tirx.Var("elem_length", "int64")
data = relax.Var("data", R.Tensor((batch_size, num_anchors, elem_length), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((batch_size,), "int32"))
indices = relax.Var("indices", R.Tensor((batch_size, num_anchors), "int32"))
_check_inference(
bb,
relax.op.vision.non_max_suppression(data, valid_count, indices, return_indices=False),
relax.TensorType((batch_size, num_anchors, elem_length), "float32"),
)
def test_nms_wrong_ndim():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((10, 6), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((2,), "int32"))
indices = relax.Var("indices", R.Tensor((2, 10), "int32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices))
def test_nms_wrong_valid_count_ndim():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((2, 1), "int32"))
indices = relax.Var("indices", R.Tensor((2, 10), "int32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices))
def test_nms_wrong_indices_ndim():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((2,), "int32"))
indices = relax.Var("indices", R.Tensor((20,), "int32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices))
def test_nms_wrong_aux_input_dtype():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count_i64 = relax.Var("valid_count_i64", R.Tensor((2,), "int64"))
valid_count_i32 = relax.Var("valid_count_i32", R.Tensor((2,), "int32"))
indices_i64 = relax.Var("indices_i64", R.Tensor((2, 10), "int64"))
indices_i32 = relax.Var("indices_i32", R.Tensor((2, 10), "int32"))
with pytest.raises(TypeError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count_i64, indices_i32))
with pytest.raises(TypeError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count_i32, indices_i64))
def test_nms_wrong_aux_input_shape():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count_bad_batch = relax.Var("valid_count_bad_batch", R.Tensor((3,), "int32"))
valid_count = relax.Var("valid_count", R.Tensor((2,), "int32"))
indices_bad_batch = relax.Var("indices_bad_batch", R.Tensor((3, 10), "int32"))
indices_bad_anchors = relax.Var("indices_bad_anchors", R.Tensor((2, 9), "int32"))
with pytest.raises(ValueError):
bb.normalize(
relax.op.vision.non_max_suppression(data, valid_count_bad_batch, indices_bad_anchors)
)
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices_bad_batch))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices_bad_anchors))
def test_nms_invalid_indices():
bb = relax.BlockBuilder()
data = relax.Var("data", R.Tensor((2, 10, 6), "float32"))
valid_count = relax.Var("valid_count", R.Tensor((2,), "int32"))
indices = relax.Var("indices", R.Tensor((2, 10), "int32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices, score_index=6))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices, id_index=6))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices, id_index=-2))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.non_max_suppression(data, valid_count, indices, coord_start=3))
def test_get_valid_counts_legalize():
@tvm.script.ir_module
class GVC:
@R.function
def main(
data: R.Tensor((1, 5, 6), "float32"),
) -> R.Tuple(
R.Tensor((1,), "int32"),
R.Tensor((1, 5, 6), "float32"),
R.Tensor((1, 5), "int32"),
):
gv = R.vision.get_valid_counts(data, score_threshold=0.5, id_index=0, score_index=1)
return gv
mod = LegalizeOps()(GVC)
_assert_relax_op_legalized(mod, "relax.vision.get_valid_counts")
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TupleType(
[
relax.TensorType((1,), "int32"),
relax.TensorType((1, 5, 6), "float32"),
relax.TensorType((1, 5), "int32"),
]
),
)
def test_nms_legalize():
@tvm.script.ir_module
class NMS:
@R.function
def main(
data: R.Tensor((1, 5, 6), "float32"),
valid_count: R.Tensor((1,), "int32"),
indices: R.Tensor((1, 5), "int32"),
) -> R.Tuple(R.Tensor((1, 5), "int32"), R.Tensor((1, 1), "int32")):
gv = R.vision.non_max_suppression(
data,
valid_count,
indices,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
soft_nms_sigma=0.0,
score_threshold=0.0,
)
return gv
mod = LegalizeOps()(NMS)
_assert_relax_op_legalized(mod, "relax.vision.non_max_suppression")
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TupleType(
[
relax.TensorType((1, 5), "int32"),
relax.TensorType((1, 1), "int32"),
]
),
)
def test_nms_legalize_soft_nms():
@tvm.script.ir_module
class NMS:
@R.function
def main(
data: R.Tensor((1, 5, 6), "float32"),
valid_count: R.Tensor((1,), "int32"),
indices: R.Tensor((1, 5), "int32"),
) -> R.Tuple(
R.Tensor((1, 5, 6), "float32"),
R.Tensor((1, 5), "int32"),
R.Tensor((1, 1), "int32"),
):
gv = R.vision.non_max_suppression(
data,
valid_count,
indices,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
soft_nms_sigma=0.5,
score_threshold=0.0,
)
return gv
mod = LegalizeOps()(NMS)
_assert_relax_op_legalized(mod, "relax.vision.non_max_suppression")
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TupleType(
[
relax.TensorType((1, 5, 6), "float32"),
relax.TensorType((1, 5), "int32"),
relax.TensorType((1, 1), "int32"),
]
),
)
def test_nms_legalize_return_data():
@tvm.script.ir_module
class NMS:
@R.function
def main(
data: R.Tensor((1, 5, 6), "float32"),
valid_count: R.Tensor((1,), "int32"),
indices: R.Tensor((1, 5), "int32"),
) -> R.Tensor((1, 5, 6), "float32"):
gv = R.vision.non_max_suppression(
data,
valid_count,
indices,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=False,
invalid_to_bottom=True,
soft_nms_sigma=0.0,
score_threshold=0.0,
)
return gv
mod = LegalizeOps()(NMS)
_assert_relax_op_legalized(mod, "relax.vision.non_max_suppression")
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TensorType((1, 5, 6), "float32"),
)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_get_valid_counts_e2e():
"""Run get_valid_counts through legalization and compare with the numpy reference."""
@tvm.script.ir_module
class GVCModule:
@R.function
def main(
data: R.Tensor((2, 5, 6), "float32"),
) -> R.Tuple(
R.Tensor((2,), "int32"),
R.Tensor((2, 5, 6), "float32"),
R.Tensor((2, 5), "int32"),
):
return R.vision.get_valid_counts(data, score_threshold=0.5, id_index=0, score_index=1)
data_np = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[1.0, 0.30, 0.0, 0.0, 1.0, 1.0],
[-1.0, 0.90, 0.0, 0.0, 1.0, 1.0],
[2.0, 0.75, 2.0, 2.0, 3.0, 3.0],
[1.0, 0.10, 4.0, 4.0, 5.0, 5.0],
],
[
[0.0, 0.55, 0.0, 0.0, 1.0, 1.0],
[1.0, 0.80, 1.0, 1.0, 2.0, 2.0],
[2.0, 0.40, 2.0, 2.0, 3.0, 3.0],
[3.0, 0.60, 3.0, 3.0, 4.0, 4.0],
[-1.0, 0.95, 5.0, 5.0, 6.0, 6.0],
],
],
dtype="float32",
)
ref_valid_count, ref_out_data, ref_out_indices = tvm.topi.testing.get_valid_counts_python(
data_np, score_threshold=0.5, id_index=0, score_index=1
)
mod = LegalizeOps()(GVCModule)
exe = tvm.compile(mod, target="llvm")
vm = relax.VirtualMachine(exe, tvm.cpu())
result = vm["main"](tvm.runtime.tensor(data_np, tvm.cpu()))
tvm.testing.assert_allclose(result[0].numpy(), ref_valid_count)
tvm.testing.assert_allclose(result[1].numpy(), ref_out_data)
tvm.testing.assert_allclose(result[2].numpy(), ref_out_indices)
def _prepare_nms_inputs(raw_data: np.ndarray):
"""Prepare classic NMS inputs with the numpy get_valid_counts reference."""
return tvm.topi.testing.get_valid_counts_python(
raw_data, score_threshold=0.5, id_index=0, score_index=1
)
def _run_nms_e2e(
data_np: np.ndarray,
valid_count_np: np.ndarray,
indices_np: np.ndarray,
*,
max_output_size: int = -1,
iou_threshold: float = 0.5,
force_suppress: bool = False,
top_k: int = -1,
coord_start: int = 2,
score_index: int = 1,
id_index: int = 0,
return_indices: bool = True,
invalid_to_bottom: bool = False,
soft_nms_sigma: float = 0.0,
score_threshold: float = 0.0,
):
"""Run classic NMS through legalization and VM execution."""
data_shape = tuple(int(dim) for dim in data_np.shape)
valid_count_shape = tuple(int(dim) for dim in valid_count_np.shape)
indices_shape = tuple(int(dim) for dim in indices_np.shape)
data = relax.Var("data", relax.TensorType(data_shape, "float32"))
valid_count = relax.Var("valid_count", relax.TensorType(valid_count_shape, "int32"))
indices = relax.Var("indices", relax.TensorType(indices_shape, "int32"))
bb = relax.BlockBuilder()
with bb.function("main", (data, valid_count, indices)):
result = bb.emit(
relax.op.vision.non_max_suppression(
data,
valid_count,
indices,
max_output_size=max_output_size,
iou_threshold=iou_threshold,
force_suppress=force_suppress,
top_k=top_k,
coord_start=coord_start,
score_index=score_index,
id_index=id_index,
return_indices=return_indices,
invalid_to_bottom=invalid_to_bottom,
soft_nms_sigma=soft_nms_sigma,
score_threshold=score_threshold,
)
)
bb.emit_func_output(result)
mod = LegalizeOps()(bb.get())
exe = tvm.compile(mod, target="llvm")
vm = relax.VirtualMachine(exe, tvm.cpu())
return vm["main"](
tvm.runtime.tensor(data_np, tvm.cpu()),
tvm.runtime.tensor(valid_count_np, tvm.cpu()),
tvm.runtime.tensor(indices_np, tvm.cpu()),
)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_return_indices():
"""Run classic NMS through legalization and compare with the numpy reference."""
raw_data = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.90, 0.05, 0.05, 1.05, 1.05],
[1.0, 0.85, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.60, 2.0, 2.0, 3.0, 3.0],
[-1.0, 0.99, 0.0, 0.0, 1.0, 1.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_indices, ref_valid_box_count = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
return_indices=True,
invalid_to_bottom=False,
)
tvm.testing.assert_allclose(result[0].numpy(), ref_indices)
tvm.testing.assert_allclose(result[1].numpy(), ref_valid_box_count)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_soft_nms_reorders_by_decayed_score():
"""Soft-NMS should re-rank by decayed scores instead of keeping the initial order."""
raw_data = np.array(
[
[
[0.0, 0.90, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.85, 0.2, 0.2, 1.2, 1.2],
[0.0, 0.80, 2.0, 2.0, 3.0, 3.0],
[-1.0, 0.99, 0.0, 0.0, 1.0, 1.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_out_data, ref_indices, ref_valid_box_count = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=True,
top_k=-1,
coord_start=2,
score_index=1,
id_index=-1,
return_indices=True,
invalid_to_bottom=False,
soft_nms_sigma=0.1,
score_threshold=0.0,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
iou_threshold=0.5,
force_suppress=True,
id_index=-1,
return_indices=True,
invalid_to_bottom=False,
soft_nms_sigma=0.1,
score_threshold=0.0,
)
np.testing.assert_array_equal(ref_indices[0, :3], np.array([0, 2, 1], dtype="int32"))
tvm.testing.assert_allclose(result[0].numpy(), ref_out_data)
tvm.testing.assert_allclose(result[1].numpy(), ref_indices)
tvm.testing.assert_allclose(result[2].numpy(), ref_valid_box_count)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_return_indices_with_invalid_to_bottom():
"""Validate that invalid_to_bottom is a no-op when returning indices."""
raw_data = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.90, 0.05, 0.05, 1.05, 1.05],
[1.0, 0.85, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.60, 2.0, 2.0, 3.0, 3.0],
[-1.0, 0.99, 0.0, 0.0, 1.0, 1.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_indices, ref_valid_box_count = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
return_indices=True,
invalid_to_bottom=True,
)
tvm.testing.assert_allclose(result[0].numpy(), ref_indices)
tvm.testing.assert_allclose(result[1].numpy(), ref_valid_box_count)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_top_k():
"""Validate that classic NMS honors top_k before suppression."""
raw_data = np.array(
[
[
[-1.0, 0.99, 9.0, 9.0, 10.0, 10.0],
[0.0, 0.97, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.96, 2.0, 2.0, 3.0, 3.0],
[0.0, 0.95, 4.0, 4.0, 5.0, 5.0],
[1.0, 0.94, 6.0, 6.0, 7.0, 7.0],
[0.0, 0.20, 8.0, 8.0, 9.0, 9.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_indices, ref_valid_box_count = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=2,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
top_k=2,
return_indices=True,
invalid_to_bottom=False,
)
tvm.testing.assert_allclose(result[0].numpy(), ref_indices)
tvm.testing.assert_allclose(result[1].numpy(), ref_valid_box_count)
np.testing.assert_array_equal(ref_indices, np.array([[1, 2, -1, -1, -1, -1]], dtype="int32"))
np.testing.assert_array_equal(ref_valid_box_count, np.array([[2]], dtype="int32"))
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_force_suppress():
"""Validate that force_suppress ignores class ids when suppressing overlaps."""
raw_data = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[1.0, 0.90, 0.05, 0.05, 1.05, 1.05],
[1.0, 0.80, 2.0, 2.0, 3.0, 3.0],
[-1.0, 0.99, 8.0, 8.0, 9.0, 9.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_indices, ref_valid_box_count = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=True,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
force_suppress=True,
return_indices=True,
invalid_to_bottom=False,
)
tvm.testing.assert_allclose(result[0].numpy(), ref_indices)
tvm.testing.assert_allclose(result[1].numpy(), ref_valid_box_count)
np.testing.assert_array_equal(ref_indices, np.array([[0, 2, -1, -1]], dtype="int32"))
np.testing.assert_array_equal(ref_valid_box_count, np.array([[2]], dtype="int32"))
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_max_output_size():
"""Validate that max_output_size truncates the kept boxes after score sorting."""
raw_data = np.array(
[
[
[0.0, 0.97, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.95, 2.0, 2.0, 3.0, 3.0],
[0.0, 0.93, 4.0, 4.0, 5.0, 5.0],
[0.0, 0.91, 6.0, 6.0, 7.0, 7.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_indices, ref_valid_box_count = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=2,
iou_threshold=1,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=2,
iou_threshold=1,
return_indices=True,
invalid_to_bottom=False,
)
tvm.testing.assert_allclose(result[0].numpy(), ref_indices)
tvm.testing.assert_allclose(result[1].numpy(), ref_valid_box_count)
np.testing.assert_array_equal(ref_indices, np.array([[0, 1, -1, -1]], dtype="int32"))
np.testing.assert_array_equal(ref_valid_box_count, np.array([[2]], dtype="int32"))
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_multi_batch():
"""Validate that classic NMS processes each batch independently."""
raw_data = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.90, 0.05, 0.05, 1.05, 1.05],
[1.0, 0.80, 2.0, 2.0, 3.0, 3.0],
[-1.0, 0.99, 8.0, 8.0, 9.0, 9.0],
],
[
[1.0, 0.96, 0.0, 0.0, 1.0, 1.0],
[2.0, 0.94, 0.04, 0.04, 1.04, 1.04],
[2.0, 0.88, 3.0, 3.0, 4.0, 4.0],
[2.0, 0.30, 6.0, 6.0, 7.0, 7.0],
],
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_indices, ref_valid_box_count = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
return_indices=True,
invalid_to_bottom=False,
)
tvm.testing.assert_allclose(result[0].numpy(), ref_indices)
tvm.testing.assert_allclose(result[1].numpy(), ref_valid_box_count)
np.testing.assert_array_equal(
ref_indices,
np.array([[0, 2, -1, -1], [0, 1, 2, -1]], dtype="int32"),
)
np.testing.assert_array_equal(ref_valid_box_count, np.array([[2], [3]], dtype="int32"))
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_invalid_to_bottom():
"""Validate that invalid_to_bottom compacts only boxes that remain valid after NMS."""
raw_data = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.90, 0.05, 0.05, 1.05, 1.05],
[1.0, 0.85, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.60, 2.0, 2.0, 3.0, 3.0],
[-1.0, 0.99, 8.0, 8.0, 9.0, 9.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_out_data = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=False,
invalid_to_bottom=True,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
return_indices=False,
invalid_to_bottom=True,
)
expected_out_data = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[1.0, 0.85, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.60, 2.0, 2.0, 3.0, 3.0],
[-1.0, -1.0, -1.0, -1.0, -1.0, -1.0],
[-1.0, -1.0, -1.0, -1.0, -1.0, -1.0],
]
],
dtype="float32",
)
tvm.testing.assert_allclose(result.numpy(), ref_out_data)
tvm.testing.assert_allclose(result.numpy(), expected_out_data)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_return_data_without_compaction():
"""Validate the return_indices=False path when invalid boxes stay in-place."""
raw_data = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.90, 0.05, 0.05, 1.05, 1.05],
[1.0, 0.85, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.60, 2.0, 2.0, 3.0, 3.0],
[-1.0, 0.99, 8.0, 8.0, 9.0, 9.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_out_data = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=False,
invalid_to_bottom=False,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
return_indices=False,
invalid_to_bottom=False,
)
expected_out_data = np.array(
[
[
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[-1.0, -1.0, -1.0, -1.0, -1.0, -1.0],
[1.0, 0.85, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.60, 2.0, 2.0, 3.0, 3.0],
[-1.0, -1.0, -1.0, -1.0, -1.0, -1.0],
]
],
dtype="float32",
)
tvm.testing.assert_allclose(result.numpy(), ref_out_data)
tvm.testing.assert_allclose(result.numpy(), expected_out_data)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_nms_e2e_index_remap():
"""Validate that returned indices remap from filtered order back to original order."""
raw_data = np.array(
[
[
[-1.0, 0.99, 9.0, 9.0, 10.0, 10.0],
[0.0, 0.60, 4.0, 4.0, 5.0, 5.0],
[0.0, 0.10, 8.0, 8.0, 9.0, 9.0],
[0.0, 0.95, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.90, 0.05, 0.05, 1.05, 1.05],
[1.0, 0.80, 2.0, 2.0, 3.0, 3.0],
]
],
dtype="float32",
)
valid_count_np, filtered_data_np, filtered_indices_np = _prepare_nms_inputs(raw_data)
ref_indices, ref_valid_box_count = tvm.topi.testing.non_max_suppression_python(
filtered_data_np,
valid_count_np,
filtered_indices_np,
max_output_size=-1,
iou_threshold=0.5,
force_suppress=False,
top_k=-1,
coord_start=2,
score_index=1,
id_index=0,
return_indices=True,
invalid_to_bottom=False,
)
result = _run_nms_e2e(
filtered_data_np,
valid_count_np,
filtered_indices_np,
return_indices=True,
invalid_to_bottom=False,
)
tvm.testing.assert_allclose(result[0].numpy(), ref_indices)
tvm.testing.assert_allclose(result[1].numpy(), ref_valid_box_count)
np.testing.assert_array_equal(ref_indices, np.array([[3, 5, 1, -1, -1, -1]], dtype="int32"))
np.testing.assert_array_equal(ref_valid_box_count, np.array([[3]], dtype="int32"))
def test_roi_pool_op_correctness():
x = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois = relax.Var("rois", R.Tensor((4, 5), "float32"))
assert relax.op.vision.roi_pool(x, rois, (7, 7), 1.0).op == Op.get("relax.vision.roi_pool")
def test_roi_pool_infer_ty():
bb = relax.BlockBuilder()
x = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois = relax.Var("rois", R.Tensor((5, 5), "float32"))
_check_inference(
bb,
relax.op.vision.roi_pool(x, rois, (7, 5), 0.25),
relax.TensorType((5, 3, 7, 5), "float32"),
)
def test_roi_pool_infer_ty_shape_var():
bb = relax.BlockBuilder()
n = tirx.Var("n", "int64")
c = tirx.Var("c", "int64")
h = tirx.Var("h", "int64")
w = tirx.Var("w", "int64")
num_roi = tirx.Var("num_roi", "int64")
x = relax.Var("x", R.Tensor((n, c, h, w), "float32"))
rois = relax.Var("rois", R.Tensor((num_roi, 5), "float32"))
_check_inference(
bb,
relax.op.vision.roi_pool(x, rois, (7, 7), 0.5),
relax.TensorType((num_roi, c, 7, 7), "float32"),
)
def test_roi_pool_wrong_input_ndim():
bb = relax.BlockBuilder()
x0 = relax.Var("x", R.Tensor((2, 3, 32), "float32"))
x1 = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois0 = relax.Var("rois", R.Tensor((4,), "float32"))
rois1 = relax.Var("rois", R.Tensor((4, 5), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.roi_pool(x0, rois1, (7, 7), 1.0))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.roi_pool(x1, rois0, (7, 7), 1.0))
def test_roi_pool_wrong_rois_last_dim():
bb = relax.BlockBuilder()
x = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois = relax.Var("rois", R.Tensor((4, 4), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.roi_pool(x, rois, (7, 7), 1.0))
def test_roi_pool_wrong_layout():
bb = relax.BlockBuilder()
x = relax.Var("x", R.Tensor((2, 3, 32, 32), "float32"))
rois = relax.Var("rois", R.Tensor((4, 5), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.roi_pool(x, rois, (7, 7), 1.0, layout="NHWC"))
def test_roi_pool_legalize():
@tvm.script.ir_module
class ROIPool:
@R.function
def main(
x: R.Tensor((1, 2, 8, 8), "float32"),
rois: R.Tensor((2, 5), "float32"),
) -> R.Tensor((2, 2, 3, 2), "float32"):
gv: R.Tensor((2, 2, 3, 2), "float32") = R.vision.roi_pool(
x,
rois,
pooled_size=(3, 2),
spatial_scale=1.0,
layout="NCHW",
)
return gv
mod = LegalizeOps()(ROIPool)
assert "call_tir" in str(mod)
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TensorType((2, 2, 3, 2), "float32"),
)
def test_all_class_non_max_suppression_infer_ty():
bb = relax.BlockBuilder()
batch_size, num_classes, num_boxes = 10, 8, 5
boxes = relax.Var("boxes", R.Tensor((batch_size, num_boxes, 4), "float32"))
scores = relax.Var("scores", R.Tensor((batch_size, num_classes, num_boxes), "float32"))
max_output_boxes_per_class = relax.const(10, "int64")
iou_threshold = relax.const(0.5, "float32")
score_threshold = relax.const(0.1, "float32")
_check_inference(
bb,
relax.op.vision.all_class_non_max_suppression(
boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, "onnx"
),
relax.TupleType(
[
relax.TensorType((batch_size * num_classes * num_boxes, 3), "int64"),
relax.TensorType((1,), "int64"),
]
),
)
def test_all_class_non_max_suppression_wrong_input_number():
boxes = relax.Var("boxes", R.Tensor((1, 5, 4), "float32"))
scores = relax.Var("scores", R.Tensor((1, 3, 5), "float32"))
with pytest.raises(TypeError):
relax.op.vision.all_class_non_max_suppression(boxes, scores)
def test_all_class_non_max_suppression_infer_ty_shape_var():
bb = relax.BlockBuilder()
batch_size = tirx.Var("batch_size", "int64")
num_classes = tirx.Var("num_classes", "int64")
num_boxes = tirx.Var("num_boxes", "int64")
boxes = relax.Var("boxes", R.Tensor((batch_size, num_boxes, 4), "float32"))
scores = relax.Var("scores", R.Tensor((batch_size, num_classes, num_boxes), "float32"))
max_output_boxes_per_class = relax.const(10, "int64")
iou_threshold = relax.const(0.5, "float32")
score_threshold = relax.const(0.1, "float32")
_check_inference(
bb,
relax.op.vision.all_class_non_max_suppression(
boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, "onnx"
),
relax.TupleType(
[
relax.TensorType((batch_size * num_classes * num_boxes, 3), "int64"),
relax.TensorType((1,), "int64"),
]
),
)
def test_all_class_non_max_suppression_legalize_dynamic_trim():
@tvm.script.ir_module
class NMSModule:
@R.function
def main(
boxes: R.Tensor((1, 5, 4), "float32"),
scores: R.Tensor((1, 2, 5), "float32"),
) -> R.Tuple(R.Tensor(dtype="int64", ndim=2), R.Tensor((1,), "int64")):
max_output_boxes_per_class = R.const(3, "int64")
iou_threshold = R.const(0.5, "float32")
score_threshold = R.const(0.1, "float32")
return R.vision.all_class_non_max_suppression(
boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, "onnx"
)
mod = LegalizeOps()(NMSModule)
# Check legalized function has dynamic output (uses dynamic_strided_slice)
assert "dynamic_strided_slice" in str(mod)
ret_ty = mod["main"].ret_ty
tvm.ir.assert_structural_equal(
ret_ty,
relax.TupleType(
[
relax.TensorType(ndim=2, dtype="int64"),
relax.TensorType((1,), "int64"),
]
),
)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_all_class_non_max_suppression_legalize_e2e():
@tvm.script.ir_module
class NMSModule:
@R.function
def main(
boxes: R.Tensor((1, 5, 4), "float32"),
scores: R.Tensor((1, 2, 5), "float32"),
) -> R.Tuple(R.Tensor(dtype="int64", ndim=2), R.Tensor((1,), "int64")):
max_output_boxes_per_class = R.const(3, "int64")
iou_threshold = R.const(0.5, "float32")
score_threshold = R.const(0.1, "float32")
return R.vision.all_class_non_max_suppression(
boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, "onnx"
)
boxes_data = np.array(
[
[
[0.0, 0.0, 1.0, 1.0],
[0.1, 0.1, 1.1, 1.1],
[2.0, 2.0, 3.0, 3.0],
[4.0, 4.0, 5.0, 5.0],
[6.0, 6.0, 7.0, 7.0],
]
],
dtype=np.float32,
)
scores_data = np.array(
[[[0.9, 0.8, 0.7, 0.6, 0.5], [0.85, 0.75, 0.65, 0.55, 0.45]]],
dtype=np.float32,
)
mod = LegalizeOps()(NMSModule)
# Check type
tvm.ir.assert_structural_equal(
mod["main"].ret_ty,
relax.TupleType(
[
relax.TensorType(ndim=2, dtype="int64"),
relax.TensorType((1,), "int64"),
]
),
)
# Check runtime execution
exe = tvm.compile(mod, target="llvm")
vm = relax.VirtualMachine(exe, tvm.cpu())
result = vm["main"](
tvm.runtime.tensor(boxes_data, tvm.cpu()),
tvm.runtime.tensor(scores_data, tvm.cpu()),
)
selected_indices = result[0].numpy()
num_total_detections = int(result[1].numpy()[0])
tvm.testing.assert_allclose(selected_indices.shape, (num_total_detections, 3))
def test_multibox_transform_loc_op_correctness():
cls = relax.Var("cls", R.Tensor((1, 5, 10), "float32"))
loc = relax.Var("loc", R.Tensor((1, 40), "float32"))
anc = relax.Var("anc", R.Tensor((1, 10, 4), "float32"))
assert relax.op.vision.multibox_transform_loc(
cls, loc, anc, False, 0.0, (1.0, 1.0, 1.0, 1.0), True
).op == Op.get("relax.vision.multibox_transform_loc")
def test_multibox_transform_loc_infer_ty():
bb = relax.BlockBuilder()
cls = relax.Var("cls", R.Tensor((2, 3, 5), "float32"))
loc = relax.Var("loc", R.Tensor((2, 20), "float32"))
anc = relax.Var("anc", R.Tensor((1, 5, 4), "float32"))
_check_inference(
bb,
relax.op.vision.multibox_transform_loc(
cls, loc, anc, False, 0.0, (0.1, 0.1, 0.2, 0.2), True
),
relax.TupleType(
[
relax.TensorType((2, 5, 4), "float32"),
relax.TensorType((2, 3, 5), "float32"),
]
),
)
def test_multibox_transform_loc_wrong_cls_ndim():
bb = relax.BlockBuilder()
cls = relax.Var("cls", R.Tensor((2, 3), "float32"))
loc = relax.Var("loc", R.Tensor((2, 20), "float32"))
anc = relax.Var("anc", R.Tensor((1, 5, 4), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.multibox_transform_loc(cls, loc, anc))
def test_multibox_transform_loc_wrong_shape_relation():
bb = relax.BlockBuilder()
cls = relax.Var("cls", R.Tensor((2, 3, 5), "float32"))
anc = relax.Var("anc", R.Tensor((1, 5, 4), "float32"))
loc_bad_div = relax.Var("loc_bad_div", R.Tensor((2, 19), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.multibox_transform_loc(cls, loc_bad_div, anc))
# Divisible by 4 but loc_dim != 4*N (N=5 -> expect 20, not 24)
loc_bad_n = relax.Var("loc_bad_n", R.Tensor((2, 24), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.multibox_transform_loc(cls, loc_bad_n, anc))
def test_multibox_transform_loc_wrong_anchor_shape():
bb = relax.BlockBuilder()
cls = relax.Var("cls", R.Tensor((2, 3, 5), "float32"))
loc = relax.Var("loc", R.Tensor((2, 20), "float32"))
anc_bad_batch = relax.Var("anc_bad_batch", R.Tensor((2, 5, 4), "float32"))
anc_bad_last = relax.Var("anc_bad_last", R.Tensor((1, 5, 5), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.multibox_transform_loc(cls, loc, anc_bad_batch))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.multibox_transform_loc(cls, loc, anc_bad_last))
def test_multibox_transform_loc_wrong_dtype():
bb = relax.BlockBuilder()
cls = relax.Var("cls", R.Tensor((2, 3, 5), "float32"))
loc = relax.Var("loc", R.Tensor((2, 20), "float16"))
anc = relax.Var("anc", R.Tensor((1, 5, 4), "float32"))
with pytest.raises(TypeError):
bb.normalize(relax.op.vision.multibox_transform_loc(cls, loc, anc))
def test_multibox_transform_loc_wrong_batch():
bb = relax.BlockBuilder()
cls = relax.Var("cls", R.Tensor((2, 3, 5), "float32"))
loc = relax.Var("loc", R.Tensor((1, 20), "float32"))
anc = relax.Var("anc", R.Tensor((1, 5, 4), "float32"))
with pytest.raises(ValueError):
bb.normalize(relax.op.vision.multibox_transform_loc(cls, loc, anc))
def _multibox_ref_numpy(
cls_pred, loc_pred, anchor, variances, clip=False, threshold=0.0, keep_background=True
):
"""Numpy reference aligned with ``topi.vision.multibox_transform_loc``."""
def _softmax(x, axis):
x_max = np.max(x, axis=axis, keepdims=True)
exp = np.exp(x - x_max)
return exp / np.sum(exp, axis=axis, keepdims=True)
B, C, N = cls_pred.shape
loc = loc_pred.reshape(B, N, 4)
scores = _softmax(cls_pred.astype("float64"), axis=1).astype(np.float32)
if threshold > 0.0:
scores = np.where(scores >= threshold, scores, 0.0).astype(np.float32)
if not keep_background:
scores = scores.copy()
scores[:, 0, :] = 0.0
vx, vy, vw, vh = variances
boxes = np.zeros((B, N, 4), dtype=np.float32)
for b in range(B):
for a in range(N):
left, top, right, bottom = anchor[0, a, :]
ay = (top + bottom) * 0.5
ax = (left + right) * 0.5
ah = bottom - top
aw = right - left
ex, ey, ew, eh = loc[b, a, :]
ycenter = ey * vy * ah + ay
xcenter = ex * vx * aw + ax
half_h = 0.5 * np.exp(eh * vh) * ah
half_w = 0.5 * np.exp(ew * vw) * aw
ymin = ycenter - half_h
xmin = xcenter - half_w
ymax = ycenter + half_h
xmax = xcenter + half_w
if clip:
ymin = np.clip(ymin, 0.0, 1.0)
xmin = np.clip(xmin, 0.0, 1.0)
ymax = np.clip(ymax, 0.0, 1.0)
xmax = np.clip(xmax, 0.0, 1.0)
boxes[b, a, :] = (ymin, xmin, ymax, xmax)
return boxes, scores
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_multibox_transform_loc_legalize_e2e():
@tvm.script.ir_module
class Mod:
@R.function
def main(
cls: R.Tensor((1, 3, 5), "float32"),
loc: R.Tensor((1, 20), "float32"),
anc: R.Tensor((1, 5, 4), "float32"),
) -> R.Tuple(R.Tensor((1, 5, 4), "float32"), R.Tensor((1, 3, 5), "float32")):
return R.vision.multibox_transform_loc(
cls,
loc,
anc,
clip=False,
threshold=0.0,
variances=(1.0, 1.0, 1.0, 1.0),
keep_background=True,
)
cls_data = np.random.randn(1, 3, 5).astype(np.float32)
loc_data = np.random.randn(1, 20).astype(np.float32) * 0.05
anc_data = np.array(
[
[
[0.1, 0.1, 0.5, 0.5],
[0.2, 0.2, 0.6, 0.6],
[0.0, 0.0, 1.0, 1.0],
[0.3, 0.3, 0.7, 0.7],
[0.05, 0.05, 0.45, 0.45],
]
],
dtype=np.float32,
)
mod = LegalizeOps()(Mod)
exe = tvm.compile(mod, target="llvm")
vm = relax.VirtualMachine(exe, tvm.cpu())
ref_b, ref_s = _multibox_ref_numpy(cls_data, loc_data, anc_data, (1.0, 1.0, 1.0, 1.0))
out = vm["main"](
tvm.runtime.tensor(cls_data, tvm.cpu()),
tvm.runtime.tensor(loc_data, tvm.cpu()),
tvm.runtime.tensor(anc_data, tvm.cpu()),
)
tvm.testing.assert_allclose(out[0].numpy(), ref_b, rtol=1e-4, atol=1e-5)
tvm.testing.assert_allclose(out[1].numpy(), ref_s, rtol=1e-4, atol=1e-5)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_multibox_transform_loc_legalize_e2e_nonunity_variances():
@tvm.script.ir_module
class Mod:
@R.function
def main(
cls: R.Tensor((1, 3, 5), "float32"),
loc: R.Tensor((1, 20), "float32"),
anc: R.Tensor((1, 5, 4), "float32"),
) -> R.Tuple(R.Tensor((1, 5, 4), "float32"), R.Tensor((1, 3, 5), "float32")):
return R.vision.multibox_transform_loc(
cls,
loc,
anc,
clip=False,
threshold=0.0,
variances=(0.1, 0.1, 0.2, 0.2),
keep_background=True,
)
cls_data = np.random.randn(1, 3, 5).astype(np.float32)
loc_data = np.random.randn(1, 20).astype(np.float32) * 0.05
anc_data = np.array(
[
[
[0.1, 0.1, 0.5, 0.5],
[0.2, 0.2, 0.6, 0.6],
[0.0, 0.0, 1.0, 1.0],
[0.3, 0.3, 0.7, 0.7],
[0.05, 0.05, 0.45, 0.45],
]
],
dtype=np.float32,
)
mod = LegalizeOps()(Mod)
exe = tvm.compile(mod, target="llvm")
vm = relax.VirtualMachine(exe, tvm.cpu())
ref_b, ref_s = _multibox_ref_numpy(cls_data, loc_data, anc_data, (0.1, 0.1, 0.2, 0.2))
out = vm["main"](
tvm.runtime.tensor(cls_data, tvm.cpu()),
tvm.runtime.tensor(loc_data, tvm.cpu()),
tvm.runtime.tensor(anc_data, tvm.cpu()),
)
tvm.testing.assert_allclose(out[0].numpy(), ref_b, rtol=1e-4, atol=1e-5)
tvm.testing.assert_allclose(out[1].numpy(), ref_s, rtol=1e-4, atol=1e-5)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_multibox_transform_loc_legalize_attr_branches():
@tvm.script.ir_module
class Mod:
@R.function
def main(
cls: R.Tensor((1, 3, 4), "float32"),
loc: R.Tensor((1, 16), "float32"),
anc: R.Tensor((1, 4, 4), "float32"),
) -> R.Tuple(R.Tensor((1, 4, 4), "float32"), R.Tensor((1, 3, 4), "float32")):
return R.vision.multibox_transform_loc(
cls,
loc,
anc,
clip=True,
threshold=0.4,
variances=(1.0, 1.0, 1.0, 1.0),
keep_background=False,
)
cls_data = np.array(
[[[2.0, 0.1, -0.5, 0.0], [0.2, 2.2, 0.3, -1.0], [0.1, 0.4, 2.0, 0.5]]],
dtype=np.float32,
)
loc_data = np.array(
[[0.1, -0.2, 0.0, 0.0, -0.2, 0.1, 0.3, -0.1, 0.0, 0.0, 0.8, 0.8, 0.2, 0.2, -0.6, -0.6]],
dtype=np.float32,
)
anc_data = np.array(
[[[0.1, 0.1, 0.5, 0.5], [0.2, 0.2, 0.6, 0.6], [0.0, 0.0, 1.0, 1.0], [0.4, 0.4, 1.2, 1.2]]],
dtype=np.float32,
)
mod = LegalizeOps()(Mod)
exe = tvm.compile(mod, target="llvm")
vm = relax.VirtualMachine(exe, tvm.cpu())
ref_b, ref_s = _multibox_ref_numpy(
cls_data,
loc_data,
anc_data,
(1.0, 1.0, 1.0, 1.0),
clip=True,
threshold=0.4,
keep_background=False,
)
out = vm["main"](
tvm.runtime.tensor(cls_data, tvm.cpu()),
tvm.runtime.tensor(loc_data, tvm.cpu()),
tvm.runtime.tensor(anc_data, tvm.cpu()),
)
boxes = out[0].numpy()
scores = out[1].numpy()
tvm.testing.assert_allclose(boxes, ref_b, rtol=1e-4, atol=1e-5)
tvm.testing.assert_allclose(scores, ref_s, rtol=1e-4, atol=1e-5)
assert np.all(boxes >= 0.0) and np.all(boxes <= 1.0)
tvm.testing.assert_allclose(scores[:, 0, :], np.zeros_like(scores[:, 0, :]))
if __name__ == "__main__":
tvm.testing.main()