Files
apache--tvm/python/tvm/relax/transform/legalize_ops/vision.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

195 lines
6.6 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.
"""Default legalization function for vision network related operators."""
from tvm import relax, te, tirx, topi
from tvm.ir import Call
from ...block_builder import BlockBuilder
from ...expr import Expr, TupleGetItem
from .common import register_legalize
@register_legalize("relax.vision.all_class_non_max_suppression")
def _all_class_non_max_suppression(block_builder: BlockBuilder, call: Call) -> Expr:
"""Legalize all_class_non_max_suppression with dynamic output trimming.
This implementation uses dynamic_strided_slice to trim the NMS output to only
contain valid detections, improving memory efficiency and ONNX compatibility.
Returns
-------
result : Expr
The legalized NMS result.
- For ONNX output format, returns a tuple of
`(trimmed_indices, num_total_detections)`, where `trimmed_indices`
contains only valid detection indices.
- For TensorFlow output format, returns the TOPI result directly to
preserve the `(selected_indices, selected_scores, num_detections)`
layout expected by the Relax op.
"""
boxes = call.args[0]
scores = call.args[1]
max_output_boxes_per_class = call.args[2]
iou_threshold = call.args[3]
score_threshold = call.args[4]
output_format = call.attrs.output_format
scores_shape = scores.ty.shape
if len(scores_shape) == 3:
_, _, num_boxes = scores_shape
elif len(scores_shape) == 2:
_, num_boxes = scores_shape
else:
raise ValueError(f"Unexpected scores shape: {scores_shape}")
if isinstance(max_output_boxes_per_class, relax.Constant):
max_boxes_val = int(max_output_boxes_per_class.data.numpy())
else:
max_boxes_val = int(num_boxes)
# Get NMS result with fixed shape from TOPI
nms_result = block_builder.call_te(
topi.vision.all_class_non_max_suppression,
boxes,
scores,
max_boxes_val,
iou_threshold,
score_threshold,
output_format,
)
if output_format == "tensorflow":
return nms_result
selected_indices = block_builder.emit(TupleGetItem(nms_result, 0))
num_total_detections = block_builder.emit(TupleGetItem(nms_result, 1))
# Build slicing parameters using TE to avoid high-level Relax ops during legalization
def build_begin():
return te.compute((2,), lambda i: tirx.const(0, "int64"), name="begin")
def build_strides():
return te.compute((2,), lambda i: tirx.const(1, "int64"), name="strides")
def build_end(count_tensor):
# end = [count_tensor[0], 3]
def compute_end(i):
return tirx.if_then_else(
i == 0,
tirx.Cast("int64", count_tensor[0]),
tirx.const(3, "int64"),
)
return te.compute((2,), compute_end, name="end")
begin = block_builder.call_te(build_begin)
strides = block_builder.call_te(build_strides)
end = block_builder.call_te(build_end, num_total_detections)
# Apply dynamic strided slice to trim to valid detections only
trimmed_indices = block_builder.emit(
relax.op.dynamic_strided_slice(selected_indices, begin, end, strides)
)
# Return trimmed indices along with num_total_detections for compatibility
return relax.Tuple([trimmed_indices, num_total_detections])
@register_legalize("relax.vision.roi_align")
def _roi_align(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.vision.roi_align,
call.args[0],
call.args[1],
pooled_size=call.attrs.pooled_size,
spatial_scale=call.attrs.spatial_scale,
mode=call.attrs.mode,
sample_ratio=call.attrs.sample_ratio,
aligned=call.attrs.aligned,
layout=call.attrs.layout,
)
@register_legalize("relax.vision.get_valid_counts")
def _get_valid_counts(block_builder: BlockBuilder, call: Call) -> Expr:
return block_builder.call_te(
topi.vision.get_valid_counts,
call.args[0],
score_threshold=call.attrs.score_threshold,
id_index=call.attrs.id_index,
score_index=call.attrs.score_index,
)
@register_legalize("relax.vision.non_max_suppression")
def _non_max_suppression(block_builder: BlockBuilder, call: Call) -> Expr:
return block_builder.call_te(
topi.vision.non_max_suppression,
call.args[0],
call.args[1],
call.args[2],
max_output_size=call.attrs.max_output_size,
iou_threshold=call.attrs.iou_threshold,
force_suppress=call.attrs.force_suppress,
top_k=call.attrs.top_k,
coord_start=call.attrs.coord_start,
score_index=call.attrs.score_index,
id_index=call.attrs.id_index,
return_indices=call.attrs.return_indices,
invalid_to_bottom=call.attrs.invalid_to_bottom,
soft_nms_sigma=call.attrs.soft_nms_sigma,
score_threshold=call.attrs.score_threshold,
)
@register_legalize("relax.vision.roi_pool")
def _roi_pool(bb: BlockBuilder, call: Call) -> Expr:
return bb.call_te(
topi.vision.roi_pool,
call.args[0],
call.args[1],
pooled_size=call.attrs.pooled_size,
spatial_scale=call.attrs.spatial_scale,
layout=call.attrs.layout,
)
@register_legalize("relax.vision.multibox_transform_loc")
def _multibox_transform_loc(bb: BlockBuilder, call: Call) -> Expr:
variances = tuple(float(x) for x in call.attrs.variances)
def _te(cls_pred, loc_pred, anchor):
return topi.vision.multibox_transform_loc(
cls_pred,
loc_pred,
anchor,
variances,
clip=call.attrs.clip,
threshold=call.attrs.threshold,
keep_background=call.attrs.keep_background,
)
return bb.call_te(
_te,
call.args[0],
call.args[1],
call.args[2],
primfunc_name_hint="multibox_transform_loc",
)