Files
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

1132 lines
47 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.
# pylint: disable=import-error, invalid-name, no-member, too-many-locals, too-many-arguments, undefined-variable, too-many-nested-blocks, too-many-branches, too-many-statements, too-many-function-args
"""Non-maximum suppression operator"""
import tvm
from tvm import te
from tvm.script.ir_builder import IRBuilder
from tvm.script.ir_builder import tirx as T
from tvm.tirx import if_then_else
from .. import reduction
from ..math import cast
from ..scan import cumsum
from ..sort import argsort
from ..transform import gather, reshape
from .nms_util import (
binary_search,
collect_selected_indices,
collect_selected_indices_and_scores,
run_all_class_nms,
)
def _get_valid_counts_ir(
data, score_threshold, id_index, score_index, valid_count, out_tensor, out_indices
):
"""IR for get_valid_counts. Filters boxes by score and compacts valid ones to the top."""
batch_size = data.shape[0]
num_anchors = data.shape[1]
box_data_length = data.shape[2]
with IRBuilder() as ib:
data = T.buffer_proxy(data)
valid_count = T.buffer_proxy(valid_count)
out_tensor = T.buffer_proxy(out_tensor)
out_indices = T.buffer_proxy(out_indices)
with T.parallel(0, batch_size) as i:
valid_count[i] = T.int32(0)
with T.serial(0, num_anchors) as j:
score = data[i, j, score_index]
if id_index < 0:
is_valid = score > score_threshold
else:
is_valid = tvm.tirx.all(score > score_threshold, data[i, j, id_index] >= 0)
with T.If(is_valid):
with T.Then():
cur = valid_count[i]
with T.serial(0, box_data_length) as k:
out_tensor[i, cur, k] = data[i, j, k]
out_indices[i, cur] = j
valid_count[i] = cur + 1
# Fill remaining slots with -1
with T.serial(0, num_anchors) as j:
with T.If(j >= valid_count[i]):
with T.Then():
with T.serial(0, box_data_length) as k:
out_tensor[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
out_indices[i, j] = T.int32(-1)
return ib.get()
def get_valid_counts(data, score_threshold=0, id_index=0, score_index=1):
"""Get valid count of bounding boxes given a score threshold.
Also moves valid boxes to the top of input data.
Parameters
----------
data : tvm.te.Tensor
Input data. 3-D tensor with shape [batch_size, num_anchors, elem_length].
score_threshold : optional, float
Lower limit of score for valid bounding boxes.
id_index : optional, int
Index of the class categories, -1 to disable.
score_index: optional, int
Index of the scores/confidence of boxes.
Returns
-------
valid_count : tvm.te.Tensor
1-D tensor for valid number of boxes, shape [batch_size].
out_tensor : tvm.te.Tensor
Rearranged data tensor, shape [batch_size, num_anchors, elem_length].
out_indices: tvm.te.Tensor
Related index in input data, shape [batch_size, num_anchors].
"""
batch_size = data.shape[0]
num_anchors = data.shape[1]
box_data_length = data.shape[2]
is_score_threshold_tensor = isinstance(score_threshold, te.Tensor)
if not is_score_threshold_tensor:
score_threshold = tvm.tirx.const(score_threshold, dtype=data.dtype)
id_index_const = tvm.tirx.const(id_index, "int32")
score_index_const = tvm.tirx.const(score_index, "int32")
valid_count_buf = tvm.tirx.decl_buffer((batch_size,), "int32", "valid_count", layout=None)
out_tensor_buf = tvm.tirx.decl_buffer(
(batch_size, num_anchors, box_data_length), data.dtype, "out_tensor", layout=None
)
out_indices_buf = tvm.tirx.decl_buffer(
(batch_size, num_anchors), "int32", "out_indices", layout=None
)
if is_score_threshold_tensor:
score_thresh_buf = tvm.tirx.decl_buffer(
score_threshold.shape, score_threshold.dtype, "score_threshold", layout=None
)
valid_count, out_tensor, out_indices = te.extern(
[(batch_size,), (batch_size, num_anchors, box_data_length), (batch_size, num_anchors)],
[data, score_threshold],
lambda ins, outs: _get_valid_counts_ir(
ins[0],
ins[1],
id_index_const,
score_index_const,
outs[0],
outs[1],
outs[2],
),
dtype=["int32", data.dtype, "int32"],
out_buffers=[valid_count_buf, out_tensor_buf, out_indices_buf],
in_buffers=[
tvm.tirx.decl_buffer(data.shape, data.dtype, "data", layout=None),
score_thresh_buf,
],
name="get_valid_counts",
tag="get_valid_counts",
)
else:
# score_threshold is a TIR constant, not a tensor
def _ir_with_const_threshold(ins, outs):
return _get_valid_counts_ir(
ins[0],
score_threshold,
id_index_const,
score_index_const,
outs[0],
outs[1],
outs[2],
)
valid_count, out_tensor, out_indices = te.extern(
[(batch_size,), (batch_size, num_anchors, box_data_length), (batch_size, num_anchors)],
[data],
_ir_with_const_threshold,
dtype=["int32", data.dtype, "int32"],
out_buffers=[valid_count_buf, out_tensor_buf, out_indices_buf],
in_buffers=[tvm.tirx.decl_buffer(data.shape, data.dtype, "data", layout=None)],
name="get_valid_counts",
tag="get_valid_counts",
)
return valid_count, out_tensor, out_indices
def _classic_nms_ir(
data,
sorted_index,
valid_count,
indices,
batch_size,
num_anchors,
box_data_length,
max_output_size,
iou_threshold,
force_suppress,
top_k,
coord_start,
score_index,
id_index,
return_indices,
out_data,
out_box_indices,
out_valid_box_count,
soft_nms_sigma=0.0,
score_threshold=0.0,
):
"""IR for classic single-class non-maximum suppression."""
with IRBuilder() as ib:
data = T.buffer_proxy(data)
sorted_index = T.buffer_proxy(sorted_index)
valid_count = T.buffer_proxy(valid_count)
indices = T.buffer_proxy(indices)
out_data = T.buffer_proxy(out_data)
out_box_indices = T.buffer_proxy(out_box_indices)
if out_valid_box_count is not None:
out_valid_box_count = T.buffer_proxy(out_valid_box_count)
is_soft_nms = soft_nms_sigma > 0.0
# For hard NMS the historical threshold is 0.0; for soft NMS use score_threshold.
thresh = tvm.tirx.Cast(data.dtype, T.float32(score_threshold if is_soft_nms else 0.0))
with T.parallel(0, batch_size) as i:
# Step 1: Reorder data by sorted score
nkeep_buf = T.alloc_buffer((1,), "int32", scope="local")
nkeep_local = T.buffer_proxy(nkeep_buf)
nkeep_local[0] = valid_count[i]
with T.If(tvm.tirx.all(top_k > 0, top_k < nkeep_local[0])):
with T.Then():
nkeep_local[0] = top_k
# Copy sorted boxes to output
with T.serial(0, num_anchors) as j:
with T.If(j < nkeep_local[0]):
with T.Then():
src_idx = sorted_index[i, j]
with T.serial(0, box_data_length) as k:
out_data[i, j, k] = data[i, src_idx, k]
out_box_indices[i, j] = sorted_index[i, j]
with T.Else():
with T.serial(0, box_data_length) as k:
out_data[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
out_box_indices[i, j] = T.int32(-1)
# Step 2: Apply NMS - greedy suppression
num_valid_boxes_buf = T.alloc_buffer((1,), "int32", scope="local")
num_valid_boxes = T.buffer_proxy(num_valid_boxes_buf)
num_valid_boxes[0] = T.int32(0)
best_idx_buf = T.alloc_buffer((1,), "int32", scope="local")
best_idx = T.buffer_proxy(best_idx_buf)
best_score_buf = T.alloc_buffer((1,), data.dtype, scope="local")
best_score = T.buffer_proxy(best_score_buf)
tmp_idx_buf = T.alloc_buffer((1,), "int32", scope="local")
tmp_idx = T.buffer_proxy(tmp_idx_buf)
tmp_val_buf = T.alloc_buffer((1,), data.dtype, scope="local")
tmp_val = T.buffer_proxy(tmp_val_buf)
zero = tvm.tirx.Cast(data.dtype, T.float32(0.0))
def compute_iou(lhs_idx, rhs_idx):
lhs_l = tvm.te.min(
out_data[i, lhs_idx, coord_start],
out_data[i, lhs_idx, coord_start + 2],
)
lhs_t = tvm.te.min(
out_data[i, lhs_idx, coord_start + 1],
out_data[i, lhs_idx, coord_start + 3],
)
lhs_r = tvm.te.max(
out_data[i, lhs_idx, coord_start],
out_data[i, lhs_idx, coord_start + 2],
)
lhs_b = tvm.te.max(
out_data[i, lhs_idx, coord_start + 1],
out_data[i, lhs_idx, coord_start + 3],
)
rhs_l = tvm.te.min(
out_data[i, rhs_idx, coord_start],
out_data[i, rhs_idx, coord_start + 2],
)
rhs_t = tvm.te.min(
out_data[i, rhs_idx, coord_start + 1],
out_data[i, rhs_idx, coord_start + 3],
)
rhs_r = tvm.te.max(
out_data[i, rhs_idx, coord_start],
out_data[i, rhs_idx, coord_start + 2],
)
rhs_b = tvm.te.max(
out_data[i, rhs_idx, coord_start + 1],
out_data[i, rhs_idx, coord_start + 3],
)
width = tvm.te.max(zero, tvm.te.min(lhs_r, rhs_r) - tvm.te.max(lhs_l, rhs_l))
height = tvm.te.max(zero, tvm.te.min(lhs_b, rhs_b) - tvm.te.max(lhs_t, rhs_t))
intersection = height * width
union = (
(lhs_r - lhs_l) * (lhs_b - lhs_t)
+ (rhs_r - rhs_l) * (rhs_b - rhs_t)
- intersection
)
return tvm.tirx.Select(union <= zero, zero, intersection / union)
if is_soft_nms:
# LiteRT soft-NMS selects the current highest-score candidate each round.
soft_nms_scale = tvm.tirx.Cast(data.dtype, T.float32(-0.5 / soft_nms_sigma))
with T.serial(0, nkeep_local[0]) as _:
with T.If(
tvm.tirx.Select(
max_output_size > 0,
num_valid_boxes[0] < max_output_size,
tvm.tirx.const(True),
)
):
with T.Then():
best_idx[0] = T.int32(-1)
best_score[0] = thresh
with T.serial(0, nkeep_local[0]) as j:
with T.If(
tvm.tirx.all(
j >= num_valid_boxes[0],
out_box_indices[i, j] >= 0,
out_data[i, j, score_index] > best_score[0],
)
):
with T.Then():
best_idx[0] = j
best_score[0] = out_data[i, j, score_index]
with T.If(best_idx[0] >= 0):
with T.Then():
with T.If(best_idx[0] != num_valid_boxes[0]):
with T.Then():
tmp_idx[0] = out_box_indices[i, num_valid_boxes[0]]
out_box_indices[i, num_valid_boxes[0]] = (
out_box_indices[i, best_idx[0]]
)
out_box_indices[i, best_idx[0]] = tmp_idx[0]
with T.serial(0, box_data_length) as k:
tmp_val[0] = out_data[i, num_valid_boxes[0], k]
out_data[i, num_valid_boxes[0], k] = out_data[
i, best_idx[0], k
]
out_data[i, best_idx[0], k] = tmp_val[0]
with T.serial(0, nkeep_local[0]) as j:
with T.If(
tvm.tirx.all(
j > num_valid_boxes[0],
out_box_indices[i, j] >= 0,
out_data[i, j, score_index] > thresh,
)
):
with T.Then():
do_suppress = tvm.tirx.const(False)
if force_suppress:
do_suppress = tvm.tirx.const(True)
elif id_index >= 0:
do_suppress = (
out_data[i, num_valid_boxes[0], id_index]
== out_data[i, j, id_index]
)
else:
do_suppress = tvm.tirx.const(True)
with T.If(do_suppress):
with T.Then():
iou = compute_iou(num_valid_boxes[0], j)
with T.If(iou >= iou_threshold):
with T.Then():
out_box_indices[i, j] = T.int32(-1)
with T.If(iou < iou_threshold):
with T.Then():
out_data[i, j, score_index] = (
out_data[i, j, score_index]
* tvm.tirx.exp(
soft_nms_scale * iou * iou
)
)
with T.If(
out_data[i, j, score_index]
<= thresh
):
with T.Then():
out_box_indices[i, j] = (
T.int32(-1)
)
num_valid_boxes[0] = num_valid_boxes[0] + 1
if return_indices:
out_valid_box_count[i, 0] = num_valid_boxes[0]
with T.serial(0, num_anchors) as j:
with T.If(j < num_valid_boxes[0]):
with T.Then():
orig_idx = out_box_indices[i, j]
out_box_indices[i, j] = indices[i, orig_idx]
with T.If(j >= num_valid_boxes[0]):
with T.Then():
with T.serial(0, box_data_length) as k:
out_data[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
out_box_indices[i, j] = T.int32(-1)
else:
with T.serial(0, num_anchors) as j:
with T.If(j >= num_valid_boxes[0]):
with T.Then():
with T.serial(0, box_data_length) as k:
out_data[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
else:
with T.serial(0, nkeep_local[0]) as j:
with T.If(
tvm.tirx.all(
out_data[i, j, score_index] > thresh,
tvm.tirx.Select(
max_output_size > 0,
num_valid_boxes[0] < max_output_size,
tvm.tirx.const(True),
),
)
):
with T.Then():
num_valid_boxes[0] = num_valid_boxes[0] + 1
with T.serial(0, nkeep_local[0]) as k:
with T.If(
tvm.tirx.all(k > j, out_data[i, k, score_index] > thresh)
):
with T.Then():
do_suppress = tvm.tirx.const(False)
if force_suppress:
do_suppress = tvm.tirx.const(True)
elif id_index >= 0:
do_suppress = (
out_data[i, j, id_index] == out_data[i, k, id_index]
)
else:
do_suppress = tvm.tirx.const(True)
with T.If(do_suppress):
with T.Then():
iou = compute_iou(j, k)
with T.If(iou >= iou_threshold):
with T.Then():
out_data[i, k, score_index] = tvm.tirx.Cast(
data.dtype, T.float32(-1.0)
)
out_box_indices[i, k] = T.int32(-1)
with T.Else():
with T.serial(0, box_data_length) as k:
out_data[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
out_box_indices[i, j] = T.int32(-1)
if return_indices:
valid_idx_buf = T.alloc_buffer((1,), "int32", scope="local")
valid_idx = T.buffer_proxy(valid_idx_buf)
valid_idx[0] = T.int32(0)
with T.serial(0, num_anchors) as j:
with T.If(out_box_indices[i, j] >= 0):
with T.Then():
orig_idx = out_box_indices[i, j]
out_box_indices[i, valid_idx[0]] = indices[i, orig_idx]
valid_idx[0] = valid_idx[0] + 1
out_valid_box_count[i, 0] = valid_idx[0]
with T.serial(0, num_anchors) as j:
with T.If(j >= valid_idx[0]):
with T.Then():
out_box_indices[i, j] = T.int32(-1)
return ib.get()
def 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,
):
"""Non-maximum suppression operator for object detection.
Parameters
----------
data : tvm.te.Tensor
3-D tensor with shape [batch_size, num_anchors, elem_length].
valid_count : tvm.te.Tensor
1-D tensor for valid number of boxes, shape [batch_size].
indices : tvm.te.Tensor
2-D tensor with shape [batch_size, num_anchors].
max_output_size : optional, int
Max number of output valid boxes for each instance.
Return all valid boxes if the value is less than 0.
iou_threshold : optional, float
Non-maximum suppression IoU threshold.
force_suppress : optional, boolean
Whether to suppress all detections regardless of class_id. When
``id_index`` is ``-1``, all valid boxes are treated as belonging to the
same class, so this flag has the same effect as ``True``.
top_k : optional, int
Keep maximum top k detections before nms, -1 for no limit.
coord_start : required, int
Start index of the consecutive 4 coordinates.
score_index: optional, int
Index of the scores/confidence of boxes.
id_index : optional, int
Index of the class categories, -1 to disable.
return_indices : optional, boolean
Whether to return box indices in input data.
invalid_to_bottom : optional, boolean
Whether to move all valid bounding boxes to the top.
soft_nms_sigma : optional, float
Sigma for soft-NMS Gaussian penalty. 0.0 means standard hard NMS.
score_threshold : optional, float
Post-decay minimum score for a box to remain eligible during soft-NMS.
Only used when ``soft_nms_sigma > 0``. This is distinct from
``get_valid_counts.score_threshold``, which filters boxes before NMS.
Returns
-------
out : tvm.te.Tensor or tuple of tvm.te.Tensor
The return tuple shape depends on ``soft_nms_sigma``.
If ``return_indices`` is ``True`` and ``soft_nms_sigma`` is ``0.0``,
returns a 2-tuple ``(box_indices, valid_box_count)``.
If ``return_indices`` is ``True`` and ``soft_nms_sigma > 0``,
returns a 3-tuple ``(out_data, box_indices, valid_box_count)`` where
decayed ``out_data`` is prepended and has the same shape as the input
data.
Otherwise returns the modified data tensor.
"""
batch_size = data.shape[0]
num_anchors = data.shape[1]
box_data_length = data.shape[2]
if isinstance(max_output_size, int):
max_output_size = tvm.tirx.const(max_output_size, dtype="int32")
if isinstance(iou_threshold, float | int):
iou_threshold = tvm.tirx.const(iou_threshold, dtype=data.dtype)
# Sort by score
score_shape = (batch_size, num_anchors)
score_tensor = te.compute(
score_shape, lambda i, j: data[i, j, score_index], name="score_tensor"
)
sort_tensor = argsort(score_tensor, valid_count=valid_count, axis=1, is_ascend=False)
data_buf = tvm.tirx.decl_buffer(data.shape, data.dtype, "data", layout=None)
sort_buf = tvm.tirx.decl_buffer(
sort_tensor.shape, sort_tensor.dtype, "sorted_index", layout=None
)
valid_count_buf = tvm.tirx.decl_buffer(
valid_count.shape, valid_count.dtype, "valid_count", layout=None
)
indices_buf = tvm.tirx.decl_buffer(indices.shape, indices.dtype, "indices", layout=None)
out_data_buf = tvm.tirx.decl_buffer(data.shape, data.dtype, "out_data", layout=None)
out_box_indices_buf = tvm.tirx.decl_buffer(
(batch_size, num_anchors), "int32", "out_box_indices", layout=None
)
if return_indices:
out_valid_box_count_buf = tvm.tirx.decl_buffer(
(batch_size, 1), "int32", "out_valid_box_count", layout=None
)
out_data, out_box_indices, out_valid_box_count = te.extern(
[data.shape, (batch_size, num_anchors), (batch_size, 1)],
[data, sort_tensor, valid_count, indices],
lambda ins, outs: _classic_nms_ir(
ins[0],
ins[1],
ins[2],
ins[3],
batch_size,
num_anchors,
box_data_length,
max_output_size,
iou_threshold,
force_suppress,
top_k,
coord_start,
score_index,
id_index,
return_indices,
outs[0],
outs[1],
outs[2],
soft_nms_sigma,
score_threshold,
),
dtype=[data.dtype, "int32", "int32"],
out_buffers=[out_data_buf, out_box_indices_buf, out_valid_box_count_buf],
in_buffers=[data_buf, sort_buf, valid_count_buf, indices_buf],
name="non_max_suppression",
tag="non_max_suppression",
)
if soft_nms_sigma > 0.0:
return [out_data, out_box_indices, out_valid_box_count]
return [out_box_indices, out_valid_box_count]
out_data, out_box_indices = te.extern(
[data.shape, (batch_size, num_anchors)],
[data, sort_tensor, valid_count, indices],
lambda ins, outs: _classic_nms_ir(
ins[0],
ins[1],
ins[2],
ins[3],
batch_size,
num_anchors,
box_data_length,
max_output_size,
iou_threshold,
force_suppress,
top_k,
coord_start,
score_index,
id_index,
return_indices,
outs[0],
outs[1],
None,
soft_nms_sigma,
score_threshold,
),
dtype=[data.dtype, "int32"],
out_buffers=[out_data_buf, out_box_indices_buf],
in_buffers=[data_buf, sort_buf, valid_count_buf, indices_buf],
name="non_max_suppression",
tag="non_max_suppression",
)
if invalid_to_bottom:
# Rearrange to move valid boxes to top
return _rearrange_out(out_data, batch_size, num_anchors, box_data_length, score_index)
return out_data
def _rearrange_out(data, batch_size, num_anchors, box_data_length, score_index):
"""Move valid boxes (score >= 0) to the top of output."""
out_buf = tvm.tirx.decl_buffer(
(batch_size, num_anchors, box_data_length), data.dtype, "rearranged", layout=None
)
def _rearrange_ir(ins, outs):
with IRBuilder() as ib:
data = T.buffer_proxy(ins[0])
out = T.buffer_proxy(outs[0])
with T.parallel(0, batch_size) as i:
valid_idx_buf = T.alloc_buffer((1,), "int32", scope="local")
valid_idx = T.buffer_proxy(valid_idx_buf)
valid_idx[0] = T.int32(0)
with T.serial(0, num_anchors) as j:
with T.If(data[i, j, score_index] >= tvm.tirx.Cast(data.dtype, T.float32(0.0))):
with T.Then():
with T.serial(0, box_data_length) as k:
out[i, valid_idx[0], k] = data[i, j, k]
valid_idx[0] = valid_idx[0] + 1
with T.serial(0, num_anchors) as j:
with T.If(j >= valid_idx[0]):
with T.Then():
with T.serial(0, box_data_length) as k:
out[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
return ib.get()
return te.extern(
[(batch_size, num_anchors, box_data_length)],
[data],
_rearrange_ir,
dtype=[data.dtype],
out_buffers=[out_buf],
name="rearrange_out",
tag="rearrange_out",
)
def _nms_loop(
batch_size,
top_k,
iou_threshold,
max_output_size,
valid_count,
on_new_valid_box_func,
on_new_invalidated_box_func,
needs_bbox_check_func,
calc_overlap_func,
out_scores,
num_valid_boxes,
score_threshold=None,
):
"""NMS loop using modern IRBuilder. Must be called within IRBuilder context."""
out_scores = T.buffer_proxy(out_scores)
num_valid_boxes = T.buffer_proxy(num_valid_boxes)
def nms_inner_loop(i, j, nkeep, num_valid_boxes_local):
on_new_valid_box_func(0, num_valid_boxes_local[0], i, j)
num_valid_boxes_local[0] = num_valid_boxes_local[0] + 1
num_boxes_to_check = nkeep - (j + 1)
with T.parallel(0, num_boxes_to_check) as _k:
k = j + 1 + _k
with T.If(
tvm.tirx.all(
k < nkeep,
out_scores[i, k] > 0, # is the box k still valid?
needs_bbox_check_func(i, j, k),
)
):
with T.Then():
iou = calc_overlap_func(i, j, k)
with T.If(iou >= iou_threshold):
with T.Then():
out_scores[i, k] = T.float32(-1.0)
on_new_invalidated_box_func(i, k)
with T.serial(0, batch_size) as i:
nkeep = if_then_else(tvm.tirx.all(top_k > 0, top_k < valid_count[i]), top_k, valid_count[i])
with T.If(tvm.tirx.all(iou_threshold > te.const(0), valid_count[i] > te.const(0))):
with T.Then():
num_valid_boxes_local_buf = T.alloc_buffer((1,), "int32", scope="local")
num_valid_boxes_local = T.buffer_proxy(num_valid_boxes_local_buf)
num_valid_boxes_local[0] = T.int32(0)
with T.serial(0, nkeep) as j:
with T.If(
tvm.tirx.all(
out_scores[i, j] > -1.0, # box is still valid
num_valid_boxes_local[0] < max_output_size, # haven't reached max limit
)
):
with T.Then():
if score_threshold is not None:
with T.If(out_scores[i, j] > score_threshold[()]):
with T.Then():
nms_inner_loop(i, j, nkeep, num_valid_boxes_local)
else:
nms_inner_loop(i, j, nkeep, num_valid_boxes_local)
num_valid_boxes[i] = num_valid_boxes_local[0]
with T.Else():
num_valid_boxes[i] = T.int32(0)
def _get_valid_box_count(scores, score_threshold):
batch_classes, num_boxes = scores.shape
def searchsorted_ir(scores_buf, score_thresh_buf, valid_count_buf):
with IRBuilder() as ib:
with T.parallel(0, batch_classes) as i:
if hasattr(score_threshold, "shape"):
if len(score_threshold.shape) == 0:
score_thresh_scalar = score_thresh_buf[()]
elif len(score_threshold.shape) == 1 and score_threshold.shape[0] > 0:
score_thresh_scalar = score_thresh_buf[0]
else:
score_thresh_scalar = tvm.tirx.FloatImm("float32", 0.0)
else:
score_thresh_scalar = score_threshold
binary_search(i, num_boxes, scores_buf, score_thresh_scalar, valid_count_buf)
return ib.get()
scores_buf = tvm.tirx.decl_buffer(
scores.shape, scores.dtype, "scores_buf", data_alignment=8, layout=None
)
searchsorted_buf = tvm.tirx.decl_buffer(
(batch_classes,), "int32", "searchsorted", data_alignment=8, layout=None
)
if hasattr(score_threshold, "shape"):
score_thresh_buf = tvm.tirx.decl_buffer(
score_threshold.shape,
score_threshold.dtype,
"score_thresh_buf",
data_alignment=8,
layout=None,
)
return te.extern(
[(batch_classes,)],
[scores, score_threshold],
lambda ins, outs: searchsorted_ir(ins[0], ins[1], outs[0]),
dtype=["int32"],
in_buffers=[scores_buf, score_thresh_buf],
out_buffers=[searchsorted_buf],
name="searchsorted",
tag="searchsorted",
)
else:
def searchsorted_ir_scalar(scores_buf, valid_count_buf):
with IRBuilder() as ib:
with T.parallel(0, batch_classes) as i:
if isinstance(score_threshold, te.Tensor):
if len(score_threshold.shape) == 0:
score_thresh_tir = score_threshold()
elif len(score_threshold.shape) == 1 and score_threshold.shape[0] == 1:
score_thresh_tir = score_threshold[0]
else:
score_thresh_tir = tvm.tirx.FloatImm("float32", 0.0)
else:
score_thresh_tir = tvm.tirx.FloatImm("float32", float(score_threshold))
binary_search(i, num_boxes, scores_buf, score_thresh_tir, valid_count_buf)
return ib.get()
return te.extern(
[(batch_classes,)],
[scores],
lambda ins, outs: searchsorted_ir_scalar(ins[0], outs[0]),
dtype=["int32"],
in_buffers=[scores_buf],
out_buffers=[searchsorted_buf],
name="searchsorted",
tag="searchsorted",
)
def _collect_selected_indices_ir(
num_class, selected_indices, num_detections, row_offsets, out, max_output_boxes_per_class=None
):
batch_classes, _ = selected_indices.shape
with IRBuilder() as ib:
with T.seq_scope():
out = T.buffer_proxy(out)
# Initialize output buffer to zero
# Calculate the actual output shape based on max_output_boxes_per_class
if isinstance(max_output_boxes_per_class, int):
max_output_rows = batch_classes * max_output_boxes_per_class
else:
# Fallback to a reasonable default if max_output_boxes_per_class is not an integer
max_output_rows = batch_classes * 10
with T.serial(0, max_output_rows) as init_i:
with T.serial(0, 3) as init_j: # 3 columns
out[init_i, init_j] = cast(0, "int64")
with T.parallel(0, batch_classes) as i:
i_64 = cast(i, "int64")
batch_id = i_64 // num_class
class_id = i_64 % num_class
if isinstance(max_output_boxes_per_class, int):
limit = tvm.tirx.min(
num_detections[i], tvm.tirx.IntImm("int32", max_output_boxes_per_class)
)
elif isinstance(max_output_boxes_per_class, te.Tensor):
if len(max_output_boxes_per_class.shape) == 0:
max_boxes_val = max_output_boxes_per_class[()]
else:
max_boxes_val = max_output_boxes_per_class[0]
limit = tvm.tirx.min(num_detections[i], max_boxes_val)
else:
limit = num_detections[i]
with T.serial(0, limit) as j:
out[row_offsets[i] + j, 0] = batch_id
out[row_offsets[i] + j, 1] = class_id
out[row_offsets[i] + j, 2] = cast(selected_indices[i, j], "int64")
return ib.get()
def _collect_selected_indices_and_scores_ir(
selected_indices,
selected_scores,
num_detections,
row_offsets,
num_total_detections,
collected_indices,
collected_scores,
):
batch_size, num_class = row_offsets.shape
num_boxes = selected_indices.shape[1]
with IRBuilder() as ib:
collected_indices = T.buffer_proxy(collected_indices)
collected_scores = T.buffer_proxy(collected_scores)
zero = cast(0, "int64")
with T.parallel(0, batch_size * num_class) as i:
i_64 = cast(i, "int64")
batch_id = i_64 // num_class
class_id = i_64 % num_class
with T.serial(0, num_boxes) as j:
with T.If(j < num_detections[batch_id, class_id]):
with T.Then():
offset = row_offsets[batch_id, class_id] + j
collected_indices[batch_id, offset, 0] = class_id
collected_indices[batch_id, offset, 1] = cast(
selected_indices[i, j], "int64"
)
collected_scores[batch_id, offset] = selected_scores[i, j]
with T.Else():
offset = (
num_total_detections[batch_id]
+ class_id * num_boxes
- row_offsets[batch_id, class_id]
+ j
- num_detections[batch_id, class_id]
)
collected_indices[batch_id, offset, 0] = zero
collected_indices[batch_id, offset, 1] = zero
collected_scores[batch_id, offset] = T.float32(0.0)
return ib.get()
def all_class_non_max_suppression(
boxes,
scores,
max_output_boxes_per_class,
iou_threshold,
score_threshold,
output_format="onnx",
output_shape=None,
):
"""Non-maximum suppression operator for object detection, corresponding to ONNX
NonMaxSuppression and TensorFlow combined_non_max_suppression.
NMS is performed for each class separately.
Parameters
----------
boxes : tvm.te.Tensor
3-D tensor with shape (batch_size, num_boxes, 4)
scores : tvm.te.Tensor
3-D tensor with shape (batch_size, num_classes, num_boxes)
max_output_boxes_per_class : int or tvm.te.Tensor, optional
The maxinum number of output selected boxes per class
iou_threshold : float or tvm.te.Tensor, optional
IoU test threshold
score_threshold : float or tvm.te.Tensor, optional
Score threshold to filter out low score boxes early
output_format : str, optional
"onnx" or "tensorflow", see below.
Returns
-------
out : list of tvm.te.Tensor
If `output_format` is "onnx", the output is two tensors. The first is `indices` of size
`(batch_size * num_class* num_boxes , 3)` and the second is a scalar tensor
`num_total_detection` of shape `(1,)` representing the total number of selected
boxes. The three values in `indices` encode batch, class, and box indices.
Rows of `indices` are ordered such that selected boxes from batch 0, class 0 come
first, in descending of scores, followed by boxes from batch 0, class 1 etc. Out of
`batch_size * num_class* num_boxes` rows of indices, only the first `num_total_detection`
rows are valid.
.. note::
**Important**: The output tensor has a fixed size based on `max_output_boxes_per_class`,
but only the first `num_total_detection` rows contain valid data. The remaining rows
may contain garbage values. When comparing with ONNX Runtime or other implementations
that output dynamic shapes, you should only compare the first
`num_total_detection` rows.
Example::
selected_indices, valid_count = nms_output
actual_count = int(valid_count.numpy()[0])
valid_indices = selected_indices.numpy()[:actual_count, :]
If `output_format` is "tensorflow", the output is three tensors, the first
is `indices` of size `(batch_size, num_class * num_boxes , 2)`, the second is `scores` of
size `(batch_size, num_class * num_boxes)`, and the third is `num_total_detection` of size
`(batch_size,)` representing the total number of selected boxes per batch. The two values
in `indices` encode class and box indices. Of num_class * num_boxes boxes in `indices` at
batch b, only the first `num_total_detection[b]` entries are valid. The second axis of
`indices` and `scores` are sorted within each class by box scores, but not across classes.
So the box indices and scores for the class 0 come first in a sorted order, followed by
the class 1 etc.
"""
batch, num_class, num_boxes = scores.shape
scores = reshape(scores, (batch * num_class, num_boxes))
sorted_indices = argsort(scores, axis=1, is_ascend=False, dtype="int32")
sorted_scores = gather(scores, 1, sorted_indices)
if not isinstance(score_threshold, te.Tensor):
score_threshold_tensor = te.compute((), lambda: score_threshold, name="score_threshold")
else:
score_threshold_tensor = score_threshold
valid_count = _get_valid_box_count(sorted_scores, score_threshold_tensor)
selected_indices, selected_scores, num_detections = run_all_class_nms(
boxes,
sorted_scores,
sorted_indices,
valid_count,
max_output_boxes_per_class,
iou_threshold,
_nms_loop,
return_scores=(output_format == "tensorflow"),
score_threshold=score_threshold_tensor, # Passed score_threshold as tensor
)
if output_format == "onnx":
row_offsets = cumsum(num_detections, exclusive=True, dtype="int64")
def _sum_clamped_total():
if isinstance(max_output_boxes_per_class, int):
k_expr = tvm.tirx.IntImm("int32", int(max_output_boxes_per_class))
clamped = te.compute(
num_detections.shape,
lambda i: tvm.tirx.min(num_detections[i], k_expr),
name="clamped_num",
)
return reduction.sum(cast(clamped, "int64"), axis=0)
if isinstance(max_output_boxes_per_class, tvm.tirx.IntImm):
k_expr = tvm.tirx.Cast("int32", max_output_boxes_per_class)
clamped = te.compute(
num_detections.shape,
lambda i: tvm.tirx.min(num_detections[i], k_expr),
name="clamped_num",
)
return reduction.sum(cast(clamped, "int64"), axis=0)
if isinstance(max_output_boxes_per_class, te.Tensor):
if len(max_output_boxes_per_class.shape) == 0:
kb = te.compute(
num_detections.shape,
lambda i: cast(max_output_boxes_per_class, "int32"),
name="k_broadcast",
)
elif (
len(max_output_boxes_per_class.shape) == 1
and max_output_boxes_per_class.shape[0] == 1
):
kb = te.compute(
num_detections.shape,
lambda i: cast(max_output_boxes_per_class[0], "int32"),
name="k_broadcast",
)
else:
return reduction.sum(cast(num_detections, "int64"), axis=0)
clamped = te.compute(
num_detections.shape,
lambda i: tvm.tirx.min(num_detections[i], kb[i]),
name="clamped_num",
)
return reduction.sum(cast(clamped, "int64"), axis=0)
return reduction.sum(cast(num_detections, "int64"), axis=0)
num_total_scalar = _sum_clamped_total()
num_total_detections = reshape(num_total_scalar, (1,))
if output_shape is not None:
selected_indices = collect_selected_indices(
num_class,
selected_indices,
num_detections,
row_offsets,
_collect_selected_indices_ir,
max_output_boxes_per_class=max_output_boxes_per_class,
output_shape=output_shape,
)
else:
# Use num_total_detections to enable dynamic trimming
# Pass image size for intelligent default estimation
input_image_size = None
if hasattr(scores, "shape") and len(scores.shape) >= 3:
# Extract image size from scores shape: (batch, num_classes, num_boxes)
# We can estimate image size from num_boxes (more boxes = larger image)
input_image_size = (scores.shape[2],) # Use num_boxes as proxy for image size
# TODO: Improve image size estimation by:
# 1. Accepting actual image dimensions as parameters
# 2. Using model metadata to infer typical image sizes
# 3. Learning from historical detection patterns
# 4. Providing user-configurable estimation strategies
selected_indices = collect_selected_indices(
num_class,
selected_indices,
num_detections,
row_offsets,
_collect_selected_indices_ir,
max_output_boxes_per_class=max_output_boxes_per_class,
num_total_detections=num_total_detections,
input_image_size=input_image_size,
)
return [selected_indices, num_total_detections]
num_detections_per_batch = reshape(num_detections, (batch, num_class))
row_offsets = cumsum(num_detections_per_batch, exclusive=True, dtype="int64", axis=1)
num_total_detections = reduction.sum(cast(num_detections_per_batch, "int64"), axis=1)
selected_indices, selected_scores = collect_selected_indices_and_scores(
selected_indices,
selected_scores,
num_detections_per_batch,
row_offsets,
num_total_detections,
_collect_selected_indices_and_scores_ir,
)
return [selected_indices, selected_scores, num_total_detections]