Skip to content

[core] Propagate value bounds through ConvertLike#36168

Open
evkotov wants to merge 2 commits into
openvinotoolkit:masterfrom
evkotov:CVS-176224-convertlike-bound-eval
Open

[core] Propagate value bounds through ConvertLike#36168
evkotov wants to merge 2 commits into
openvinotoolkit:masterfrom
evkotov:CVS-176224-convertlike-bound-eval

Conversation

@evkotov
Copy link
Copy Markdown
Contributor

@evkotov evkotov commented Jun 1, 2026

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 stop is fed by
Select(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_bounds
breaks at ConvertLike before reaching Select. Constant folding handles this
subgraph (it uses constant_fold, which ConvertLike has), but shape inference
does not (it uses bound evaluation, which ConvertLike lacked).

This change adds evaluate_lower, evaluate_upper, and evaluate_symbol to
ConvertLike, each delegating to an equivalent v0::Convert, which already
supports them. This mirrors the existing constant_fold delegation. With bounds
flowing 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:

  • 176224

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.
@evkotov evkotov requested review from mvafin and praasz June 1, 2026 14:48
@evkotov evkotov self-assigned this Jun 1, 2026
@evkotov evkotov requested a review from a team as a code owner June 1, 2026 14:48
@github-actions github-actions Bot added category: Core OpenVINO Core (aka ngraph) category: CPP API OpenVINO CPP API bindings labels Jun 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and evaluate_symbol by reusing v0::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 stop cascade (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.

Comment thread src/core/tests/type_prop/slice.cpp Outdated
Comment thread src/core/tests/bound_evaluate.cpp Outdated
@CuriousPanCake
Copy link
Copy Markdown
Contributor

@mitruska , please take a look

@CuriousPanCake CuriousPanCake requested a review from mitruska June 2, 2026 14:51
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try having Convert here and have no changes done in convert_like.hpp to confirm this is ConvertLike's problem?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is not required.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const auto bounds = ov::util::evaluate_both_bounds(convert_like);
const auto& [lower, upper] = ov::util::evaluate_both_bounds(convert_like);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category: Core OpenVINO Core (aka ngraph) category: CPP API OpenVINO CPP API bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants