# 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()