[core] Propagate value bounds through ConvertLike#36168
Conversation
ConvertLike implemented constant_fold but not value bound evaluation, so bounds-based shape inference stalled at a ConvertLike node. A v8::Slice whose stop is fed by Select(Less(size, 0), ConvertLike(ShapeOf(data), size), Add(start, size)) - the shape the TF/TFLite Slice translator emits - was left dynamic even when every input is a static constant, because evaluate_both_bounds breaks at ConvertLike before reaching Select. Delegate ConvertLike::evaluate_lower/upper/symbol to an equivalent v0::Convert (which already supports them), mirroring the existing constant_fold delegation. With ConvertLike propagating bounds, Select resolves its statically-false condition to the Add branch and Slice infers a static output during validate_and_infer_types(), with no ConstantFolding pass required. Add type_prop tests for the cascade (now static) and a Minimum(ShapeOf) control, plus a bound_evaluate test asserting ConvertLike propagates bounds.
There was a problem hiding this comment.
Pull request overview
This PR enables value-bound propagation through ov::op::v1::ConvertLike by delegating bound evaluation (evaluate_lower/upper/symbol) to an equivalent v0::Convert. This unblocks bounds-based shape inference in graphs where ConvertLike sits on the path to shape-relevant ops (notably TF/TFLite Slice translation patterns), allowing validate_and_infer_types() to produce static shapes without requiring a ConstantFolding pass.
Changes:
- Implement
ConvertLike::evaluate_lower,evaluate_upper, andevaluate_symbolby reusingv0::Convert’s existing bound evaluation logic. - Add a bound-evaluator regression test to confirm
ConvertLike(ShapeOf(static), like)yields concrete bounds of the expected element type. - Add Slice type-prop tests reproducing the TF/TFLite
stopcascade (Less+ConvertLike+Select) to ensure shape inference becomes static via bounds propagation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/core/src/op/convert_like.cpp |
Adds lower/upper/symbol bound evaluation delegation via a temporary v0::Convert. |
src/core/include/openvino/op/convert_like.hpp |
Declares the new bound evaluation overrides on the ConvertLike op. |
src/core/tests/bound_evaluate.cpp |
Adds a regression test verifying ConvertLike propagates bounds from ShapeOf. |
src/core/tests/type_prop/slice.cpp |
Adds type-prop coverage for TF/TFLite Slice stop cascade and a control case using Minimum. |
|
@mitruska , please take a look |
| const auto zero = op::v0::Constant::create(et, Shape{}, {0}); | ||
| const auto negative_size_mask = std::make_shared<op::v1::Less>(size, zero); | ||
| const auto shape_of = std::make_shared<op::v3::ShapeOf>(data); | ||
| const auto stop_neg = std::make_shared<op::v1::ConvertLike>(shape_of, size); |
There was a problem hiding this comment.
Did you try having Convert here and have no changes done in convert_like.hpp to confirm this is ConvertLike's problem?
There was a problem hiding this comment.
Yes — confirmed both directions. I swapped ConvertLike for v0::Convert in this exact test with the convert_like.cpp/.hpp change reverted, and the Slice output already comes out static [1,128,4,128] — because Convert already implements evaluate_lower/evaluate_upper/evaluate_symbol (convert.cpp:274-289). With ConvertLike and the fix reverted, the same Slice stays dynamic; with the fix it's static again. So the only missing piece was ConvertLike's value-bound evaluation, which is exactly what this PR adds by delegating to v0::Convert.
To lock this in I added a Convert-based control test (slice_v8_stop_select_cascade_convert_is_static) next to the existing Minimum(ShapeOf) control — it's the same cascade but with the then-branch built from v0::Convert, and it stays static regardless of ConvertLike's own bound support.
Address review feedback on the ConvertLike value-bound tests (no change to the ConvertLike implementation itself): - Reword the Slice `stop` cascade type_prop test comment: `data` is a Parameter with a static shape, not a Constant, so the `stop` subgraph is statically determinable via bounds propagation rather than fully-constant. - Harden the bound_evaluate test: assert the bound tensor shapes before dereferencing and size the copies from `expected.size()`, avoiding an unconditional out-of-bounds read. - Add a Convert-based control type_prop test that pins the cascade dynamism to ConvertLike specifically: v0::Convert has always propagated value bounds, so the same cascade built with Convert resolves to a static Slice regardless of ConvertLike's own bound-evaluation support. Mirrors the existing Minimum(ShapeOf) control.
| bool ConvertLike::evaluate_lower(TensorVector& outputs) const { | ||
| // ConvertLike(data, like) is Convert(data, like.element_type); reuse Convert's bound evaluation so the | ||
| // value bounds propagate through this node (otherwise downstream bounds-based shape inference stalls). | ||
| return v0::Convert(input_value(0), get_output_element_type(0)).evaluate_lower(outputs); |
There was a problem hiding this comment.
| return v0::Convert(input_value(0), get_output_element_type(0)).evaluate_lower(outputs); | |
| return v0::Convert().evaluate_lower(outputs); |
It should work for this function, there should be no need to create full validated operator to use bound evalute.
|
|
||
| const auto slice = std::make_shared<op::v8::Slice>(data, start, stop, step); | ||
|
|
||
| EXPECT_TRUE(slice->get_output_partial_shape(0).is_static()); |
There was a problem hiding this comment.
This redundant as lower the output shape is compared with static shape
|
|
||
| const auto slice = std::make_shared<op::v8::Slice>(data, start, stop, step); | ||
|
|
||
| EXPECT_TRUE(slice->get_output_partial_shape(0).is_static()); |
| const auto like = Constant::create(element::i32, Shape{4}, {0, 0, 0, 0}); | ||
| const auto convert_like = std::make_shared<op::v1::ConvertLike>(shape_of, like); | ||
|
|
||
| const auto bounds = ov::util::evaluate_both_bounds(convert_like); |
There was a problem hiding this comment.
| const auto bounds = ov::util::evaluate_both_bounds(convert_like); | |
| const auto& [lower, upper] = ov::util::evaluate_both_bounds(convert_like); |
Details:
ConvertLike implements constant folding but not value bound evaluation. As a
result, bounds-based shape inference stops at a ConvertLike node.
A v8::Slice whose
stopis fed bySelect(Less(size, 0), ConvertLike(ShapeOf(data), size), Add(start, size))—the subgraph the TF/TFLite Slice translator emits — is left with a dynamic
output even when every input is a static constant, because
evaluate_both_boundsbreaks at ConvertLike before reaching Select. Constant folding handles this
subgraph (it uses
constant_fold, which ConvertLike has), but shape inferencedoes not (it uses bound evaluation, which ConvertLike lacked).
This change adds
evaluate_lower,evaluate_upper, andevaluate_symboltoConvertLike, each delegating to an equivalent
v0::Convert, which alreadysupports them. This mirrors the existing
constant_folddelegation. With boundsflowing through ConvertLike, Select resolves its statically-false condition to
the Add branch, and Slice infers a static output during
validate_and_infer_types()— no ConstantFolding pass needed.Tickets: