From e488ac52d38a6dfffa28c8ae8cdad0a2068eeffe Mon Sep 17 00:00:00 2001 From: "yuan.xiong" Date: Thu, 2 Jul 2026 03:01:43 +0000 Subject: [PATCH 1/6] fix different number of detected objects for layer 'detections' (17 vs 16) Signed-off-by: yuan.xiong --- .../keep_nms_boundary_precision.cpp | 111 +++++++++++++++ .../keep_nms_boundary_precision.hpp | 21 +++ .../src/plugin/transformations_pipeline.cpp | 2 + .../keep_nms_boundary_precision_test.cpp | 127 ++++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp create mode 100644 src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.hpp create mode 100644 src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp diff --git a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp new file mode 100644 index 00000000000000..1936843ff768ae --- /dev/null +++ b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp @@ -0,0 +1,111 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "keep_nms_boundary_precision.hpp" + +#include +#include + +#include "openvino/op/add.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/non_max_suppression.hpp" +#include "openvino/op/reduce_max.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" + +namespace ov::intel_gpu { +namespace { + +constexpr char kTargetNmsPrefix[] = "torchvision::nms/"; + +bool has_target_nms_prefix(const std::shared_ptr& node) { + return node && node->get_friendly_name().rfind(kTargetNmsPrefix, 0) == 0; +} + +bool is_local_nms_helper(const std::shared_ptr& node) { + return ov::is_type(node) || ov::is_type(node) || + ov::is_type(node) || ov::is_type(node) || + ov::is_type(node) || ov::is_type(node); +} + +void mark_fp32_chain(const std::shared_ptr& node, + std::unordered_set& visited, + size_t remaining_depth) { + if (!node || !visited.insert(node.get()).second) { + return; + } + + ov::disable_conversion(node, ov::element::f16); + + if (remaining_depth == 0) { + return; + } + + for (const auto& input : node->inputs()) { + auto source = input.get_source_output().get_node_shared_ptr(); + if (!source) { + continue; + } + + if (ov::is_type(source)) { + ov::disable_conversion(source, ov::element::f16); + continue; + } + + if (has_target_nms_prefix(source) || is_local_nms_helper(source)) { + mark_fp32_chain(source, visited, remaining_depth - 1); + } + } +} + +} // namespace + +KeepNMSBoundaryPrecision::KeepNMSBoundaryPrecision() { + using namespace ov::pass::pattern; + + auto boxes_offset_add_m = wrap_type(); + auto boxes_reshape_m = wrap_type({boxes_offset_add_m, any_input()}); + auto scores_unsqueeze_m = wrap_type({any_input(), any_input()}); + auto nms_m = wrap_type({boxes_reshape_m, + scores_unsqueeze_m, + any_input(), + any_input(), + any_input()}); + + ov::matcher_pass_callback callback = [this, boxes_offset_add_m, boxes_reshape_m, scores_unsqueeze_m, nms_m](Matcher& m) { + const auto& pattern_map = m.get_pattern_value_map(); + + auto nms = ov::as_type_ptr(pattern_map.at(nms_m).get_node_shared_ptr()); + if (!nms || transformation_callback(nms) || !has_target_nms_prefix(nms)) { + return false; + } + + auto boxes_reshape = pattern_map.at(boxes_reshape_m).get_node_shared_ptr(); + auto boxes_offset_add = pattern_map.at(boxes_offset_add_m).get_node_shared_ptr(); + auto scores_unsqueeze = pattern_map.at(scores_unsqueeze_m).get_node_shared_ptr(); + + std::unordered_set visited; + mark_fp32_chain(nms, visited, 1); + mark_fp32_chain(boxes_reshape, visited, 5); + mark_fp32_chain(boxes_offset_add, visited, 5); + mark_fp32_chain(scores_unsqueeze, visited, 2); + + for (size_t index = 2; index < nms->get_input_size(); ++index) { + auto source = nms->input_value(index).get_node_shared_ptr(); + if (source) { + ov::disable_conversion(source, ov::element::f16); + } + } + + return true; + }; + + auto matcher = std::make_shared(nms_m, "KeepNMSBoundaryPrecision"); + register_matcher(matcher, callback); +} + +} // namespace ov::intel_gpu \ No newline at end of file diff --git a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.hpp b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.hpp new file mode 100644 index 00000000000000..b83d8593be029c --- /dev/null +++ b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.hpp @@ -0,0 +1,21 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/matcher_pass.hpp" + +namespace ov::intel_gpu { + +// Keeps the precision-sensitive NMS boundary pattern in fp32 before +// ConvertPrecision. The local box-offset construction and the NMS thresholds +// are boundary-sensitive; lowering them to fp16 can change suppression +// results. +class KeepNMSBoundaryPrecision : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("ov::intel_gpu::KeepNMSBoundaryPrecision"); + KeepNMSBoundaryPrecision(); +}; + +} // namespace ov::intel_gpu \ No newline at end of file diff --git a/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp b/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp index 46f6654e1a6013..0e68e43635a7b2 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp +++ b/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp @@ -99,6 +99,7 @@ #include "plugin/transformations/increase_position_ids_precision.hpp" #include "plugin/transformations/indirect_kv_cache.hpp" #include "plugin/transformations/keep_moe_3gemm_const_precision.hpp" +#include "plugin/transformations/keep_nms_boundary_precision.hpp" #include "plugin/transformations/keep_xattention_threshold_precision.hpp" #include "plugin/transformations/kv_cache_compression.hpp" #include "plugin/transformations/kv_cache_fusion.hpp" @@ -715,6 +716,7 @@ void TransformationsPipeline::apply(std::shared_ptr func) { manager.register_pass( ov::element::TypeVector{ov::element::i32, ov::element::u32, ov::element::u16}, add_precision_sensitive_convert); + manager.register_pass(); // Keep xattention threshold in fp32 to avoid boundary issues caused by fp16 quantization. manager.register_pass(); diff --git a/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp new file mode 100644 index 00000000000000..41a1abff8d4cee --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp @@ -0,0 +1,127 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "plugin/transformations/keep_nms_boundary_precision.hpp" + +#include + +#include "openvino/core/model.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/non_max_suppression.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/reduce_max.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/pass/manager.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" + +using namespace ov::intel_gpu; + +namespace { + +std::shared_ptr make_nms_model(const std::string& nms_prefix) { + auto boxes = std::make_shared(ov::element::f32, ov::PartialShape{-1, 4}); + boxes->set_friendly_name("boxes"); + auto scores = std::make_shared(ov::element::f32, ov::PartialShape{-1}); + scores->set_friendly_name("scores"); + + auto reduce_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {1}); + auto reduce_max = std::make_shared(boxes, reduce_axis, false); + reduce_max->set_friendly_name("aten::max/ReduceMax"); + + auto one = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{}, {1.0f}); + auto max_plus_one = std::make_shared(reduce_max, one); + max_plus_one->set_friendly_name("aten::add/Add_3"); + + auto offsets = std::make_shared(scores, max_plus_one); + offsets->set_friendly_name("aten::mul/Multiply_2"); + + auto unsqueeze_axis_1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + auto offsets_unsqueeze = std::make_shared(offsets, unsqueeze_axis_1); + offsets_unsqueeze->set_friendly_name("aten::unsqueeze/Unsqueeze_2"); + + auto boxes_for_nms = std::make_shared(boxes, offsets_unsqueeze); + boxes_for_nms->set_friendly_name("aten::add/Add_4"); + + auto reshape_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {1, -1, 4}); + auto boxes_reshape = std::make_shared(boxes_for_nms, reshape_shape, false); + boxes_reshape->set_friendly_name(nms_prefix + "Reshape"); + + auto score_unsqueeze_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{2}, {0, 1}); + auto scores_unsqueeze = std::make_shared(scores, score_unsqueeze_axis); + scores_unsqueeze->set_friendly_name(nms_prefix + "Unsqueeze"); + + auto max_output_boxes = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {2000}); + max_output_boxes->set_friendly_name(nms_prefix + "Constant_max_output_boxes"); + auto iou_threshold = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{}, {0.5f}); + iou_threshold->set_friendly_name(nms_prefix + "Constant_iou"); + auto score_threshold_compressed = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{}, {0.7f}); + score_threshold_compressed->set_friendly_name("2636_1_compressed"); + auto score_threshold = std::make_shared(score_threshold_compressed, ov::element::f32); + score_threshold->set_friendly_name("2636_1"); + + auto nms = std::make_shared(boxes_reshape, + scores_unsqueeze, + max_output_boxes, + iou_threshold, + score_threshold, + ov::op::v9::NonMaxSuppression::BoxEncodingType::CORNER, + true, + ov::element::i64); + nms->set_friendly_name(nms_prefix + "NonMaxSuppression"); + + auto result = std::make_shared(nms->output(0)); + return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{boxes, scores}); +} + +std::shared_ptr find_node_by_name(const std::shared_ptr& model, const std::string& name) { + for (const auto& node : model->get_ordered_ops()) { + if (node->get_friendly_name() == name) { + return node; + } + } + return nullptr; +} + +} // namespace + +TEST(KeepNMSBoundaryPrecisionTest, MarksTargetNmsSubgraph) { + auto model = make_nms_model("torchvision::nms/"); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + for (const auto& name : {std::string{"torchvision::nms/NonMaxSuppression"}, + std::string{"torchvision::nms/Reshape"}, + std::string{"torchvision::nms/Unsqueeze"}, + std::string{"aten::add/Add_4"}, + std::string{"aten::unsqueeze/Unsqueeze_2"}, + std::string{"aten::mul/Multiply_2"}, + std::string{"aten::add/Add_3"}, + std::string{"aten::max/ReduceMax"}, + std::string{"torchvision::nms/Constant_max_output_boxes"}, + std::string{"torchvision::nms/Constant_iou"}, + std::string{"2636_1"}}) { + auto node = find_node_by_name(model, name); + ASSERT_NE(node, nullptr) << name; + EXPECT_TRUE(ov::is_conversion_disabled(node, ov::element::f16)) << name; + } +} + +TEST(KeepNMSBoundaryPrecisionTest, IgnoresOtherNmsNames) { + auto model = make_nms_model("custom::nms/"); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + auto node = find_node_by_name(model, "custom::nms/NonMaxSuppression"); + ASSERT_NE(node, nullptr); + EXPECT_FALSE(ov::is_conversion_disabled(node, ov::element::f16)); +} \ No newline at end of file From 89ef2bf44e37cc9aa0bbbf6e638d1f09e190e498 Mon Sep 17 00:00:00 2001 From: Xiong Yuan Date: Wed, 8 Jul 2026 14:41:58 +0800 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/plugin/transformations/keep_nms_boundary_precision.cpp | 1 + .../unit/transformations/keep_nms_boundary_precision_test.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp index 1936843ff768ae..974f24009f9751 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp +++ b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp @@ -8,6 +8,7 @@ #include #include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" #include "openvino/op/convert.hpp" #include "openvino/op/multiply.hpp" #include "openvino/op/non_max_suppression.hpp" diff --git a/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp index 41a1abff8d4cee..dd02563a0f58cf 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp @@ -20,7 +20,7 @@ #include "openvino/pass/manager.hpp" #include "transformations/rt_info/disable_precision_conversion.hpp" -using namespace ov::intel_gpu; +using ov::intel_gpu::KeepNMSBoundaryPrecision; namespace { From d6bf4d3aaa4513e39ad1236ea1442b9b85ef5b31 Mon Sep 17 00:00:00 2001 From: "yuan.xiong" Date: Wed, 8 Jul 2026 09:20:15 +0000 Subject: [PATCH 3/6] Update nms match chain to keep FP32 precision Signed-off-by: yuan.xiong --- .../keep_nms_boundary_precision.cpp | 169 +++++++++++++++++- .../keep_nms_boundary_precision_test.cpp | 118 ++++++++---- 2 files changed, 245 insertions(+), 42 deletions(-) diff --git a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp index 974f24009f9751..89fb70e59cf304 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp +++ b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp @@ -4,6 +4,7 @@ #include "keep_nms_boundary_precision.hpp" +#include #include #include @@ -21,10 +22,156 @@ namespace ov::intel_gpu { namespace { -constexpr char kTargetNmsPrefix[] = "torchvision::nms/"; +bool is_const_one_like(const std::shared_ptr& node) { + if (!node) { + return false; + } + + if (const auto constant = ov::as_type_ptr(node)) { + return constant->cast_vector() == std::vector{1.0f}; + } + + const auto convert = ov::as_type_ptr(node); + if (!convert) { + return false; + } + + const auto constant = ov::as_type_ptr(convert->input_value(0).get_node_shared_ptr()); + return constant && constant->cast_vector() == std::vector{1.0f}; +} + +// The lowered pattern may materialize `1` either as a plain Constant or as a +// Constant followed by Convert (for example when a compressed constant is +// decompressed back to fp32 before it is added to ReduceMax(boxes)). +bool is_integral_to_fp_convert(const std::shared_ptr& node) { + const auto convert = ov::as_type_ptr(node); + if (!convert) { + return false; + } -bool has_target_nms_prefix(const std::shared_ptr& node) { - return node && node->get_friendly_name().rfind(kTargetNmsPrefix, 0) == 0; + const auto input_type = convert->input_value(0).get_element_type(); + const auto output_type = convert->get_output_element_type(0); + return input_type.is_integral_number() && output_type.is_real(); +} + +template +std::shared_ptr get_node_if(const ov::Output& output) { + return ov::as_type_ptr(output.get_node_shared_ptr()); +} + +// Match the lowered batched-NMS boxes path: +// boxes_for_nms = Add(boxes, Unsqueeze(offsets)) +// The surrounding callback validates that `offsets` is the class-dependent +// coordinate shift used to collapse per-class NMS into a single NMS call. +bool match_batched_nms_offsets(const std::shared_ptr& boxes_offset_add, + std::shared_ptr& boxes_source, + std::shared_ptr& offsets_unsqueeze) { + const auto add = ov::as_type_ptr(boxes_offset_add); + if (!add) { + return false; + } + + const auto first = add->input_value(0).get_node_shared_ptr(); + const auto second = add->input_value(1).get_node_shared_ptr(); + + if (ov::is_type(first)) { + offsets_unsqueeze = first; + boxes_source = second; + return true; + } + + if (ov::is_type(second)) { + offsets_unsqueeze = second; + boxes_source = first; + return true; + } + + return false; +} + +// Validate the full class-aware offsets trick typically lowered from +// batched/multi-class NMS frontends: +// offsets = Convert(class_ids) * (ReduceMax(boxes) + 1) +// boxes_for_nms = boxes + Unsqueeze(offsets) +// +// Why this matters: +// - `ReduceMax(boxes) + 1` produces a stride larger than any coordinate in the +// current boxes tensor. +// - multiplying that stride by `class_ids` moves boxes from different classes +// into disjoint coordinate ranges. +// - a single NMS call can then behave like per-class NMS, because boxes from +// different classes no longer overlap after the shift. +// +// Example: +// - assume two boxes overlap in the original image: +// class 0: [10, 10, 20, 20] +// class 1: [11, 11, 21, 21] +// - if the maximum coordinate in the tensor is 100, then the stride is 101 +// - the class 0 box keeps offset 0 * 101 = 0 and stays [10, 10, 20, 20] +// - the class 1 box gets offset 1 * 101 = 101 and becomes +// [112, 112, 122, 122] +// The boxes overlapped before the shift, but no longer overlap after it, so a +// single NMS call behaves like class-wise NMS. +// Matching this structure is more stable than relying on a frontend-specific +// friendly name such as `torchvision::nms/...`. +bool matches_batched_nms_chain(const std::shared_ptr& boxes_offset_add) { + std::shared_ptr boxes_source; + std::shared_ptr offsets_unsqueeze; + if (!match_batched_nms_offsets(boxes_offset_add, boxes_source, offsets_unsqueeze)) { + return false; + } + + const auto unsqueeze = ov::as_type_ptr(offsets_unsqueeze); + const auto multiply = unsqueeze ? get_node_if(unsqueeze->input_value(0)) : nullptr; + if (!multiply) { + return false; + } + + std::shared_ptr class_ids_convert; + std::shared_ptr max_plus_one; + for (size_t index = 0; index < 2; ++index) { + auto lhs = multiply->input_value(index).get_node_shared_ptr(); + auto rhs = multiply->input_value(1 - index).get_node_shared_ptr(); + // The offsets trick multiplies two specific ingredients: + // 1) integer class ids converted to floating point + // 2) a scalar stride computed as ReduceMax(boxes) + 1 + if (is_integral_to_fp_convert(lhs) && ov::is_type(rhs)) { + class_ids_convert = lhs; + max_plus_one = rhs; + break; + } + } + + if (!class_ids_convert || !max_plus_one) { + return false; + } + + const auto add = ov::as_type_ptr(max_plus_one); + if (!add) { + return false; + } + + std::shared_ptr reduce_max; + for (size_t index = 0; index < 2; ++index) { + auto lhs = add->input_value(index).get_node_shared_ptr(); + auto rhs = add->input_value(1 - index).get_node_shared_ptr(); + // `ReduceMax(boxes) + 1` is the key part of the coordinate stride: the + // `+1` guarantees that boxes shifted by neighboring class ids land in + // non-overlapping coordinate ranges. + if (ov::is_type(lhs) && is_const_one_like(rhs)) { + reduce_max = lhs; + break; + } + } + + if (!reduce_max) { + return false; + } + + const auto reduce_max_boxes = reduce_max->input_value(0).get_node_shared_ptr(); + // Tie the stride back to the same `boxes` tensor that is later shifted. + // This avoids matching an unrelated ReduceMax/Add chain in the neighborhood. + return reduce_max_boxes == boxes_source; } bool is_local_nms_helper(const std::shared_ptr& node) { @@ -57,7 +204,7 @@ void mark_fp32_chain(const std::shared_ptr& node, continue; } - if (has_target_nms_prefix(source) || is_local_nms_helper(source)) { + if (is_local_nms_helper(source)) { mark_fp32_chain(source, visited, remaining_depth - 1); } } @@ -68,6 +215,9 @@ void mark_fp32_chain(const std::shared_ptr& node, KeepNMSBoundaryPrecision::KeepNMSBoundaryPrecision() { using namespace ov::pass::pattern; + // Coarse candidate: a boxes path feeding NMS through Add -> Reshape and a + // scores path feeding NMS through Unsqueeze. The callback below performs + // the stricter batched-NMS offsets validation before marking anything. auto boxes_offset_add_m = wrap_type(); auto boxes_reshape_m = wrap_type({boxes_offset_add_m, any_input()}); auto scores_unsqueeze_m = wrap_type({any_input(), any_input()}); @@ -81,7 +231,7 @@ KeepNMSBoundaryPrecision::KeepNMSBoundaryPrecision() { const auto& pattern_map = m.get_pattern_value_map(); auto nms = ov::as_type_ptr(pattern_map.at(nms_m).get_node_shared_ptr()); - if (!nms || transformation_callback(nms) || !has_target_nms_prefix(nms)) { + if (!nms || transformation_callback(nms)) { return false; } @@ -89,7 +239,16 @@ KeepNMSBoundaryPrecision::KeepNMSBoundaryPrecision() { auto boxes_offset_add = pattern_map.at(boxes_offset_add_m).get_node_shared_ptr(); auto scores_unsqueeze = pattern_map.at(scores_unsqueeze_m).get_node_shared_ptr(); + // Reject generic NMS tails. We only want the precision-sensitive + // lowered batched-NMS/class-aware offsets variant. + if (!matches_batched_nms_chain(boxes_offset_add)) { + return false; + } + std::unordered_set visited; + // Keep the local NMS tail in fp32 once the structure is confirmed. + // We start from the matched NMS inputs and walk only through the small + // helper subgraph around the offsets computation. mark_fp32_chain(nms, visited, 1); mark_fp32_chain(boxes_reshape, visited, 5); mark_fp32_chain(boxes_offset_add, visited, 5); diff --git a/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp index dd02563a0f58cf..b212d91f869ebb 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp @@ -20,25 +20,54 @@ #include "openvino/pass/manager.hpp" #include "transformations/rt_info/disable_precision_conversion.hpp" -using ov::intel_gpu::KeepNMSBoundaryPrecision; +using namespace ov::intel_gpu; namespace { -std::shared_ptr make_nms_model(const std::string& nms_prefix) { +struct NmsTestModel { + std::shared_ptr model; + std::shared_ptr boxes_offset_add; + std::shared_ptr offsets_unsqueeze; + std::shared_ptr offsets_multiply; + std::shared_ptr class_ids_convert; + std::shared_ptr max_plus_one; + std::shared_ptr reduce_max; + std::shared_ptr max_output_boxes; + std::shared_ptr iou_threshold; + std::shared_ptr score_threshold; + std::shared_ptr boxes_reshape; + std::shared_ptr scores_unsqueeze; + std::shared_ptr nms; +}; + +NmsTestModel make_nms_model(const std::string& nms_prefix, bool use_batched_nms_offsets = true) { auto boxes = std::make_shared(ov::element::f32, ov::PartialShape{-1, 4}); boxes->set_friendly_name("boxes"); auto scores = std::make_shared(ov::element::f32, ov::PartialShape{-1}); scores->set_friendly_name("scores"); + auto class_ids = std::make_shared(ov::element::i64, ov::PartialShape{-1}); + class_ids->set_friendly_name("class_ids"); - auto reduce_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {1}); + auto reduce_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{2}, {0, 1}); auto reduce_max = std::make_shared(boxes, reduce_axis, false); reduce_max->set_friendly_name("aten::max/ReduceMax"); - auto one = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{}, {1.0f}); + auto one_compressed = ov::op::v0::Constant::create(ov::element::f16, ov::Shape{}, {1.0f}); + one_compressed->set_friendly_name("aten::add/Multiply_3_compressed"); + auto one = std::make_shared(one_compressed, ov::element::f32); + one->set_friendly_name("aten::add/Multiply_3"); auto max_plus_one = std::make_shared(reduce_max, one); max_plus_one->set_friendly_name("aten::add/Add_3"); - auto offsets = std::make_shared(scores, max_plus_one); + std::shared_ptr offsets_input = scores; + std::shared_ptr class_ids_convert; + if (use_batched_nms_offsets) { + class_ids_convert = std::make_shared(class_ids, ov::element::f32); + class_ids_convert->set_friendly_name("aten::to/ConvertLike_2"); + offsets_input = class_ids_convert; + } + + auto offsets = std::make_shared(offsets_input, max_plus_one); offsets->set_friendly_name("aten::mul/Multiply_2"); auto unsqueeze_axis_1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); @@ -76,52 +105,67 @@ std::shared_ptr make_nms_model(const std::string& nms_prefix) { nms->set_friendly_name(nms_prefix + "NonMaxSuppression"); auto result = std::make_shared(nms->output(0)); - return std::make_shared(ov::ResultVector{result}, ov::ParameterVector{boxes, scores}); -} -std::shared_ptr find_node_by_name(const std::shared_ptr& model, const std::string& name) { - for (const auto& node : model->get_ordered_ops()) { - if (node->get_friendly_name() == name) { - return node; - } - } - return nullptr; + return NmsTestModel{std::make_shared(ov::ResultVector{result}, ov::ParameterVector{boxes, scores, class_ids}), + boxes_for_nms, + offsets_unsqueeze, + offsets, + class_ids_convert, + max_plus_one, + reduce_max, + max_output_boxes, + iou_threshold, + score_threshold, + boxes_reshape, + scores_unsqueeze, + nms}; } } // namespace TEST(KeepNMSBoundaryPrecisionTest, MarksTargetNmsSubgraph) { - auto model = make_nms_model("torchvision::nms/"); + auto test_model = make_nms_model("torchvision::nms/"); ov::pass::Manager manager; manager.register_pass(); - manager.run_passes(model); - - for (const auto& name : {std::string{"torchvision::nms/NonMaxSuppression"}, - std::string{"torchvision::nms/Reshape"}, - std::string{"torchvision::nms/Unsqueeze"}, - std::string{"aten::add/Add_4"}, - std::string{"aten::unsqueeze/Unsqueeze_2"}, - std::string{"aten::mul/Multiply_2"}, - std::string{"aten::add/Add_3"}, - std::string{"aten::max/ReduceMax"}, - std::string{"torchvision::nms/Constant_max_output_boxes"}, - std::string{"torchvision::nms/Constant_iou"}, - std::string{"2636_1"}}) { - auto node = find_node_by_name(model, name); - ASSERT_NE(node, nullptr) << name; - EXPECT_TRUE(ov::is_conversion_disabled(node, ov::element::f16)) << name; + manager.run_passes(test_model.model); + + for (const auto& node : {test_model.nms, + test_model.boxes_reshape, + test_model.scores_unsqueeze, + test_model.boxes_offset_add, + test_model.offsets_unsqueeze, + test_model.offsets_multiply, + test_model.class_ids_convert, + test_model.max_plus_one, + test_model.reduce_max, + test_model.max_output_boxes, + test_model.iou_threshold, + test_model.score_threshold}) { + ASSERT_NE(node, nullptr); + EXPECT_TRUE(ov::is_conversion_disabled(node, ov::element::f16)); } } -TEST(KeepNMSBoundaryPrecisionTest, IgnoresOtherNmsNames) { - auto model = make_nms_model("custom::nms/"); +TEST(KeepNMSBoundaryPrecisionTest, MarksStructuralPatternWithoutTorchvisionNames) { + auto test_model = make_nms_model("custom::nms/"); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(test_model.model); + + ASSERT_NE(test_model.nms, nullptr); + EXPECT_TRUE(ov::is_conversion_disabled(test_model.nms, ov::element::f16)); +} + +TEST(KeepNMSBoundaryPrecisionTest, IgnoresNmsWithoutBatchedOffsetChain) { + auto test_model = make_nms_model("custom::nms/", false); ov::pass::Manager manager; manager.register_pass(); - manager.run_passes(model); + manager.run_passes(test_model.model); - auto node = find_node_by_name(model, "custom::nms/NonMaxSuppression"); - ASSERT_NE(node, nullptr); - EXPECT_FALSE(ov::is_conversion_disabled(node, ov::element::f16)); + ASSERT_NE(test_model.nms, nullptr); + EXPECT_FALSE(ov::is_conversion_disabled(test_model.nms, ov::element::f16)); + EXPECT_EQ(test_model.class_ids_convert, nullptr); } \ No newline at end of file From e692b41e77d56c51d377fcc67794624abd38c10a Mon Sep 17 00:00:00 2001 From: "yuan.xiong" Date: Mon, 13 Jul 2026 08:55:47 +0000 Subject: [PATCH 4/6] convert batched nms to multiclass nms Signed-off-by: yuan.xiong --- .../common_optimizations/nop_elimination.cpp | 9 +- .../simplify_shape_of_sub_graph.cpp | 8 + .../common_optimizations/nop_elimination.cpp | 15 + .../multiclass_nms_shape_inference.hpp | 11 +- src/core/tests/type_prop/multiclass_nms.cpp | 13 + .../convert_batched_nms_to_multiclass_nms.cpp | 450 ++++++++++++++++++ .../convert_batched_nms_to_multiclass_nms.hpp | 29 ++ .../src/plugin/transformations_pipeline.cpp | 24 +- ...ert_batched_nms_to_multiclass_nms_test.cpp | 152 ++++++ 9 files changed, 706 insertions(+), 5 deletions(-) create mode 100644 src/plugins/intel_gpu/src/plugin/transformations/convert_batched_nms_to_multiclass_nms.cpp create mode 100644 src/plugins/intel_gpu/src/plugin/transformations/convert_batched_nms_to_multiclass_nms.hpp create mode 100644 src/plugins/intel_gpu/tests/unit/transformations/convert_batched_nms_to_multiclass_nms_test.cpp diff --git a/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp b/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp index a563fa291083b9..09031d58ab9496 100644 --- a/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp @@ -71,7 +71,14 @@ static bool simplify_gather(std::shared_ptr node) { auto indices = gather->input_value(1); // we need to know data and indices shape to infer if gather is Nop - if (data.get_partial_shape().is_dynamic() || indices.get_partial_shape().is_dynamic()) { + const auto has_only_static_dimensions = [](const PartialShape& shape) { + return shape.rank().is_static() && + std::all_of(shape.begin(), shape.end(), [](const Dimension& dimension) { + return dimension.is_static(); + }); + }; + if (!has_only_static_dimensions(data.get_partial_shape()) || + !has_only_static_dimensions(indices.get_partial_shape())) { return false; } diff --git a/src/common/transformations/src/transformations/common_optimizations/simplify_shape_of_sub_graph.cpp b/src/common/transformations/src/transformations/common_optimizations/simplify_shape_of_sub_graph.cpp index 5762fb500b6658..57547461a04c34 100644 --- a/src/common/transformations/src/transformations/common_optimizations/simplify_shape_of_sub_graph.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/simplify_shape_of_sub_graph.cpp @@ -136,6 +136,14 @@ pass::GatherNopElimination::GatherNopElimination() { matcher_pass_callback callback = [](Matcher& m) { auto gather = m.get_match_root(); + const auto& data_shape = gather->get_input_partial_shape(0); + if (data_shape.rank().is_dynamic() || + std::any_of(data_shape.begin(), data_shape.end(), [](const Dimension& dimension) { + return dimension.is_dynamic(); + })) { + return false; + } + const auto& number_of_indices = shape_size(gather->get_input_shape(1)); if (gather->get_input_shape(0) != gather->get_output_shape(0) || shape_size(gather->get_input_shape(2)) != 1 || number_of_indices > 10) diff --git a/src/common/transformations/tests/common_optimizations/nop_elimination.cpp b/src/common/transformations/tests/common_optimizations/nop_elimination.cpp index e299438e304125..3161cd320bc21e 100644 --- a/src/common/transformations/tests/common_optimizations/nop_elimination.cpp +++ b/src/common/transformations/tests/common_optimizations/nop_elimination.cpp @@ -1447,6 +1447,21 @@ TEST(nop_elimination, gather_to_squeeze) { run_and_check(func_axis_3); } +TEST(nop_elimination, keep_gather_with_dynamic_dimension) { + auto arg = std::make_shared(element::f32, PartialShape{Dimension::dynamic(), 1}); + auto indices = op::v0::Constant::create(element::i64, Shape{}, vector{0}); + auto axis = op::v0::Constant::create(element::i64, Shape{}, vector{1}); + auto gather = std::make_shared(arg, indices, axis); + auto model = std::make_shared(OutputVector{gather}, ParameterVector{arg}); + + pass::Manager pass_manager; + pass_manager.register_pass(); + pass_manager.run_passes(model); + + EXPECT_EQ(count_ops_of_type(model), 1); + EXPECT_EQ(count_ops_of_type(model), 0); +} + TEST(nop_elimination, not_gather_to_squeeze_with_vector_indices) { auto generate_func = [](int64_t gather_axis) { ov::Shape shape{3, 3, 4, 4}; diff --git a/src/core/shape_inference/include/multiclass_nms_shape_inference.hpp b/src/core/shape_inference/include/multiclass_nms_shape_inference.hpp index 59a6033132b761..6c6e494a4bc1ff 100644 --- a/src/core/shape_inference/include/multiclass_nms_shape_inference.hpp +++ b/src/core/shape_inference/include/multiclass_nms_shape_inference.hpp @@ -93,8 +93,12 @@ std::vector shape_infer(const util::MulticlassNmsBase* op, const auto& num_images = has_rois_num ? rois_num_shape[0] : scores_shape[0]; auto& selected_boxes = output_shapes[0][0]; - selected_boxes = - (nms_top_k > -1) ? TDim(std::min(boxes_shape[1].get_max_length(), nms_top_k)) : boxes_shape[1]; + if (nms_top_k > -1) { + const auto boxes_upper_bound = boxes_shape[1].get_max_length(); + selected_boxes = TDim(boxes_upper_bound < 0 ? nms_top_k : std::min(boxes_upper_bound, nms_top_k)); + } else { + selected_boxes = boxes_shape[1]; + } if (ignore_bg_class && (background_class > -1) && (background_class < num_classes.get_max_length())) { selected_boxes *= std::max(1, num_classes.get_max_length() - 1); @@ -102,7 +106,8 @@ std::vector shape_infer(const util::MulticlassNmsBase* op, selected_boxes *= num_classes; } - if (keep_top_k > -1 && (keep_top_k < selected_boxes.get_max_length())) { + if (keep_top_k > -1 && + (selected_boxes.get_max_length() < 0 || keep_top_k < selected_boxes.get_max_length())) { selected_boxes = TDim(keep_top_k); } diff --git a/src/core/tests/type_prop/multiclass_nms.cpp b/src/core/tests/type_prop/multiclass_nms.cpp index 075a73061d5c3d..b0dd7980afd526 100644 --- a/src/core/tests/type_prop/multiclass_nms.cpp +++ b/src/core/tests/type_prop/multiclass_nms.cpp @@ -275,6 +275,19 @@ TYPED_TEST(type_prop, multiclass_nms_output_shape_1dim_keep_topk) { EXPECT_EQ(nms->get_output_shape(2), (Shape{2})); } +TYPED_TEST(type_prop, multiclass_nms_dynamic_boxes_with_finite_topk) { + const auto boxes = make_shared(element::f32, PartialShape{1, Dimension::dynamic(), 4}); + const auto scores = make_shared(element::f32, PartialShape{1, 80, Dimension::dynamic()}); + this->attrs.nms_top_k = -1; + this->attrs.keep_top_k = 100; + + const auto nms = make_shared(boxes, scores, this->attrs); + + EXPECT_EQ(nms->get_output_partial_shape(0), (PartialShape{{0, 100}, 6})); + EXPECT_EQ(nms->get_output_partial_shape(1), (PartialShape{{0, 100}, 1})); + EXPECT_EQ(nms->get_output_partial_shape(2), (PartialShape{1})); +} + TYPED_TEST(type_prop, multiclass_nms_input_f16) { const auto boxes = make_shared(element::f16, Shape{2, 7, 4}); const auto scores = make_shared(element::f16, Shape{2, 5, 7}); diff --git a/src/plugins/intel_gpu/src/plugin/transformations/convert_batched_nms_to_multiclass_nms.cpp b/src/plugins/intel_gpu/src/plugin/transformations/convert_batched_nms_to_multiclass_nms.cpp new file mode 100644 index 00000000000000..d8a877db5e5425 --- /dev/null +++ b/src/plugins/intel_gpu/src/plugin/transformations/convert_batched_nms_to_multiclass_nms.cpp @@ -0,0 +1,450 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "convert_batched_nms_to_multiclass_nms.hpp" + +#include +#include +#include +#include + +#include "openvino/core/graph_util.hpp" +#include "openvino/core/rt_info.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/multiclass_nms.hpp" +#include "openvino/op/non_zero.hpp" +#include "openvino/op/non_max_suppression.hpp" +#include "openvino/op/one_hot.hpp" +#include "openvino/op/reduce_max.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/select.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/transpose.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/pass/pattern/op/wrap_type.hpp" +#include "openvino/op/util/multi_subgraph_base.hpp" +#include "ov_ops/multiclass_nms_ie_internal.hpp" +#include "transformations/rt_info/disable_precision_conversion.hpp" + +namespace ov::intel_gpu { +namespace { + +constexpr const char* static_class_count_key = "intel_gpu_batched_nms_static_class_count"; +constexpr const char* prefix_limit_key = "intel_gpu_batched_nms_prefix_limit"; + +bool is_const_one_like(const std::shared_ptr& node) { + if (!node) { + return false; + } + + if (const auto constant = ov::as_type_ptr(node)) { + return constant->cast_vector() == std::vector{1.0f}; + } + + const auto convert = ov::as_type_ptr(node); + if (!convert) { + return false; + } + + const auto constant = ov::as_type_ptr(convert->input_value(0).get_node_shared_ptr()); + return constant && constant->cast_vector() == std::vector{1.0f}; +} + +bool is_integral_to_fp_convert(const std::shared_ptr& node) { + const auto convert = ov::as_type_ptr(node); + if (!convert) { + return false; + } + + const auto input_type = convert->input_value(0).get_element_type(); + const auto output_type = convert->get_output_element_type(0); + return input_type.is_integral_number() && output_type.is_real(); +} + +template +std::shared_ptr get_node_if(const ov::Output& output) { + return ov::as_type_ptr(output.get_node_shared_ptr()); +} + +bool match_batched_nms_offsets(const std::shared_ptr& boxes_offset_add, + std::shared_ptr& boxes_source, + std::shared_ptr& offsets_unsqueeze) { + const auto add = ov::as_type_ptr(boxes_offset_add); + if (!add) { + return false; + } + + const auto first = add->input_value(0).get_node_shared_ptr(); + const auto second = add->input_value(1).get_node_shared_ptr(); + + if (ov::is_type(first)) { + offsets_unsqueeze = first; + boxes_source = second; + return true; + } + + if (ov::is_type(second)) { + offsets_unsqueeze = second; + boxes_source = first; + return true; + } + + return false; +} + +bool matches_batched_nms_chain(const std::shared_ptr& boxes_offset_add, + ov::Output& boxes_source, + ov::Output& class_ids_source) { + std::shared_ptr boxes_source_node; + std::shared_ptr offsets_unsqueeze; + if (!match_batched_nms_offsets(boxes_offset_add, boxes_source_node, offsets_unsqueeze)) { + return false; + } + + const auto unsqueeze = ov::as_type_ptr(offsets_unsqueeze); + const auto multiply = unsqueeze ? get_node_if(unsqueeze->input_value(0)) : nullptr; + if (!multiply) { + return false; + } + + std::shared_ptr class_ids_convert; + std::shared_ptr max_plus_one; + for (size_t index = 0; index < 2; ++index) { + auto lhs = multiply->input_value(index).get_node_shared_ptr(); + auto rhs = multiply->input_value(1 - index).get_node_shared_ptr(); + if (is_integral_to_fp_convert(lhs) && ov::is_type(rhs)) { + class_ids_convert = lhs; + max_plus_one = rhs; + break; + } + } + + if (!class_ids_convert || !max_plus_one) { + return false; + } + + const auto add = ov::as_type_ptr(max_plus_one); + if (!add) { + return false; + } + + std::shared_ptr reduce_max; + for (size_t index = 0; index < 2; ++index) { + auto lhs = add->input_value(index).get_node_shared_ptr(); + auto rhs = add->input_value(1 - index).get_node_shared_ptr(); + if (ov::is_type(lhs) && is_const_one_like(rhs)) { + reduce_max = lhs; + break; + } + } + + if (!reduce_max) { + return false; + } + + if (reduce_max->input_value(0).get_node_shared_ptr() != boxes_source_node) { + return false; + } + + boxes_source = boxes_source_node; + class_ids_source = class_ids_convert->input_value(0); + return true; +} + +template +bool get_scalar_from_const_source(const ov::Output& output, T& value) { + auto node = output.get_node_shared_ptr(); + if (const auto convert = ov::as_type_ptr(node)) { + return get_scalar_from_const_source(convert->input_value(0), value); + } + + const auto constant = ov::as_type_ptr(node); + if (!constant) { + return false; + } + + const auto values = constant->cast_vector(); + if (values.size() != 1) { + return false; + } + + value = values[0]; + return true; +} + +bool is_scalar_constant_value(const ov::Output& output, int64_t expected) { + int64_t value = 0; + return get_scalar_from_const_source(output, value) && value == expected; +} + +bool infer_class_count_from_nonzero_indices(const ov::Output& output, int64_t& class_count) { + const auto gather = get_node_if(output); + if (!gather || !is_scalar_constant_value(gather->input_value(1), 1) || + !is_scalar_constant_value(gather->input_value(2), 1)) { + return false; + } + + const auto transpose = get_node_if(gather->input_value(0)); + const auto non_zero = transpose ? get_node_if(transpose->input_value(0)) : nullptr; + if (!non_zero) { + return false; + } + + const auto input_shape = non_zero->get_input_partial_shape(0); + if (input_shape.rank().is_dynamic() || input_shape.rank().get_length() != 2 || input_shape[1].is_dynamic()) { + return false; + } + + class_count = input_shape[1].get_length(); + return class_count > 0; +} + +bool infer_prefix_limit(const ov::Output& output, int64_t& prefix_limit) { + prefix_limit = 0; + const auto& consumers = output.get_target_inputs(); + if (consumers.empty()) { + return false; + } + + for (const auto& consumer : consumers) { + const auto slice = ov::as_type_ptr(consumer.get_node()->shared_from_this()); + int64_t start = 0; + int64_t stop = 0; + int64_t step = 0; + int64_t axis = 0; + if (!slice || !get_scalar_from_const_source(slice->input_value(1), start) || start != 0 || + !get_scalar_from_const_source(slice->input_value(2), stop) || stop <= 0 || + !get_scalar_from_const_source(slice->input_value(3), step) || step != 1 || + !get_scalar_from_const_source(slice->input_value(4), axis) || axis != 0) { + return false; + } + prefix_limit = std::max(prefix_limit, stop); + } + return true; +} + +} // namespace + +bool MarkBatchedNmsStaticClassCount::run_on_model(const std::shared_ptr& model) { + bool marked = false; + for (const auto& node : model->get_ordered_ops()) { + const auto subgraph = ov::as_type_ptr(node); + if (!subgraph) { + continue; + } + + const auto& bodies = subgraph->get_functions(); + for (size_t body_index = 0; body_index < bodies.size(); ++body_index) { + const auto& parameters = bodies[body_index]->get_parameters(); + for (const auto& input_desc : subgraph->get_input_descriptions(body_index)) { + int64_t class_count = 0; + const auto source = subgraph->input(input_desc->m_input_index).get_source_output(); + if (infer_class_count_from_nonzero_indices(source, class_count)) { + parameters[input_desc->m_body_parameter_index]->get_rt_info()[static_class_count_key] = class_count; + marked = true; + } + } + + for (const auto& output_desc : subgraph->get_output_descriptions(body_index)) { + int64_t prefix_limit = 0; + if (infer_prefix_limit(subgraph->output(output_desc->m_output_index), prefix_limit)) { + const auto& result = bodies[body_index]->get_results()[output_desc->m_body_value_index]; + result->input_value(0).get_node_shared_ptr()->get_rt_info()[prefix_limit_key] = prefix_limit; + marked = true; + } + } + marked |= run_on_model(bodies[body_index]); + } + } + return marked; +} + +ConvertBatchedNmsToMulticlassNms::ConvertBatchedNmsToMulticlassNms() { + using namespace ov::pass::pattern; + + auto boxes_offset_add_m = wrap_type(); + auto boxes_reshape_m = wrap_type({boxes_offset_add_m, any_input()}); + auto scores_unsqueeze_m = wrap_type({any_input(), any_input()}); + auto nms_m = wrap_type({boxes_reshape_m, + scores_unsqueeze_m, + any_input(), + any_input(), + any_input()}); + auto gather_m = wrap_type({nms_m, any_input(), any_input()}); + auto squeeze_m = wrap_type({gather_m, any_input()}); + + ov::matcher_pass_callback callback = [this, + boxes_offset_add_m, + boxes_reshape_m, + scores_unsqueeze_m, + nms_m, + gather_m, + squeeze_m](Matcher& m) { + const auto& pattern_map = m.get_pattern_value_map(); + + auto squeeze = ov::as_type_ptr(pattern_map.at(squeeze_m).get_node_shared_ptr()); + auto gather = ov::as_type_ptr(pattern_map.at(gather_m).get_node_shared_ptr()); + auto nms = ov::as_type_ptr(pattern_map.at(nms_m).get_node_shared_ptr()); + auto boxes_offset_add = pattern_map.at(boxes_offset_add_m).get_node_shared_ptr(); + auto boxes_reshape = ov::as_type_ptr(pattern_map.at(boxes_reshape_m).get_node_shared_ptr()); + auto scores_unsqueeze = ov::as_type_ptr(pattern_map.at(scores_unsqueeze_m).get_node_shared_ptr()); + + if (!squeeze || !gather || !nms || !boxes_reshape || !scores_unsqueeze || transformation_callback(squeeze)) { + return false; + } + + if (!is_scalar_constant_value(gather->input_value(1), 2) || + !is_scalar_constant_value(gather->input_value(2), 1) || + !is_scalar_constant_value(squeeze->input_value(1), 1)) { + return false; + } + + ov::Output boxes_source; + ov::Output class_ids_source; + if (!matches_batched_nms_chain(boxes_offset_add, boxes_source, class_ids_source)) { + return false; + } + + const auto raw_scores = scores_unsqueeze->input_value(0); + if (boxes_source.get_partial_shape().rank().is_static() && boxes_source.get_partial_shape().rank().get_length() != 2) { + return false; + } + if (raw_scores.get_partial_shape().rank().is_static() && raw_scores.get_partial_shape().rank().get_length() != 1) { + return false; + } + if (class_ids_source.get_partial_shape().rank().is_static() && class_ids_source.get_partial_shape().rank().get_length() != 1) { + return false; + } + + int64_t max_output_boxes = 0; + float iou_threshold = 0.0f; + float score_threshold = 0.0f; + if (!get_scalar_from_const_source(nms->input_value(2), max_output_boxes) || + !get_scalar_from_const_source(nms->input_value(3), iou_threshold) || + !get_scalar_from_const_source(nms->input_value(4), score_threshold)) { + return false; + } + + if (max_output_boxes < 0 || max_output_boxes > std::numeric_limits::max()) { + return false; + } + + const auto& class_ids_rt_info = class_ids_source.get_node_shared_ptr()->get_rt_info(); + const auto class_count_it = class_ids_rt_info.find(static_class_count_key); + if (class_count_it == class_ids_rt_info.end()) { + return false; + } + const auto class_count = class_count_it->second.as(); + + const auto& squeeze_rt_info = squeeze->get_rt_info(); + const auto prefix_limit_it = squeeze_rt_info.find(prefix_limit_key); + if (prefix_limit_it == squeeze_rt_info.end()) { + return false; + } + const auto prefix_limit = prefix_limit_it->second.as(); + if (prefix_limit <= 0 || prefix_limit > std::numeric_limits::max()) { + return false; + } + + ov::NodeVector new_ops; + + auto boxes_f32 = std::make_shared(boxes_source, ov::element::f32); + auto scores_f32 = std::make_shared(raw_scores, ov::element::f32); + auto classes_count = ov::op::v0::Constant::create(class_ids_source.get_element_type(), ov::Shape{}, {class_count}); + + auto one_hot = std::make_shared( + class_ids_source, + classes_count, + ov::op::v0::Constant::create(ov::element::boolean, ov::Shape{}, {true}), + ov::op::v0::Constant::create(ov::element::boolean, ov::Shape{}, {false}), + -1); + + auto score_expand_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1}); + auto scores_2d = std::make_shared(scores_f32, score_expand_axis); + auto masked_score = ov::op::v0::Constant::create(ov::element::f32, + ov::Shape{}, + {-std::numeric_limits::infinity()}); + auto class_wise_scores_nc = std::make_shared( + one_hot, + scores_2d, + masked_score); + auto scores_transpose = std::make_shared( + class_wise_scores_nc, + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{2}, {1, 0})); + auto batch_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}); + auto class_wise_scores = std::make_shared(scores_transpose, batch_axis); + auto boxes_for_multiclass = std::make_shared(boxes_f32, boxes_reshape->input_value(1), false); + + ov::op::util::MulticlassNmsBase::Attributes attrs; + attrs.sort_result_type = ov::op::util::MulticlassNmsBase::SortResultType::SCORE; + attrs.sort_result_across_batch = false; + attrs.output_type = nms->get_output_element_type(0); + attrs.iou_threshold = iou_threshold; + attrs.score_threshold = score_threshold; + attrs.nms_top_k = static_cast(max_output_boxes); + attrs.keep_top_k = static_cast(prefix_limit); + attrs.background_class = -1; + attrs.nms_eta = 1.0f; + attrs.normalized = true; + + auto multiclass_nms = std::make_shared(boxes_for_multiclass, + class_wise_scores, + attrs); + multiclass_nms->set_friendly_name(nms->get_friendly_name() + "/MulticlassNms"); + + for (const auto& fp32_node : ov::NodeVector{boxes_f32, + scores_f32, + scores_2d, + masked_score, + class_wise_scores_nc, + scores_transpose, + class_wise_scores, + boxes_for_multiclass, + multiclass_nms}) { + ov::disable_conversion(fp32_node, ov::element::f16); + } + + auto valid_selected_indices = std::make_shared( + multiclass_nms->output(1), + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {0}), + multiclass_nms->output(2), + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}), + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {0})); + + auto selected_box_indices = std::make_shared( + valid_selected_indices, + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}), + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1})); + selected_box_indices->set_friendly_name(squeeze->get_friendly_name()); + + new_ops.insert(new_ops.begin(), {boxes_f32, + scores_f32, + classes_count, + one_hot, + scores_2d, + masked_score, + class_wise_scores_nc, + scores_transpose, + class_wise_scores, + boxes_for_multiclass, + multiclass_nms, + valid_selected_indices, + selected_box_indices}); + + ov::copy_runtime_info(m.get_matched_nodes(), new_ops); + ov::replace_node(squeeze, selected_box_indices); + return true; + }; + + auto matcher = std::make_shared(squeeze_m, "ConvertBatchedNmsToMulticlassNms"); + register_matcher(matcher, callback); +} + +} // namespace ov::intel_gpu + diff --git a/src/plugins/intel_gpu/src/plugin/transformations/convert_batched_nms_to_multiclass_nms.hpp b/src/plugins/intel_gpu/src/plugin/transformations/convert_batched_nms_to_multiclass_nms.hpp new file mode 100644 index 00000000000000..1af3286cc3a862 --- /dev/null +++ b/src/plugins/intel_gpu/src/plugin/transformations/convert_batched_nms_to_multiclass_nms.hpp @@ -0,0 +1,29 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/pass/matcher_pass.hpp" +#include "openvino/pass/pass.hpp" + +namespace ov::intel_gpu { + +class MarkBatchedNmsStaticClassCount : public ov::pass::ModelPass { +public: + OPENVINO_MODEL_PASS_RTTI("ov::intel_gpu::MarkBatchedNmsStaticClassCount"); + bool run_on_model(const std::shared_ptr& model) override; +}; + +// Replaces the lowered PyTorch-style batched NMS pattern with a real +// MulticlassNms when the graph still exposes the original 1D selected indices +// output. This removes the artificial coordinate shift used to emulate +// per-class suppression with a single NonMaxSuppression call. +class ConvertBatchedNmsToMulticlassNms : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("ov::intel_gpu::ConvertBatchedNmsToMulticlassNms"); + ConvertBatchedNmsToMulticlassNms(); +}; + +} // namespace ov::intel_gpu + diff --git a/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp b/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp index 0e68e43635a7b2..1c2fd5458d7348 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp +++ b/src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp @@ -77,6 +77,7 @@ #include "openvino/pass/manager.hpp" #include "openvino/pass/sdpa_to_vlsdpa.hpp" #include "ov_ops/gather_matmul_compressed.hpp" +#include "ov_ops/multiclass_nms_ie_internal.hpp" #include "plugin/transformations/bcast_and_pad_zp_buffers.hpp" #include "plugin/transformations/binary_conv_to_conv.hpp" #include "plugin/transformations/clamp_fp16_output.hpp" @@ -100,6 +101,7 @@ #include "plugin/transformations/indirect_kv_cache.hpp" #include "plugin/transformations/keep_moe_3gemm_const_precision.hpp" #include "plugin/transformations/keep_nms_boundary_precision.hpp" +#include "plugin/transformations/convert_batched_nms_to_multiclass_nms.hpp" #include "plugin/transformations/keep_xattention_threshold_precision.hpp" #include "plugin/transformations/kv_cache_compression.hpp" #include "plugin/transformations/kv_cache_fusion.hpp" @@ -716,6 +718,8 @@ void TransformationsPipeline::apply(std::shared_ptr func) { manager.register_pass( ov::element::TypeVector{ov::element::i32, ov::element::u32, ov::element::u16}, add_precision_sensitive_convert); + manager.register_pass(); + manager.register_pass(); manager.register_pass(); // Keep xattention threshold in fp32 to avoid boundary issues caused by fp16 quantization. manager.register_pass(); @@ -1026,7 +1030,25 @@ void TransformationsPipeline::apply(std::shared_ptr func) { const bool keep_precision_sensitive_in_fp32_2 = true; // To convert to f16 input to boolean which is converted to u8, add abs + ceiling + clamp before convert. - type_to_fuse_map type_to_fuse = {{ov::opset10::Convert::get_type_info_static(), fuse_type_to_convert}}; + type_to_fuse_map type_to_fuse = { + {ov::opset10::Convert::get_type_info_static(), fuse_type_to_convert}, + {ov::op::internal::MulticlassNmsIEInternal::get_type_info_static(), + [](const std::shared_ptr& node, const precisions_map& precisions) { + auto multiclass_nms = ov::as_type_ptr(node); + if (!multiclass_nms) { + return false; + } + + const auto output_type = multiclass_nms->get_attrs().output_type; + const auto precision = precisions.find(output_type); + if (precision == precisions.end()) { + return false; + } + + multiclass_nms->set_output_type(precision->second); + return true; + }}, + }; manager.register_pass(int_convert_precision_map, type_to_fuse, keep_precision_sensitive_in_fp32_2, diff --git a/src/plugins/intel_gpu/tests/unit/transformations/convert_batched_nms_to_multiclass_nms_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/convert_batched_nms_to_multiclass_nms_test.cpp new file mode 100644 index 00000000000000..7359c9aa0d5346 --- /dev/null +++ b/src/plugins/intel_gpu/tests/unit/transformations/convert_batched_nms_to_multiclass_nms_test.cpp @@ -0,0 +1,152 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "plugin/transformations/convert_batched_nms_to_multiclass_nms.hpp" + +#include + +#include "openvino/core/model.hpp" +#include "openvino/op/add.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/gather.hpp" +#include "openvino/op/multiply.hpp" +#include "openvino/op/multiclass_nms.hpp" +#include "openvino/op/non_max_suppression.hpp" +#include "openvino/op/parameter.hpp" +#include "openvino/op/reduce_max.hpp" +#include "openvino/op/reshape.hpp" +#include "openvino/op/result.hpp" +#include "openvino/op/slice.hpp" +#include "openvino/op/squeeze.hpp" +#include "openvino/op/unsqueeze.hpp" +#include "openvino/pass/manager.hpp" +#include "ov_ops/multiclass_nms_ie_internal.hpp" + +using namespace ov::intel_gpu; + +namespace { + +std::shared_ptr make_batched_nms_output_model(int64_t gather_column = 2) { + auto boxes = std::make_shared(ov::element::f32, ov::PartialShape{-1, 4}); + auto scores = std::make_shared(ov::element::f32, ov::PartialShape{-1}); + auto class_ids = std::make_shared(ov::element::i64, ov::PartialShape{-1}); + + auto reduce_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{2}, {0, 1}); + auto reduce_max = std::make_shared(boxes, reduce_axis, false); + auto one = std::make_shared( + ov::op::v0::Constant::create(ov::element::f16, ov::Shape{}, {1.0f}), + ov::element::f32); + auto max_plus_one = std::make_shared(reduce_max, one); + auto class_ids_convert = std::make_shared(class_ids, ov::element::f32); + auto offsets = std::make_shared(class_ids_convert, max_plus_one); + auto offsets_unsqueeze = std::make_shared( + offsets, + ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1})); + auto shifted_boxes = std::make_shared(boxes, offsets_unsqueeze); + + auto boxes_shape = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{3}, {1, -1, 4}); + auto boxes_reshape = std::make_shared(shifted_boxes, boxes_shape, false); + auto scores_unsqueeze = std::make_shared( + scores, + ov::op::v0::Constant::create(ov::element::i32, ov::Shape{2}, {0, 1})); + + auto nms = std::make_shared( + boxes_reshape, + scores_unsqueeze, + ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {2000}), + ov::op::v0::Constant::create(ov::element::f32, ov::Shape{}, {0.5f}), + std::make_shared( + ov::op::v0::Constant::create(ov::element::f16, ov::Shape{}, {0.7f}), + ov::element::f32), + ov::op::v9::NonMaxSuppression::BoxEncodingType::CORNER, + true, + ov::element::i64); + + auto gather = std::make_shared( + nms, + ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {gather_column}), + ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {1})); + auto squeeze = std::make_shared( + gather, + ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {1})); + class_ids->get_rt_info()["intel_gpu_batched_nms_static_class_count"] = int64_t{80}; + squeeze->get_rt_info()["intel_gpu_batched_nms_prefix_limit"] = int64_t{100}; + + return std::make_shared( + ov::ResultVector{std::make_shared(squeeze)}, + ov::ParameterVector{boxes, scores, class_ids}); +} + +} // namespace + +TEST(ConvertBatchedNmsToMulticlassNmsTest, ReplacesLoweredBatchedNmsOutputChain) { + auto model = make_batched_nms_output_model(); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + size_t multiclass_nms_count = 0; + size_t nms_count = 0; + size_t gather_count = 0; + for (const auto& node : model->get_ops()) { + if (ov::is_type(node)) { + ++multiclass_nms_count; + } + if (ov::is_type(node)) { + ++nms_count; + } + if (ov::is_type(node)) { + ++gather_count; + } + } + + EXPECT_EQ(multiclass_nms_count, 1); + EXPECT_EQ(nms_count, 0); + EXPECT_EQ(gather_count, 1); + + auto result_input = model->get_results().front()->input_value(0).get_node_shared_ptr(); + auto selected_box_indices = ov::as_type_ptr(result_input); + ASSERT_NE(selected_box_indices, nullptr); + + auto valid_selected_indices = + ov::as_type_ptr(selected_box_indices->input_value(0).get_node_shared_ptr()); + ASSERT_NE(valid_selected_indices, nullptr); + + auto multiclass_nms = ov::as_type_ptr( + valid_selected_indices->input_value(0).get_node_shared_ptr()); + ASSERT_NE(multiclass_nms, nullptr); + EXPECT_EQ(multiclass_nms->get_attrs().sort_result_type, + ov::op::util::MulticlassNmsBase::SortResultType::SCORE); + EXPECT_EQ(multiclass_nms->get_attrs().output_type, ov::element::i64); + EXPECT_EQ(multiclass_nms->get_attrs().nms_top_k, 2000); + EXPECT_EQ(multiclass_nms->get_attrs().keep_top_k, 100); + EXPECT_EQ(multiclass_nms->get_input_element_type(0), ov::element::f32); + EXPECT_EQ(multiclass_nms->get_input_element_type(1), ov::element::f32); + EXPECT_EQ(valid_selected_indices->input_value(2), multiclass_nms->output(2)); +} + +TEST(ConvertBatchedNmsToMulticlassNmsTest, KeepsGenericNmsGatherChainUntouched) { + auto model = make_batched_nms_output_model(1); + + ov::pass::Manager manager; + manager.register_pass(); + manager.run_passes(model); + + size_t multiclass_nms_count = 0; + size_t nms_count = 0; + for (const auto& node : model->get_ops()) { + if (ov::is_type(node)) { + ++multiclass_nms_count; + } + if (ov::is_type(node)) { + ++nms_count; + } + } + + EXPECT_EQ(multiclass_nms_count, 0); + EXPECT_EQ(nms_count, 1); +} + From 14d123983a56d5c86c3568ab75f4af9266fb988e Mon Sep 17 00:00:00 2001 From: "yuan.xiong" Date: Tue, 14 Jul 2026 08:44:48 +0000 Subject: [PATCH 5/6] add empty line to the end of new file Signed-off-by: yuan.xiong --- .../src/plugin/transformations/keep_nms_boundary_precision.cpp | 3 ++- .../src/plugin/transformations/keep_nms_boundary_precision.hpp | 3 ++- .../unit/transformations/keep_nms_boundary_precision_test.cpp | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp index 89fb70e59cf304..44d09d950e7972 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp +++ b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.cpp @@ -268,4 +268,5 @@ KeepNMSBoundaryPrecision::KeepNMSBoundaryPrecision() { register_matcher(matcher, callback); } -} // namespace ov::intel_gpu \ No newline at end of file +} // namespace ov::intel_gpu + diff --git a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.hpp b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.hpp index b83d8593be029c..3a25f9bcabc911 100644 --- a/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.hpp +++ b/src/plugins/intel_gpu/src/plugin/transformations/keep_nms_boundary_precision.hpp @@ -18,4 +18,5 @@ class KeepNMSBoundaryPrecision : public ov::pass::MatcherPass { KeepNMSBoundaryPrecision(); }; -} // namespace ov::intel_gpu \ No newline at end of file +} // namespace ov::intel_gpu + diff --git a/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp b/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp index b212d91f869ebb..4d4c73ed76c0e0 100644 --- a/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp +++ b/src/plugins/intel_gpu/tests/unit/transformations/keep_nms_boundary_precision_test.cpp @@ -168,4 +168,5 @@ TEST(KeepNMSBoundaryPrecisionTest, IgnoresNmsWithoutBatchedOffsetChain) { ASSERT_NE(test_model.nms, nullptr); EXPECT_FALSE(ov::is_conversion_disabled(test_model.nms, ov::element::f16)); EXPECT_EQ(test_model.class_ids_convert, nullptr); -} \ No newline at end of file +} + From b2942d54daeca7a3ef926e09f7c045532a1a53c9 Mon Sep 17 00:00:00 2001 From: "yuan.xiong" Date: Thu, 16 Jul 2026 02:24:03 +0000 Subject: [PATCH 6/6] revert unnecessary code Signed-off-by: yuan.xiong --- .../common_optimizations/nop_elimination.cpp | 9 +-------- .../common_optimizations/simplify_shape_of_sub_graph.cpp | 8 -------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp b/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp index 09031d58ab9496..a563fa291083b9 100644 --- a/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/nop_elimination.cpp @@ -71,14 +71,7 @@ static bool simplify_gather(std::shared_ptr node) { auto indices = gather->input_value(1); // we need to know data and indices shape to infer if gather is Nop - const auto has_only_static_dimensions = [](const PartialShape& shape) { - return shape.rank().is_static() && - std::all_of(shape.begin(), shape.end(), [](const Dimension& dimension) { - return dimension.is_static(); - }); - }; - if (!has_only_static_dimensions(data.get_partial_shape()) || - !has_only_static_dimensions(indices.get_partial_shape())) { + if (data.get_partial_shape().is_dynamic() || indices.get_partial_shape().is_dynamic()) { return false; } diff --git a/src/common/transformations/src/transformations/common_optimizations/simplify_shape_of_sub_graph.cpp b/src/common/transformations/src/transformations/common_optimizations/simplify_shape_of_sub_graph.cpp index 57547461a04c34..5762fb500b6658 100644 --- a/src/common/transformations/src/transformations/common_optimizations/simplify_shape_of_sub_graph.cpp +++ b/src/common/transformations/src/transformations/common_optimizations/simplify_shape_of_sub_graph.cpp @@ -136,14 +136,6 @@ pass::GatherNopElimination::GatherNopElimination() { matcher_pass_callback callback = [](Matcher& m) { auto gather = m.get_match_root(); - const auto& data_shape = gather->get_input_partial_shape(0); - if (data_shape.rank().is_dynamic() || - std::any_of(data_shape.begin(), data_shape.end(), [](const Dimension& dimension) { - return dimension.is_dynamic(); - })) { - return false; - } - const auto& number_of_indices = shape_size(gather->get_input_shape(1)); if (gather->get_input_shape(0) != gather->get_output_shape(0) || shape_size(gather->get_input_shape(2)) != 1 || number_of_indices > 10)